Skip to content

Commit

Permalink
GH-218 - handling setters
Browse files Browse the repository at this point in the history
  • Loading branch information
tpodolak committed May 4, 2024
1 parent a4d589a commit 8f9230d
Show file tree
Hide file tree
Showing 22 changed files with 261 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ private void AnalyzeInvocation(OperationAnalysisContext operationAnalysisContext
return;
}

var substituteCallParameters = GetSubstituteCallArgumentOperations(operationAnalysisContext, invocationOperation);
var substitute = GetSubstitute(operationAnalysisContext, invocationOperation);

if (substituteCallParameters == null)
if (substitute == null)
{
return;
}
Expand All @@ -85,79 +85,82 @@ private void AnalyzeInvocation(OperationAnalysisContext operationAnalysisContext
{
var callInfoContext = _callInfoFinder.GetCallInfoContext(argumentExpressionSyntax);

AnalyzeArgAtInvocations(operationAnalysisContext, callInfoContext, substituteCallParameters);
AnalyzeArgAtInvocations(operationAnalysisContext, callInfoContext, substitute);

AnalyzeArgInvocations(operationAnalysisContext, callInfoContext, substituteCallParameters);
AnalyzeArgInvocations(operationAnalysisContext, callInfoContext, substitute);

AnalyzeIndexerInvocations(operationAnalysisContext, callInfoContext, substituteCallParameters);
AnalyzeIndexerInvocations(operationAnalysisContext, callInfoContext, substitute);
}
}

private void AnalyzeIndexerInvocations(OperationAnalysisContext operationAnalysisContext, CallInfoContext callInfoContext, IReadOnlyList<IArgumentOperation> substituteCallParameters)
private void AnalyzeIndexerInvocations(OperationAnalysisContext operationAnalysisContext, CallInfoContext callInfoContext, Substitute substitute)
{
foreach (var indexer in callInfoContext.IndexerAccessesOperations)
{
var indexerInfo = GetIndexerInfo(indexer);

var position = indexer.GetIndexerPosition();

if (AnalyzeArgumentAccess(operationAnalysisContext, substituteCallParameters, indexer, position))
if (AnalyzeArgumentAccess(operationAnalysisContext, substitute, indexer, position))
{
continue;
}

if (AnalyzeCast(operationAnalysisContext, substituteCallParameters, indexer, in indexerInfo, position))
if (AnalyzeCast(operationAnalysisContext, substitute, indexer, in indexerInfo, position))
{
continue;
}

AnalyzeAssignment(operationAnalysisContext, substituteCallParameters, indexer, indexerInfo, position);
AnalyzeAssignment(operationAnalysisContext, substitute, indexer, indexerInfo, position);
}
}

private void AnalyzeArgAtInvocations(OperationAnalysisContext operationAnalysisContext, CallInfoContext callInfoContext, IReadOnlyList<IArgumentOperation> substituteCallParameters)
private void AnalyzeArgAtInvocations(OperationAnalysisContext operationAnalysisContext, CallInfoContext callInfoContext, Substitute substitute)
{
foreach (var argAtInvocation in callInfoContext.ArgAtInvocationsOperations)
{
var position = argAtInvocation.GetIndexerPosition();
if (position.HasValue)

if (position.HasValue == false)
{
continue;
}

if (substitute.HasArgumentAt(position.Value) == false)
{
if (position.Value > substituteCallParameters.Count - 1)
{
var diagnostic = Diagnostic.Create(
DiagnosticDescriptorsProvider.CallInfoArgumentOutOfRange,
argAtInvocation.Syntax.GetLocation(),
position);

operationAnalysisContext.ReportDiagnostic(diagnostic);
continue;
}

var substituteParameterTypeSymbol = substituteCallParameters[position.Value].GetTypeSymbol();
if (substituteParameterTypeSymbol.IsArgAnyType(operationAnalysisContext.Compilation) == false && IsAssignableTo(
operationAnalysisContext.Compilation,
substituteParameterTypeSymbol,
argAtInvocation.TargetMethod.TypeArguments.First()) == false)
{
var diagnostic = Diagnostic.Create(
DiagnosticDescriptorsProvider.CallInfoCouldNotConvertParameterAtPosition,
argAtInvocation.Syntax.GetLocation(),
position,
argAtInvocation.TargetMethod.TypeArguments.First());

operationAnalysisContext.ReportDiagnostic(diagnostic);
}
var diagnostic = Diagnostic.Create(
DiagnosticDescriptorsProvider.CallInfoArgumentOutOfRange,
argAtInvocation.Syntax.GetLocation(),
position);

operationAnalysisContext.ReportDiagnostic(diagnostic);
continue;
}

var substituteParameterTypeSymbol = substitute.GetArgumentTypeAt(position.Value);
if (substituteParameterTypeSymbol.IsArgAnyType(operationAnalysisContext.Compilation) == false && IsAssignableTo(
operationAnalysisContext.Compilation,
substituteParameterTypeSymbol,
argAtInvocation.TargetMethod.TypeArguments.First()) == false)
{
var diagnostic = Diagnostic.Create(
DiagnosticDescriptorsProvider.CallInfoCouldNotConvertParameterAtPosition,
argAtInvocation.Syntax.GetLocation(),
position,
argAtInvocation.TargetMethod.TypeArguments.First());

operationAnalysisContext.ReportDiagnostic(diagnostic);
}
}
}

private void AnalyzeArgInvocations(OperationAnalysisContext operationAnalysisContext, CallInfoContext callInfoContext, IReadOnlyList<IArgumentOperation> substituteCallParameters)
private void AnalyzeArgInvocations(OperationAnalysisContext operationAnalysisContext, CallInfoContext callInfoContext, Substitute substitute)
{
foreach (var argInvocationOperation in callInfoContext.ArgInvocationsOperations)
{
var typeSymbol = argInvocationOperation.TargetMethod.TypeArguments.First();
var parameterCount =
GetMatchingParametersCount(operationAnalysisContext.Compilation, substituteCallParameters, typeSymbol);
GetMatchingParametersCount(operationAnalysisContext.Compilation, substitute, typeSymbol);
if (parameterCount == 0)
{
var diagnostic = Diagnostic.Create(
Expand All @@ -183,25 +186,25 @@ private void AnalyzeArgInvocations(OperationAnalysisContext operationAnalysisCon

private bool AnalyzeArgumentAccess(
OperationAnalysisContext syntaxNodeContext,
IReadOnlyList<IArgumentOperation> substituteCallParameters,
Substitute substitute,
IOperation indexerOperation,
int? position)
{
if (position.HasValue && position.Value > substituteCallParameters.Count - 1)
if (!position.HasValue || substitute.HasArgumentAt(position.Value))
{
var diagnostic = Diagnostic.Create(
DiagnosticDescriptorsProvider.CallInfoArgumentOutOfRange,
indexerOperation.Syntax.GetLocation(),
position.Value);

syntaxNodeContext.ReportDiagnostic(diagnostic);
return true;
return false;
}

return false;
var diagnostic = Diagnostic.Create(
DiagnosticDescriptorsProvider.CallInfoArgumentOutOfRange,
indexerOperation.Syntax.GetLocation(),
position.Value);

syntaxNodeContext.ReportDiagnostic(diagnostic);
return true;
}

private bool AnalyzeCast(OperationAnalysisContext operationAnalysisContext, IReadOnlyList<IArgumentOperation> substituteCallParameters, IOperation indexer, in IndexerInfo indexerInfo, int? position)
private bool AnalyzeCast(OperationAnalysisContext operationAnalysisContext, Substitute substitute, IOperation indexer, in IndexerInfo indexerInfo, int? position)
{
if (!position.HasValue || !indexerInfo.VerifyIndexerCast)
{
Expand All @@ -214,7 +217,7 @@ private bool AnalyzeCast(OperationAnalysisContext operationAnalysisContext, IRea
}

var type = conversionOperation.Type;
var substituteParameterTypeSymbol = substituteCallParameters[position.Value].GetTypeSymbol();
var substituteParameterTypeSymbol = substitute.GetArgumentTypeAt(position.Value);
if (type != null && substituteParameterTypeSymbol.IsArgAnyType(operationAnalysisContext.Compilation) == false &&
CanCast(operationAnalysisContext.Compilation, substituteParameterTypeSymbol, type) == false)
{
Expand All @@ -232,19 +235,19 @@ private bool AnalyzeCast(OperationAnalysisContext operationAnalysisContext, IRea

private bool AnalyzeAssignment(
OperationAnalysisContext operationAnalysisContext,
IReadOnlyList<IArgumentOperation> substituteCallParameters,
Substitute substitute,
IOperation indexerOperation,
in IndexerInfo indexerInfo,
int? position)
{
if (!indexerInfo.VerifyAssignment || !position.HasValue || position.Value >= substituteCallParameters.Count)
if (!indexerInfo.VerifyAssignment || !position.HasValue || !substitute.HasArgumentOperationAt(position.Value))
{
return false;
}

if (indexerOperation is IPropertyReferenceOperation { Parent: ISimpleAssignmentOperation simpleAssignmentOperation })
{
var parameterSymbol = substituteCallParameters[position.Value];
var parameterSymbol = substitute.GetArgumentOperationAt(position.Value);
if (parameterSymbol.Parameter.RefKind != RefKind.Out &&
parameterSymbol.Parameter.RefKind != RefKind.Ref)
{
Expand All @@ -258,7 +261,7 @@ private bool AnalyzeCast(OperationAnalysisContext operationAnalysisContext, IRea
}

var assignmentType = simpleAssignmentOperation.GetTypeSymbol();
var typeSymbol = substituteCallParameters[position.Value].GetArgumentOperationDeclaredTypeSymbol();
var typeSymbol = substitute.GetArgumentTypeAt(position.Value);
if (assignmentType != null &&
IsAssignableTo(operationAnalysisContext.Compilation, assignmentType, typeSymbol) == false)
{
Expand All @@ -276,7 +279,7 @@ private bool AnalyzeCast(OperationAnalysisContext operationAnalysisContext, IRea
return false;
}

private IReadOnlyList<IArgumentOperation>? GetSubstituteCallArgumentOperations(OperationAnalysisContext operationAnalysisContext, IInvocationOperation invocationOperation)
private Substitute? GetSubstitute(OperationAnalysisContext operationAnalysisContext, IInvocationOperation invocationOperation)
{
var substituteOperation = _substitutionOperationFinder
.Find(operationAnalysisContext.Compilation, invocationOperation).FirstOrDefault();
Expand All @@ -286,20 +289,7 @@ private bool AnalyzeCast(OperationAnalysisContext operationAnalysisContext, IRea
return null;
}

var argumentOperations = GetArgumentOperations(substituteOperation);

return argumentOperations?.OrderBy(argOperation => argOperation.Parameter.Ordinal).ToList();
}

private static IEnumerable<IArgumentOperation>? GetArgumentOperations(IOperation substituteOperation)
{
return substituteOperation switch
{
IInvocationOperation substituteMethodSymbol => substituteMethodSymbol.Arguments,
IPropertyReferenceOperation propertySymbol => propertySymbol.Arguments,
IConversionOperation conversionOperation => GetArgumentOperations(conversionOperation.Operand),
_ => null
};
return Substitute.TryCreate(substituteOperation);
}

private IndexerInfo GetIndexerInfo(IOperation indexerOperation)
Expand All @@ -320,18 +310,17 @@ private IndexerInfo GetIndexerInfo(IOperation indexerOperation)

// See https://github.com/nsubstitute/NSubstitute/blob/26d0b0b880c623ef8cae8a0a71360ae2a9982f53/src/NSubstitute/Core/CallInfo.cs#L70
// for the logic behind it
private int GetMatchingParametersCount(Compilation compilation, IReadOnlyList<IArgumentOperation> substituteCallParameters, ITypeSymbol typeSymbol)
private int GetMatchingParametersCount(Compilation compilation, Substitute substitute, ITypeSymbol typeSymbol)
{
var declaringTypeMatchCount =
substituteCallParameters.Count(param => param.GetArgumentOperationDeclaredTypeSymbol().Equals(typeSymbol));
substitute.ArgumentsTypeSymbols.Count(type => type.Equals(typeSymbol));

if (declaringTypeMatchCount > 0)
{
return declaringTypeMatchCount;
}

return substituteCallParameters.Count(param =>
IsAssignableTo(compilation, param.GetTypeSymbol(), typeSymbol));
return substitute.ArgumentsTypeSymbols.Count(type => IsAssignableTo(compilation, type, typeSymbol));
}

private struct IndexerInfo
Expand Down
85 changes: 85 additions & 0 deletions src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/Substitute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Operations;
using NSubstitute.Analyzers.Shared.Extensions;

namespace NSubstitute.Analyzers.Shared.DiagnosticAnalyzers;

internal class Substitute
{
private readonly IReadOnlyList<IArgumentOperation> _argumentOperations;

public IReadOnlyList<ITypeSymbol> ArgumentsTypeSymbols { get; }

private Substitute(
IInvocationOperation? invocationOperation,
IPropertyReferenceOperation? propertyReferenceOperation)
{
var argumentOperations = invocationOperation?.Arguments ?? propertyReferenceOperation?.Arguments ??
throw new ArgumentNullException(nameof(invocationOperation), $"Both {nameof(invocationOperation)} and {nameof(propertyReferenceOperation)} are null");

_argumentOperations = argumentOperations.OrderBy(argOperation => argOperation.Parameter.Ordinal).ToList();
var typeSymbols = _argumentOperations.Select(argOperation => argOperation.GetTypeSymbol()).ToList();

// when property has a setter, we might have one more call argument than parameters in property/indexer
// depending on whether value is set as a part of substitute or not
// as we cant easily determine if value was set or not, we assume it was set in order not to report false positives
if (propertyReferenceOperation?.Property.SetMethod != null)
{
typeSymbols.Add(propertyReferenceOperation.Property.Type);
}

ArgumentsTypeSymbols = typeSymbols;
}

public static Substitute? TryCreate(IOperation operation)
{
var actualOperation = GetActualOperation(operation);

if (actualOperation == null)
{
return null;
}

return new Substitute(
actualOperation.Value.InvocationOperation,
actualOperation.Value.PropertyReferenceOperation);
}

public bool HasArgumentAt(int position) => position >= 0 && position <= ArgumentsTypeSymbols.Count - 1;

public bool HasArgumentOperationAt(int position) => position >= 0 && position <= _argumentOperations.Count - 1;

public ITypeSymbol GetArgumentTypeAt(int position)
{
if (position <= ArgumentsTypeSymbols.Count - 1)
{
return ArgumentsTypeSymbols[position];
}

throw new ArgumentException($"Could not find argument type at position {position}", nameof(position));
}

public IArgumentOperation GetArgumentOperationAt(int position)
{
if (position <= this._argumentOperations.Count - 1)
{
return this._argumentOperations[position];
}

throw new ArgumentException($"Could not find argument at position {position}", nameof(position));
}

private static (IPropertyReferenceOperation? PropertyReferenceOperation, IInvocationOperation? InvocationOperation)? GetActualOperation(IOperation substituteOperation)
{
return substituteOperation switch
{
IInvocationOperation invocationOperation => (null, invocationOperation),
IPropertyReferenceOperation propertyReferenceOperation => (PropertyReferenceOperation: propertyReferenceOperation, null),
IConversionOperation conversionOperation => GetActualOperation(conversionOperation.Operand),
_ => null
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ public void Test()
await VerifyNoDiagnostic(source);
}

public override Task ReportsNoDiagnostic_WhenAccessingIndexerValueArgument(string method, string call, string argAccess)
{
throw new System.NotImplementedException();
}

public override async Task ReportsNoDiagnostic_WhenAccessingArgumentWithinBoundsForNestedCall(string method)
{
var source = $@"using System;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ public void Test()
await VerifyNoDiagnostic(source);
}

public override Task ReportsNoDiagnostic_WhenAccessingIndexerValueArgument(string method, string call, string argAccess)
{
throw new System.NotImplementedException();
}

public override async Task ReportsNoDiagnostic_WhenAccessingArgumentWithinBoundsForNestedCall(string method)
{
var source = $@"using System;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ public abstract class CallInfoDiagnosticVerifier : CSharpDiagnosticVerifier, ICa
[InlineData("substitute.Bar(Arg.Any<int>())", "var x = callInfo.ArgTypes()[1];")]
public abstract Task ReportsNoDiagnostic_WhenAccessingArgumentWithinBounds(string method, string call, string argAccess);

[CombinatoryTheory]
[InlineData("substitute[Arg.Any<int>()]", "var x = callInfo.ArgTypes()[2];")]
public abstract Task ReportsNoDiagnostic_WhenAccessingIndexerValueArgument(string method, string call, string argAccess);

[CombinatoryTheory]
[InlineData]
public abstract Task ReportsNoDiagnostic_WhenAccessingArgumentWithinBoundsForNestedCall(string method);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ public void Test()
await VerifyNoDiagnostic(source);
}

public override Task ReportsNoDiagnostic_WhenAccessingIndexerValueArgument(string method, string call, string argAccess)
{
throw new System.NotImplementedException();
}

public override async Task ReportsNoDiagnostic_WhenAccessingArgumentWithinBoundsForNestedCall(string method)
{
var source = $@"using System;
Expand Down

0 comments on commit 8f9230d

Please sign in to comment.