diff --git a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoAnalyzer.cs b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoAnalyzer.cs index e18c3f06..4e6b21a2 100644 --- a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoAnalyzer.cs +++ b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoAnalyzer.cs @@ -13,54 +13,17 @@ namespace NSubstitute.Analyzers.CSharp.DiagnosticAnalyzers internal class CallInfoAnalyzer : AbstractCallInfoAnalyzer { public CallInfoAnalyzer() - : base(new DiagnosticDescriptorsProvider()) + : base(new DiagnosticDescriptorsProvider(), new CallInfoCallFinder(), new SubstitutionNodeFinder()) { } protected override SyntaxKind InvocationExpressionKind { get; } = SyntaxKind.InvocationExpression; - protected override SyntaxNode GetSubstituteCall(SyntaxNodeAnalysisContext syntaxNodeContext, IMethodSymbol methodSymbol, InvocationExpressionSyntax invocationExpressionSyntax) - { - if (methodSymbol.IsExtensionMethod) - { - switch (methodSymbol.MethodKind) - { - case MethodKind.ReducedExtension: - return invocationExpressionSyntax.Expression.DescendantNodes().First(); - case MethodKind.Ordinary: - return invocationExpressionSyntax.ArgumentList.Arguments.First().Expression; - default: - return null; - } - } - - var parentInvocation = invocationExpressionSyntax.GetParentInvocationExpression(); - - if (parentInvocation == null) - { - return null; - } - - var symbol = syntaxNodeContext.SemanticModel.GetSymbolInfo(parentInvocation); - - if (symbol.Symbol is IMethodSymbol mSymbol && mSymbol.ReducedFrom == null) - { - return parentInvocation.ArgumentList.Arguments.First().Expression; - } - - return parentInvocation.Expression.DescendantNodes().First(); - } - protected override IEnumerable GetArgumentExpressions(InvocationExpressionSyntax invocationExpressionSyntax) { return invocationExpressionSyntax.ArgumentList.Arguments.Select(arg => arg.Expression); } - protected override AbstractCallInfoFinder GetCallInfoFinder() - { - return new CallInfoCallFinder(); - } - protected override SyntaxNode GetCastTypeExpression(ElementAccessExpressionSyntax indexerExpressionSyntax) { switch (indexerExpressionSyntax.Parent) diff --git a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoCallFinder.cs b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoCallFinder.cs index a27436c1..c6c1e14a 100644 --- a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoCallFinder.cs +++ b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoCallFinder.cs @@ -7,9 +7,9 @@ namespace NSubstitute.Analyzers.CSharp.DiagnosticAnalyzers { - internal class CallInfoCallFinder : AbstractCallInfoFinder + internal class CallInfoCallFinder : ICallInfoFinder { - public override CallInfoContext GetCallInfoContext(SemanticModel semanticModel, SyntaxNode syntaxNode) + public CallInfoContext GetCallInfoContext(SemanticModel semanticModel, SyntaxNode syntaxNode) { var visitor = new CallInfoVisitor(semanticModel); visitor.Visit(syntaxNode); diff --git a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/ConflictingArgumentAssignmentsAnalyzer.cs b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/ConflictingArgumentAssignmentsAnalyzer.cs index e2d78a83..e9100f96 100644 --- a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/ConflictingArgumentAssignmentsAnalyzer.cs +++ b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/ConflictingArgumentAssignmentsAnalyzer.cs @@ -14,7 +14,7 @@ namespace NSubstitute.Analyzers.CSharp.DiagnosticAnalyzers internal class ConflictingArgumentAssignmentsAnalyzer : AbstractConflictingArgumentAssignmentsAnalyzer { public ConflictingArgumentAssignmentsAnalyzer() - : base(new DiagnosticDescriptorsProvider()) + : base(new DiagnosticDescriptorsProvider(), new CallInfoCallFinder()) { } @@ -30,11 +30,6 @@ protected override SyntaxNode GetSubstituteCall(SyntaxNodeAnalysisContext syntax return invocationExpressionSyntax.GetParentInvocationExpression(); } - protected override AbstractCallInfoFinder GetCallInfoFinder() - { - return new CallInfoCallFinder(); - } - protected override int? GetIndexerPosition(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, ElementAccessExpressionSyntax indexerExpressionSyntax) { var position = syntaxNodeAnalysisContext.SemanticModel.GetConstantValue(indexerExpressionSyntax.ArgumentList.Arguments.First().Expression); diff --git a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/NonSubstitutableMemberWhenAnalyzer.cs b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/NonSubstitutableMemberWhenAnalyzer.cs index 021c10b2..065b3064 100644 --- a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/NonSubstitutableMemberWhenAnalyzer.cs +++ b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/NonSubstitutableMemberWhenAnalyzer.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -12,62 +13,10 @@ namespace NSubstitute.Analyzers.CSharp.DiagnosticAnalyzers internal class NonSubstitutableMemberWhenAnalyzer : AbstractNonSubstitutableMemberWhenAnalyzer { public NonSubstitutableMemberWhenAnalyzer() - : base(new DiagnosticDescriptorsProvider()) + : base(new DiagnosticDescriptorsProvider(), new SubstitutionNodeFinder()) { } protected override SyntaxKind InvocationExpressionKind { get; } = SyntaxKind.InvocationExpression; - - protected override IEnumerable GetExpressionsForAnalysys(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, IMethodSymbol methodSymbol, InvocationExpressionSyntax invocationExpressionSyntax) - { - var argumentListArguments = invocationExpressionSyntax.ArgumentList.Arguments; - var argumentSyntax = methodSymbol.MethodKind == MethodKind.ReducedExtension ? argumentListArguments.First() : argumentListArguments.Skip(1).First(); - return GetExpressionsForAnalysys(syntaxNodeAnalysisContext, argumentSyntax.Expression); - } - - private IEnumerable GetExpressionsForAnalysys(SyntaxNodeAnalysisContext syntaxNodeContext, SyntaxNode argumentSyntax) - { - SyntaxNode body = null; - switch (argumentSyntax) - { - case SimpleLambdaExpressionSyntax simpleLambdaExpressionSyntax: - body = simpleLambdaExpressionSyntax.Body; - break; - case AnonymousFunctionExpressionSyntax anonymousFunctionExpressionSyntax: - body = anonymousFunctionExpressionSyntax.Body; - break; - case LocalFunctionStatementSyntax localFunctionStatementSyntax: - body = (SyntaxNode)localFunctionStatementSyntax.Body ?? localFunctionStatementSyntax.ExpressionBody; - break; - case MethodDeclarationSyntax methodDeclarationSyntax: - body = (SyntaxNode)methodDeclarationSyntax.Body ?? methodDeclarationSyntax.ExpressionBody; - break; - case IdentifierNameSyntax identifierNameSyntax: - var symbol = syntaxNodeContext.SemanticModel.GetSymbolInfo(identifierNameSyntax); - if (symbol.Symbol != null && symbol.Symbol.Locations.Any()) - { - var location = symbol.Symbol.Locations.First(); - var syntaxNode = location.SourceTree.GetRoot().FindNode(location.SourceSpan); - - foreach (var expressionsForAnalysy in GetExpressionsForAnalysys(syntaxNodeContext, syntaxNode)) - { - yield return expressionsForAnalysy; - } - } - - break; - } - - if (body == null) - { - yield break; - } - - foreach (var invocationExpressionSyntax in body.DescendantNodes().Where(node => node.IsKind(SyntaxKind.SimpleMemberAccessExpression) || - node.IsKind(SyntaxKind.ElementAccessExpression))) - { - yield return invocationExpressionSyntax; - } - } } } \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/ReEntrantSetupAnalyzer.cs b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/ReEntrantSetupAnalyzer.cs index 219561e2..e7ed366e 100644 --- a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/ReEntrantSetupAnalyzer.cs +++ b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/ReEntrantSetupAnalyzer.cs @@ -13,15 +13,10 @@ namespace NSubstitute.Analyzers.CSharp.DiagnosticAnalyzers internal class ReEntrantSetupAnalyzer : AbstractReEntrantSetupAnalyzer { public ReEntrantSetupAnalyzer() - : base(new DiagnosticDescriptorsProvider()) + : base(new DiagnosticDescriptorsProvider(), new ReEntrantCallFinder()) { } - protected override AbstractReEntrantCallFinder GetReEntrantCallFinder() - { - return new ReEntrantCallFinder(); - } - protected override SyntaxKind InvocationExpressionKind { get; } = SyntaxKind.InvocationExpression; protected override IEnumerable ExtractArguments(InvocationExpressionSyntax invocationExpressionSyntax) diff --git a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/SubstitutionNodeFinder.cs b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/SubstitutionNodeFinder.cs new file mode 100644 index 00000000..18737878 --- /dev/null +++ b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/SubstitutionNodeFinder.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using NSubstitute.Analyzers.CSharp.Extensions; +using NSubstitute.Analyzers.Shared.DiagnosticAnalyzers; + +namespace NSubstitute.Analyzers.CSharp.DiagnosticAnalyzers +{ + /// + /// Finds nodes which are considered to be a part of substitution call. For instance substitute.Bar().Returns(1) will return substitute.Bar() + /// + internal class SubstitutionNodeFinder : AbstractSubstitutionNodeFinder + { + public override IEnumerable FindForWhenExpression(SyntaxNodeAnalysisContext syntaxNodeContext, InvocationExpressionSyntax whenInvocationExpression, IMethodSymbol whenInvocationSymbol = null) + { + if (whenInvocationExpression == null) + { + return Enumerable.Empty(); + } + + whenInvocationSymbol = whenInvocationSymbol ?? syntaxNodeContext.SemanticModel.GetSymbolInfo(whenInvocationExpression).Symbol as IMethodSymbol; + + if (whenInvocationSymbol == null) + { + return Enumerable.Empty(); + } + + var argumentExpression = whenInvocationSymbol.MethodKind == MethodKind.ReducedExtension + ? whenInvocationExpression.ArgumentList.Arguments.First().Expression + : whenInvocationExpression.ArgumentList.Arguments.Skip(1).First().Expression; + + return FindForWhenExpression(syntaxNodeContext, argumentExpression); + } + + public override SyntaxNode FindForAndDoesExpression(SyntaxNodeAnalysisContext syntaxNodeContext, InvocationExpressionSyntax invocationExpression, IMethodSymbol invocationExpressionSymbol) + { + var parentInvocationExpression = invocationExpression?.GetParentInvocationExpression(); + if (parentInvocationExpression == null) + { + return null; + } + + var symbol = syntaxNodeContext.SemanticModel.GetSymbolInfo(parentInvocationExpression); + + return symbol.Symbol is IMethodSymbol methodSymbol && methodSymbol.ReducedFrom == null + ? parentInvocationExpression.ArgumentList.Arguments.First().Expression + : parentInvocationExpression.Expression.DescendantNodes().First(); + } + + public override SyntaxNode FindForStandardExpression(InvocationExpressionSyntax invocationExpressionSyntax, IMethodSymbol invocationExpressionSymbol) + { + switch (invocationExpressionSymbol.MethodKind) + { + case MethodKind.ReducedExtension: + return invocationExpressionSyntax.Expression.DescendantNodes().First(); + case MethodKind.Ordinary: + return invocationExpressionSyntax.ArgumentList.Arguments.First().Expression; + default: + return null; + } + } + + protected override InvocationExpressionSyntax GetParentInvocationExpression(InvocationExpressionSyntax invocationExpressionSyntax) + { + return invocationExpressionSyntax.GetParentInvocationExpression(); + } + + private IEnumerable FindForWhenExpression(SyntaxNodeAnalysisContext syntaxNodeContext, SyntaxNode argumentSyntax) + { + SyntaxNode body = null; + switch (argumentSyntax) + { + case SimpleLambdaExpressionSyntax simpleLambdaExpressionSyntax: + body = simpleLambdaExpressionSyntax.Body; + break; + case AnonymousFunctionExpressionSyntax anonymousFunctionExpressionSyntax: + body = anonymousFunctionExpressionSyntax.Body; + break; + case LocalFunctionStatementSyntax localFunctionStatementSyntax: + body = (SyntaxNode)localFunctionStatementSyntax.Body ?? localFunctionStatementSyntax.ExpressionBody; + break; + case MethodDeclarationSyntax methodDeclarationSyntax: + body = (SyntaxNode)methodDeclarationSyntax.Body ?? methodDeclarationSyntax.ExpressionBody; + break; + case IdentifierNameSyntax identifierNameSyntax: + var symbol = syntaxNodeContext.SemanticModel.GetSymbolInfo(identifierNameSyntax); + if (symbol.Symbol != null && symbol.Symbol.Locations.Any()) + { + var location = symbol.Symbol.Locations.First(); + var syntaxNode = location.SourceTree.GetRoot().FindNode(location.SourceSpan); + + foreach (var expressionForAnalysis in FindForWhenExpression(syntaxNodeContext, syntaxNode)) + { + yield return expressionForAnalysis; + } + } + + break; + } + + if (body == null) + { + yield break; + } + + foreach (var invocationExpressionSyntax in body.DescendantNodes().Where(node => node.IsKind(SyntaxKind.SimpleMemberAccessExpression) || + node.IsKind(SyntaxKind.ElementAccessExpression))) + { + yield return invocationExpressionSyntax; + } + } + } +} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractCallInfoAnalyzer.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractCallInfoAnalyzer.cs index 2c7db134..4a2a9f3e 100644 --- a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractCallInfoAnalyzer.cs +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractCallInfoAnalyzer.cs @@ -14,14 +14,17 @@ internal abstract class AbstractCallInfoAnalyzer> _callInfoFinderProxy; + private readonly ICallInfoFinder _callInfoFinder; + private readonly ISubstitutionNodeFinder _substitutionNodeFinder; - private AbstractCallInfoFinder CallInfoFinder => _callInfoFinderProxy.Value; - - protected AbstractCallInfoAnalyzer(IDiagnosticDescriptorsProvider diagnosticDescriptorsProvider) + protected AbstractCallInfoAnalyzer( + IDiagnosticDescriptorsProvider diagnosticDescriptorsProvider, + ICallInfoFinder callInfoFinder, + ISubstitutionNodeFinder substitutionNodeFinder) : base(diagnosticDescriptorsProvider) { - _callInfoFinderProxy = new Lazy>(GetCallInfoFinder); + _callInfoFinder = callInfoFinder; + _substitutionNodeFinder = substitutionNodeFinder; } private static readonly ImmutableDictionary MethodNames = new Dictionary() @@ -30,7 +33,8 @@ protected AbstractCallInfoAnalyzer(IDiagnosticDescriptorsProvider diagnosticDesc [MetadataNames.NSubstituteReturnsForAnyArgsMethod] = MetadataNames.NSubstituteSubstituteExtensionsFullTypeName, [MetadataNames.NSubstituteThrowsMethod] = MetadataNames.NSubstituteExceptionExtensionsFullTypeName, [MetadataNames.NSubstituteThrowsForAnyArgsMethod] = MetadataNames.NSubstituteExceptionExtensionsFullTypeName, - [MetadataNames.NSubstituteAndDoesMethod] = MetadataNames.NSubstituteConfiguredCallFullTypeName + [MetadataNames.NSubstituteAndDoesMethod] = MetadataNames.NSubstituteConfiguredCallFullTypeName, + [MetadataNames.NSubstituteDoMethod] = MetadataNames.NSubstituteWhenCalledType }.ToImmutableDictionary(); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( @@ -48,12 +52,8 @@ public override void Initialize(AnalysisContext context) protected abstract TSyntaxKind InvocationExpressionKind { get; } - protected abstract SyntaxNode GetSubstituteCall(SyntaxNodeAnalysisContext syntaxNodeContext, IMethodSymbol methodSymbol, TInvocationExpressionSyntax invocationExpressionSyntax); - protected abstract IEnumerable GetArgumentExpressions(TInvocationExpressionSyntax invocationExpressionSyntax); - protected abstract AbstractCallInfoFinder GetCallInfoFinder(); - protected abstract SyntaxNode GetCastTypeExpression(TIndexerExpressionSyntax indexerExpressionSyntax); protected abstract SyntaxNode GetAssignmentExpression(TIndexerExpressionSyntax indexerExpressionSyntax); @@ -68,6 +68,37 @@ public override void Initialize(AnalysisContext context) protected abstract bool IsAssignableTo(Compilation compilation, ITypeSymbol fromSymbol, ITypeSymbol toSymbol); + private bool SupportsCallInfo(SyntaxNodeAnalysisContext syntaxNodeContext, TInvocationExpressionSyntax syntax, IMethodSymbol methodSymbol) + { + if (MethodNames.TryGetValue(methodSymbol.Name, out var typeName) == false) + { + return false; + } + + var symbol = syntaxNodeContext.SemanticModel.GetSymbolInfo(syntax); + + var supportsCallInfo = + symbol.Symbol?.ContainingAssembly?.Name.Equals(MetadataNames.NSubstituteAssemblyName, StringComparison.OrdinalIgnoreCase) == true && + (symbol.Symbol?.ContainingType?.ToString().Equals(typeName, StringComparison.OrdinalIgnoreCase) == true || + (symbol.Symbol.ContainingType?.ConstructedFrom.Name)?.Equals(typeName, StringComparison.OrdinalIgnoreCase) == true); + + if (supportsCallInfo == false) + { + return false; + } + + var allArguments = GetArgumentExpressions(syntax); + IEnumerable argumentsForAnalysis; + if (methodSymbol.MethodKind == MethodKind.ReducedExtension) + argumentsForAnalysis = allArguments; + else if (methodSymbol.IsExtensionMethod) + argumentsForAnalysis = allArguments.Skip(1); + else + argumentsForAnalysis = allArguments; + + return argumentsForAnalysis.Any(arg => syntaxNodeContext.SemanticModel.GetTypeInfo(arg).IsCallInfoDelegate(syntaxNodeContext.SemanticModel)); + } + private void AnalyzeInvocation(SyntaxNodeAnalysisContext syntaxNodeContext) { var invocationExpression = (TInvocationExpressionSyntax)syntaxNodeContext.Node; @@ -93,7 +124,7 @@ private void AnalyzeInvocation(SyntaxNodeAnalysisContext syntaxNodeContext) foreach (var argumentExpressionSyntax in GetArgumentExpressions(invocationExpression)) { - var callInfoContext = CallInfoFinder.GetCallInfoContext(syntaxNodeContext.SemanticModel, argumentExpressionSyntax); + var callInfoContext = _callInfoFinder.GetCallInfoContext(syntaxNodeContext.SemanticModel, argumentExpressionSyntax); AnalyzeArgAtInvocations(syntaxNodeContext, callInfoContext, substituteCallParameters); @@ -264,34 +295,9 @@ private bool AnalyzeAssignment(SyntaxNodeAnalysisContext syntaxNodeContext, ILis return false; } - private bool SupportsCallInfo(SyntaxNodeAnalysisContext syntaxNodeContext, TInvocationExpressionSyntax syntax, IMethodSymbol methodSymbol) - { - if (MethodNames.TryGetValue(methodSymbol.Name, out var typeName) == false) - { - return false; - } - - var allArguments = GetArgumentExpressions(syntax); - IEnumerable argumentsForAnalysis; - if (methodSymbol.MethodKind == MethodKind.ReducedExtension) - argumentsForAnalysis = allArguments; - else if (methodSymbol.IsExtensionMethod) - argumentsForAnalysis = allArguments.Skip(1); - else - argumentsForAnalysis = allArguments; - - var symbol = syntaxNodeContext.SemanticModel.GetSymbolInfo(syntax); - - var supportsCallInfo = - symbol.Symbol?.ContainingAssembly?.Name.Equals(MetadataNames.NSubstituteAssemblyName, StringComparison.OrdinalIgnoreCase) == true && - symbol.Symbol?.ContainingType?.ToString().Equals(typeName, StringComparison.OrdinalIgnoreCase) == true; - - return supportsCallInfo && argumentsForAnalysis.Any(arg => syntaxNodeContext.SemanticModel.GetTypeInfo(arg).IsCallInfoDelegate(syntaxNodeContext.SemanticModel)); - } - private IList GetSubstituteCallParameters(SyntaxNodeAnalysisContext syntaxNodeContext, IMethodSymbol methodSymbol, TInvocationExpressionSyntax invocationExpression) { - var parentMethodCallSyntax = GetSubstituteCall(syntaxNodeContext, methodSymbol, invocationExpression); + var parentMethodCallSyntax = _substitutionNodeFinder.Find(syntaxNodeContext, invocationExpression, methodSymbol).FirstOrDefault(); if (parentMethodCallSyntax == null) { diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractCallInfoFinder.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractCallInfoFinder.cs deleted file mode 100644 index 28e0f20e..00000000 --- a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractCallInfoFinder.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.CodeAnalysis; - -namespace NSubstitute.Analyzers.Shared.DiagnosticAnalyzers -{ - internal abstract class AbstractCallInfoFinder - { - public abstract CallInfoContext GetCallInfoContext(SemanticModel semanticModel, SyntaxNode syntaxNode); - } -} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractConflictingArgumentAssignmentsAnalyzer.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractConflictingArgumentAssignmentsAnalyzer.cs index 69fbc237..f4cf8e76 100644 --- a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractConflictingArgumentAssignmentsAnalyzer.cs +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractConflictingArgumentAssignmentsAnalyzer.cs @@ -14,9 +14,12 @@ internal abstract class AbstractConflictingArgumentAssignmentsAnalyzer _callInfoFinder; + + protected AbstractConflictingArgumentAssignmentsAnalyzer(IDiagnosticDescriptorsProvider diagnosticDescriptorsProvider, ICallInfoFinder callInfoFinder) : base(diagnosticDescriptorsProvider) { + _callInfoFinder = callInfoFinder; SupportedDiagnostics = ImmutableArray.Create(DiagnosticDescriptorsProvider.ConflictingArgumentAssignments); } @@ -38,8 +41,6 @@ public override void Initialize(AnalysisContext context) protected abstract SyntaxNode GetSubstituteCall(SyntaxNodeAnalysisContext syntaxNodeContext, IMethodSymbol methodSymbol, TInvocationExpressionSyntax invocationExpressionSyntax); - protected abstract AbstractCallInfoFinder GetCallInfoFinder(); - protected abstract int? GetIndexerPosition(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, TIndexerExpressionSyntax indexerExpressionSyntax); protected abstract ISymbol GetIndexerSymbol(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, TIndexerExpressionSyntax indexerExpressionSyntax); @@ -110,7 +111,7 @@ private bool IsAssigned(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, TIn private IEnumerable FindCallInfoIndexers(SyntaxNodeAnalysisContext syntaxNodeContext, TInvocationExpressionSyntax invocationExpressionSyntax) { - return GetArgumentExpressions(invocationExpressionSyntax).SelectMany(argument => GetCallInfoFinder().GetCallInfoContext(syntaxNodeContext.SemanticModel, argument).IndexerAccesses) + return GetArgumentExpressions(invocationExpressionSyntax).SelectMany(argument => _callInfoFinder.GetCallInfoContext(syntaxNodeContext.SemanticModel, argument).IndexerAccesses) .Where(indexerExpression => IsAssigned(syntaxNodeContext, indexerExpression)); } } diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableMemberWhenAnalyzer.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableMemberWhenAnalyzer.cs index 6eaa0ab9..c76f424e 100644 --- a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableMemberWhenAnalyzer.cs +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableMemberWhenAnalyzer.cs @@ -13,6 +13,8 @@ internal abstract class AbstractNonSubstitutableMemberWhenAnalyzer _substitutionNodeFinder; + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( DiagnosticDescriptorsProvider.NonVirtualWhenSetupSpecification, DiagnosticDescriptorsProvider.InternalSetupSpecification); @@ -23,9 +25,10 @@ internal abstract class AbstractNonSubstitutableMemberWhenAnalyzer substitutionNodeFinder) : base(diagnosticDescriptorsProvider) { + _substitutionNodeFinder = substitutionNodeFinder; } public override void Initialize(AnalysisContext context) @@ -33,8 +36,6 @@ public override void Initialize(AnalysisContext context) context.RegisterSyntaxNodeAction(AnalyzeInvocation, InvocationExpressionKind); } - protected abstract IEnumerable GetExpressionsForAnalysys(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, IMethodSymbol methodSymbol, TInvocationExpressionSyntax invocationExpressionSyntax); - private void AnalyzeInvocation(SyntaxNodeAnalysisContext syntaxNodeContext) { var invocationExpression = (TInvocationExpressionSyntax)syntaxNodeContext.Node; @@ -56,7 +57,7 @@ private void AnalyzeInvocation(SyntaxNodeAnalysisContext syntaxNodeContext) return; } - var expressionsForAnalysys = GetExpressionsForAnalysys(syntaxNodeContext, methodSymbol, invocationExpression); + var expressionsForAnalysys = _substitutionNodeFinder.FindForWhenExpression(syntaxNodeContext, invocationExpression, methodSymbol); var typeSymbol = methodSymbol.TypeArguments.FirstOrDefault() ?? methodSymbol.ReceiverType; foreach (var analysedSyntax in expressionsForAnalysys) { diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractReEntrantCallFinder.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractReEntrantCallFinder.cs index c28cf09d..b393413e 100644 --- a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractReEntrantCallFinder.cs +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractReEntrantCallFinder.cs @@ -7,7 +7,7 @@ namespace NSubstitute.Analyzers.Shared.DiagnosticAnalyzers { - internal abstract class AbstractReEntrantCallFinder + internal abstract class AbstractReEntrantCallFinder : IReEntrantCallFinder { private static readonly ImmutableDictionary MethodNames = new Dictionary() { diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractReEntrantSetupAnalyzer.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractReEntrantSetupAnalyzer.cs index 1a28c982..2f074cd6 100644 --- a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractReEntrantSetupAnalyzer.cs +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractReEntrantSetupAnalyzer.cs @@ -11,25 +11,21 @@ internal abstract class AbstractReEntrantSetupAnalyzer MethodNames = ImmutableHashSet.Create( MetadataNames.NSubstituteReturnsMethod, MetadataNames.NSubstituteReturnsForAnyArgsMethod); - private AbstractReEntrantCallFinder ReEntrantCallFinder => _reEntrantCallFinderProxy.Value; - - private readonly Lazy _reEntrantCallFinderProxy; - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptorsProvider.ReEntrantSubstituteCall); - protected AbstractReEntrantSetupAnalyzer(IDiagnosticDescriptorsProvider diagnosticDescriptorsProvider) + protected AbstractReEntrantSetupAnalyzer(IDiagnosticDescriptorsProvider diagnosticDescriptorsProvider, IReEntrantCallFinder reEntrantCallFinder) : base(diagnosticDescriptorsProvider) { - _reEntrantCallFinderProxy = new Lazy(GetReEntrantCallFinder); + _reEntrantCallFinder = reEntrantCallFinder; } - protected abstract AbstractReEntrantCallFinder GetReEntrantCallFinder(); - protected abstract TSyntaxKind InvocationExpressionKind { get; } public override void Initialize(AnalysisContext context) @@ -61,7 +57,7 @@ private void AnalyzeInvocation(SyntaxNodeAnalysisContext syntaxNodeContext) foreach (var argument in argumentsForAnalysis) { - var reentrantSymbol = ReEntrantCallFinder.GetReEntrantCalls(syntaxNodeContext.Compilation, argument).FirstOrDefault(); + var reentrantSymbol = _reEntrantCallFinder.GetReEntrantCalls(syntaxNodeContext.Compilation, argument).FirstOrDefault(); if (reentrantSymbol != null) { var diagnostic = Diagnostic.Create( diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractSubstitutionNodeFinder.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractSubstitutionNodeFinder.cs new file mode 100644 index 00000000..32e5af67 --- /dev/null +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractSubstitutionNodeFinder.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace NSubstitute.Analyzers.Shared.DiagnosticAnalyzers +{ + internal abstract class AbstractSubstitutionNodeFinder : ISubstitutionNodeFinder + where TInvocationExpressionSyntax : SyntaxNode + { + public IEnumerable Find(SyntaxNodeAnalysisContext syntaxNodeContext, TInvocationExpressionSyntax invocationExpression, IMethodSymbol invocationExpressionSymbol = null) + { + invocationExpressionSymbol = invocationExpressionSymbol ?? syntaxNodeContext.SemanticModel.GetSymbolInfo(invocationExpression).Symbol as IMethodSymbol; + + if (invocationExpression == null || invocationExpressionSymbol == null || invocationExpressionSymbol.ContainingAssembly.Name.Equals(MetadataNames.NSubstituteAssemblyName, StringComparison.Ordinal) == false) + { + return Enumerable.Empty(); + } + + if (invocationExpressionSymbol.Name.Equals(MetadataNames.NSubstituteDoMethod, StringComparison.Ordinal)) + { + var parentInvocationExpression = GetParentInvocationExpression(invocationExpression); + return FindForWhenExpression(syntaxNodeContext, parentInvocationExpression); + } + + if (invocationExpressionSymbol.Name.Equals(MetadataNames.NSubstituteWhenMethod, StringComparison.Ordinal) || + invocationExpressionSymbol.Name.Equals(MetadataNames.NSubstituteWhenForAnyArgsMethod, StringComparison.Ordinal)) + { + return FindForWhenExpression(syntaxNodeContext, invocationExpression, invocationExpressionSymbol); + } + + if (invocationExpressionSymbol.Name.Equals(MetadataNames.NSubstituteAndDoesMethod, StringComparison.Ordinal)) + { + var substitution = FindForAndDoesExpression(syntaxNodeContext, invocationExpression, invocationExpressionSymbol); + return substitution != null ? new[] { substitution } : Enumerable.Empty(); + } + + var standardSubstitution = FindForStandardExpression(invocationExpression, invocationExpressionSymbol); + + return standardSubstitution != null ? new[] { standardSubstitution } : Enumerable.Empty(); + } + + public abstract IEnumerable FindForWhenExpression(SyntaxNodeAnalysisContext syntaxNodeContext, TInvocationExpressionSyntax whenInvocationExpression, IMethodSymbol whenInvocationSymbol = null); + + public abstract SyntaxNode FindForAndDoesExpression(SyntaxNodeAnalysisContext syntaxNodeContext, TInvocationExpressionSyntax invocationExpression, IMethodSymbol invocationExpressionSymbol = null); + + public abstract SyntaxNode FindForStandardExpression(TInvocationExpressionSyntax invocationExpressionSyntax, IMethodSymbol invocationExpressionSymbol = null); + + protected abstract TInvocationExpressionSyntax GetParentInvocationExpression(TInvocationExpressionSyntax invocationExpressionSyntax); + } +} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/ICallInfoFinder.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/ICallInfoFinder.cs new file mode 100644 index 00000000..32472528 --- /dev/null +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/ICallInfoFinder.cs @@ -0,0 +1,9 @@ +using Microsoft.CodeAnalysis; + +namespace NSubstitute.Analyzers.Shared.DiagnosticAnalyzers +{ + internal interface ICallInfoFinder + { + CallInfoContext GetCallInfoContext(SemanticModel semanticModel, SyntaxNode syntaxNode); + } +} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/IReEntrantCallFinder.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/IReEntrantCallFinder.cs new file mode 100644 index 00000000..157cd09a --- /dev/null +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/IReEntrantCallFinder.cs @@ -0,0 +1,10 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace NSubstitute.Analyzers.Shared.DiagnosticAnalyzers +{ + internal interface IReEntrantCallFinder + { + ImmutableList GetReEntrantCalls(Compilation compilation, SyntaxNode rootNode); + } +} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/ISubstitutionNodeFinder.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/ISubstitutionNodeFinder.cs new file mode 100644 index 00000000..5d80d6b8 --- /dev/null +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/ISubstitutionNodeFinder.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace NSubstitute.Analyzers.Shared.DiagnosticAnalyzers +{ + internal interface ISubstitutionNodeFinder where TInvocationExpressionSyntax : SyntaxNode + { + IEnumerable Find(SyntaxNodeAnalysisContext syntaxNodeContext, TInvocationExpressionSyntax invocationExpression, IMethodSymbol invocationExpressionSymbol = null); + + IEnumerable FindForWhenExpression(SyntaxNodeAnalysisContext syntaxNodeContext, TInvocationExpressionSyntax whenInvocationExpression, IMethodSymbol whenInvocationSymbol = null); + + SyntaxNode FindForAndDoesExpression(SyntaxNodeAnalysisContext syntaxNodeContext, TInvocationExpressionSyntax invocationExpression, IMethodSymbol invocationExpressionSymbol = null); + + SyntaxNode FindForStandardExpression(TInvocationExpressionSyntax invocationExpressionSyntax, IMethodSymbol invocationExpressionSymbol = null); + } +} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/CallInfoAnalyzer.cs b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/CallInfoAnalyzer.cs index 6046999a..4abae668 100644 --- a/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/CallInfoAnalyzer.cs +++ b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/CallInfoAnalyzer.cs @@ -13,54 +13,17 @@ namespace NSubstitute.Analyzers.VisualBasic.DiagnosticAnalyzers internal class CallInfoAnalyzer : AbstractCallInfoAnalyzer { public CallInfoAnalyzer() - : base(new DiagnosticDescriptorsProvider()) + : base(new DiagnosticDescriptorsProvider(), new CallInfoCallFinder(), new SubstitutionNodeFinder()) { } protected override SyntaxKind InvocationExpressionKind { get; } = SyntaxKind.InvocationExpression; - protected override SyntaxNode GetSubstituteCall(SyntaxNodeAnalysisContext syntaxNodeContext, IMethodSymbol methodSymbol, InvocationExpressionSyntax invocationExpressionSyntax) - { - if (methodSymbol.IsExtensionMethod) - { - switch (methodSymbol.MethodKind) - { - case MethodKind.ReducedExtension: - return invocationExpressionSyntax.Expression.DescendantNodes().First(); - case MethodKind.Ordinary: - return invocationExpressionSyntax.ArgumentList.Arguments.First().GetExpression(); - default: - return null; - } - } - - var parentInvocation = invocationExpressionSyntax.GetParentInvocationExpression(); - - if (parentInvocation == null) - { - return null; - } - - var symbol = syntaxNodeContext.SemanticModel.GetSymbolInfo(parentInvocation); - - if (symbol.Symbol is IMethodSymbol mSymbol && mSymbol.ReducedFrom == null) - { - return parentInvocation.ArgumentList.Arguments.First().GetExpression(); - } - - return parentInvocation.Expression.DescendantNodes().First(); - } - protected override IEnumerable GetArgumentExpressions(InvocationExpressionSyntax invocationExpressionSyntax) { return invocationExpressionSyntax.ArgumentList.Arguments.Select(arg => arg.GetExpression()); } - protected override AbstractCallInfoFinder GetCallInfoFinder() - { - return new CallInfoCallFinder(); - } - protected override SyntaxNode GetCastTypeExpression(InvocationExpressionSyntax indexerExpressionSyntax) { if (indexerExpressionSyntax.Parent is CastExpressionSyntax castExpressionSyntax) diff --git a/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/CallInfoCallFinder.cs b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/CallInfoCallFinder.cs index a9f6841f..3f3cd8d4 100644 --- a/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/CallInfoCallFinder.cs +++ b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/CallInfoCallFinder.cs @@ -7,9 +7,9 @@ namespace NSubstitute.Analyzers.VisualBasic.DiagnosticAnalyzers { - internal class CallInfoCallFinder : AbstractCallInfoFinder + internal class CallInfoCallFinder : ICallInfoFinder { - public override CallInfoContext GetCallInfoContext(SemanticModel semanticModel, SyntaxNode syntaxNode) + public CallInfoContext GetCallInfoContext(SemanticModel semanticModel, SyntaxNode syntaxNode) { var visitor = new CallInfoVisitor(semanticModel); visitor.Visit(syntaxNode); diff --git a/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/ConflictingArgumentAssignmentsAnalyzer.cs b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/ConflictingArgumentAssignmentsAnalyzer.cs index 26a941db..675a1642 100644 --- a/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/ConflictingArgumentAssignmentsAnalyzer.cs +++ b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/ConflictingArgumentAssignmentsAnalyzer.cs @@ -13,7 +13,7 @@ namespace NSubstitute.Analyzers.VisualBasic.DiagnosticAnalyzers internal class ConflictingArgumentAssignmentsAnalyzer : AbstractConflictingArgumentAssignmentsAnalyzer { public ConflictingArgumentAssignmentsAnalyzer() - : base(new DiagnosticDescriptorsProvider()) + : base(new DiagnosticDescriptorsProvider(), new CallInfoCallFinder()) { } @@ -29,11 +29,6 @@ protected override SyntaxNode GetSubstituteCall(SyntaxNodeAnalysisContext syntax return invocationExpressionSyntax.GetParentInvocationExpression(); } - protected override AbstractCallInfoFinder GetCallInfoFinder() - { - return new CallInfoCallFinder(); - } - protected override int? GetIndexerPosition(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, InvocationExpressionSyntax indexerExpressionSyntax) { var position = syntaxNodeAnalysisContext.SemanticModel.GetConstantValue(indexerExpressionSyntax.ArgumentList.Arguments.First().GetExpression()); diff --git a/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/NonSubstitutableMemberWhenAnalyzer.cs b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/NonSubstitutableMemberWhenAnalyzer.cs index 9c907663..7037ada1 100644 --- a/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/NonSubstitutableMemberWhenAnalyzer.cs +++ b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/NonSubstitutableMemberWhenAnalyzer.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; @@ -12,103 +13,10 @@ namespace NSubstitute.Analyzers.VisualBasic.DiagnosticAnalyzers internal class NonSubstitutableMemberWhenAnalyzer : AbstractNonSubstitutableMemberWhenAnalyzer { public NonSubstitutableMemberWhenAnalyzer() - : base(new DiagnosticDescriptorsProvider()) + : base(new DiagnosticDescriptorsProvider(), new SubstitutionNodeFinder()) { } protected override SyntaxKind InvocationExpressionKind { get; } = SyntaxKind.InvocationExpression; - - protected override IEnumerable GetExpressionsForAnalysys(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, IMethodSymbol methodSymbol, InvocationExpressionSyntax invocationExpressionSyntax) - { - var argumentListArguments = invocationExpressionSyntax.ArgumentList.Arguments; - var argumentSyntax = methodSymbol.MethodKind == MethodKind.ReducedExtension ? argumentListArguments.First() : argumentListArguments.Skip(1).First(); - return GetExpressionsForAnalysys(syntaxNodeAnalysisContext, argumentSyntax.GetExpression()); - } - - private IEnumerable GetExpressionsForAnalysys(SyntaxNodeAnalysisContext syntaxNodeContext, SyntaxNode syntax) - { - SyntaxNode body = null; - switch (syntax) - { - case SingleLineLambdaExpressionSyntax _: - case ExpressionStatementSyntax _: - case LocalDeclarationStatementSyntax _: - case AssignmentStatementSyntax _: - body = syntax; - break; - case MultiLineLambdaExpressionSyntax simpleLambdaExpressionSyntax: - foreach (var syntaxNode in IterateStatements(simpleLambdaExpressionSyntax.Statements)) - { - yield return syntaxNode; - } - - break; - case MethodBlockSyntax methodBlockSyntax: - foreach (var syntaxNode in IterateStatements(methodBlockSyntax.Statements)) - { - yield return syntaxNode; - } - - break; - case UnaryExpressionSyntax unaryExpressionSyntax: - foreach (var syntaxNode in GetExpressionsForAnalysys(syntaxNodeContext, unaryExpressionSyntax.Operand)) - { - yield return syntaxNode; - } - - break; - case IdentifierNameSyntax identifierNameSyntax: - var symbol = syntaxNodeContext.SemanticModel.GetSymbolInfo(identifierNameSyntax); - if (symbol.Symbol != null && symbol.Symbol.Locations.Any()) - { - var location = symbol.Symbol.Locations.First(); - var syntaxNode = location.SourceTree.GetRoot().FindNode(location.SourceSpan); - - SyntaxNode innerNode = null; - if (syntaxNode is MethodStatementSyntax methodStatementSyntax) - { - innerNode = methodStatementSyntax.Parent; - } - - innerNode = innerNode ?? syntaxNode; - foreach (var expressionsForAnalysy in GetExpressionsForAnalysys(syntaxNodeContext, innerNode)) - { - yield return expressionsForAnalysy; - } - } - - break; - } - - if (body == null) - { - yield break; - } - - var memberAccessExpressions = body.DescendantNodes().Where(node => node.IsKind(SyntaxKind.SimpleMemberAccessExpression)); - var invocationExpressions = body.DescendantNodes().Where(node => node.IsKind(SyntaxKind.InvocationExpression)); - - // rather ugly but prevents reporting two times the same thing - // as VB syntax is based on statements, you can't access body of method directly - if (invocationExpressions.Any()) - { - foreach (var invocationExpression in invocationExpressions) - { - yield return invocationExpression; - } - } - else if (memberAccessExpressions.Any()) - { - foreach (var memberAccessExpression in memberAccessExpressions) - { - yield return memberAccessExpression; - } - } - - IEnumerable IterateStatements(IEnumerable statements) - { - return statements.SelectMany(statement => GetExpressionsForAnalysys(syntaxNodeContext, statement)); - } - } } } \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/ReEntrantSetupAnalyzer.cs b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/ReEntrantSetupAnalyzer.cs index 1d2c2b43..1b0298e9 100644 --- a/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/ReEntrantSetupAnalyzer.cs +++ b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/ReEntrantSetupAnalyzer.cs @@ -12,15 +12,10 @@ namespace NSubstitute.Analyzers.VisualBasic.DiagnosticAnalyzers internal class ReEntrantSetupAnalyzer : AbstractReEntrantSetupAnalyzer { public ReEntrantSetupAnalyzer() - : base(new DiagnosticDescriptorsProvider()) + : base(new DiagnosticDescriptorsProvider(), new ReEntrantCallFinder()) { } - protected override AbstractReEntrantCallFinder GetReEntrantCallFinder() - { - return new ReEntrantCallFinder(); - } - protected override SyntaxKind InvocationExpressionKind { get; } = SyntaxKind.InvocationExpression; protected override IEnumerable ExtractArguments(InvocationExpressionSyntax invocationExpressionSyntax) diff --git a/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/SubstitutionNodeFinder.cs b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/SubstitutionNodeFinder.cs new file mode 100644 index 00000000..87bf81c2 --- /dev/null +++ b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/SubstitutionNodeFinder.cs @@ -0,0 +1,154 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.VisualBasic; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; +using NSubstitute.Analyzers.Shared.DiagnosticAnalyzers; +using NSubstitute.Analyzers.VisualBasic.Extensions; + +namespace NSubstitute.Analyzers.VisualBasic.DiagnosticAnalyzers +{ + internal class SubstitutionNodeFinder : AbstractSubstitutionNodeFinder + { + public override IEnumerable FindForWhenExpression(SyntaxNodeAnalysisContext syntaxNodeContext, InvocationExpressionSyntax whenInvocationExpression, IMethodSymbol whenInvocationSymbol = null) + { + if (whenInvocationExpression == null) + { + return Enumerable.Empty(); + } + + whenInvocationSymbol = whenInvocationSymbol ?? syntaxNodeContext.SemanticModel.GetSymbolInfo(whenInvocationExpression).Symbol as IMethodSymbol; + + if (whenInvocationSymbol == null) + { + return Enumerable.Empty(); + } + + var argumentExpression = whenInvocationSymbol.MethodKind == MethodKind.ReducedExtension + ? whenInvocationExpression.ArgumentList.Arguments.First().GetExpression() + : whenInvocationExpression.ArgumentList.Arguments.Skip(1).First().GetExpression(); + + return FindForWhenExpression(syntaxNodeContext, argumentExpression); + } + + public override SyntaxNode FindForAndDoesExpression(SyntaxNodeAnalysisContext syntaxNodeContext, InvocationExpressionSyntax invocationExpression, IMethodSymbol invocationExpressionSymbol) + { + var parentInvocationExpression = invocationExpression?.GetParentInvocationExpression(); + if (parentInvocationExpression == null) + { + return null; + } + + var symbol = syntaxNodeContext.SemanticModel.GetSymbolInfo(parentInvocationExpression); + + return symbol.Symbol is IMethodSymbol methodSymbol && methodSymbol.ReducedFrom == null + ? parentInvocationExpression.ArgumentList.Arguments.First().GetExpression() + : parentInvocationExpression.Expression.DescendantNodes().First(); + } + + public override SyntaxNode FindForStandardExpression(InvocationExpressionSyntax invocationExpressionSyntax, IMethodSymbol invocationExpressionSymbol) + { + switch (invocationExpressionSymbol.MethodKind) + { + case MethodKind.ReducedExtension: + return invocationExpressionSyntax.Expression.DescendantNodes().First(); + case MethodKind.Ordinary: + return invocationExpressionSyntax.ArgumentList.Arguments.First().GetExpression(); + default: + return null; + } + } + + protected override InvocationExpressionSyntax GetParentInvocationExpression(InvocationExpressionSyntax invocationExpressionSyntax) + { + return invocationExpressionSyntax.GetParentInvocationExpression(); + } + + private IEnumerable FindForWhenExpression(SyntaxNodeAnalysisContext syntaxNodeContext, SyntaxNode argumentSyntax) + { + SyntaxNode body = null; + switch (argumentSyntax) + { + case SingleLineLambdaExpressionSyntax _: + case ExpressionStatementSyntax _: + case LocalDeclarationStatementSyntax _: + case AssignmentStatementSyntax _: + body = argumentSyntax; + break; + case MultiLineLambdaExpressionSyntax simpleLambdaExpressionSyntax: + foreach (var syntaxNode in IterateStatements(simpleLambdaExpressionSyntax.Statements)) + { + yield return syntaxNode; + } + + break; + case MethodBlockSyntax methodBlockSyntax: + foreach (var syntaxNode in IterateStatements(methodBlockSyntax.Statements)) + { + yield return syntaxNode; + } + + break; + case UnaryExpressionSyntax unaryExpressionSyntax: + foreach (var syntaxNode in FindForWhenExpression(syntaxNodeContext, unaryExpressionSyntax.Operand)) + { + yield return syntaxNode; + } + + break; + case IdentifierNameSyntax identifierNameSyntax: + var symbol = ModelExtensions.GetSymbolInfo(syntaxNodeContext.SemanticModel, identifierNameSyntax); + if (symbol.Symbol != null && symbol.Symbol.Locations.Any()) + { + var location = symbol.Symbol.Locations.First(); + var syntaxNode = location.SourceTree.GetRoot().FindNode(location.SourceSpan); + + SyntaxNode innerNode = null; + if (syntaxNode is MethodStatementSyntax methodStatementSyntax) + { + innerNode = methodStatementSyntax.Parent; + } + + innerNode = innerNode ?? syntaxNode; + foreach (var expressionsForAnalysy in FindForWhenExpression(syntaxNodeContext, innerNode)) + { + yield return expressionsForAnalysy; + } + } + + break; + } + + if (body == null) + { + yield break; + } + + var memberAccessExpressions = body.DescendantNodes().Where(node => node.IsKind(SyntaxKind.SimpleMemberAccessExpression)).ToList(); + var invocationExpressions = body.DescendantNodes().Where(node => node.IsKind(SyntaxKind.InvocationExpression)).ToList(); + + // rather ugly but prevents reporting two times the same thing + // as VB syntax is based on statements, you can't access body of method directly + if (invocationExpressions.Any()) + { + foreach (var invocationExpression in invocationExpressions) + { + yield return invocationExpression; + } + } + else if (memberAccessExpressions.Any()) + { + foreach (var memberAccessExpression in memberAccessExpressions) + { + yield return memberAccessExpression; + } + } + + IEnumerable IterateStatements(IEnumerable statements) + { + return statements.SelectMany(statement => FindForWhenExpression(syntaxNodeContext, statement)); + } + } + } +} \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/CallInfoAnalyzerTests/DoMethodPrecededByExtensionMethodTests.cs b/tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/CallInfoAnalyzerTests/DoMethodPrecededByExtensionMethodTests.cs new file mode 100644 index 00000000..ec6ff828 --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/CallInfoAnalyzerTests/DoMethodPrecededByExtensionMethodTests.cs @@ -0,0 +1,664 @@ +using System.Threading.Tasks; +using NSubstitute.Analyzers.Tests.Shared.Extensibility; + +namespace NSubstitute.Analyzers.Tests.CSharp.DiagnosticAnalyzerTests.CallInfoAnalyzerTests +{ + [CombinatoryData("When", "WhenForAnyArgs")] + public class DoMethodPrecededByExtensionMethodTests : CallInfoDiagnosticVerifier + { + public override async Task ReportsNoDiagnostics_WhenSubstituteMethodCannotBeInferred(string method, string call, string argAccess) + { + var source = $@"using System; +using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(int x); + + int Barr {{ get; }} + + int this[int x] {{ get; }} + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + var returnValue = sub.{method}(substitute => {{var _ = {call}; }}); + returnValue.Do(callInfo => + {{ + {argAccess} + }}); + }} + }} +}}"; + + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsDiagnostic_WhenAccessingArgumentOutOfBounds(string method, string call, string argAccess) + { + var source = $@"using System; +using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(int x); + + int Barr {{ get; }} + + int this[int x] {{ get; }} + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + sub.{method}(substitute => {{var _ = {call}; }}).Do(callInfo => + {{ + {argAccess} + }}); + }} + }} +}}"; + await VerifyDiagnostic(source, CallInfoArgumentOutOfRangeDescriptor, "There is no argument at position 1"); + } + + public override async Task ReportsNoDiagnostic_WhenAccessingArgumentOutOfBound_AndPositionIsNotLiteralExpression(string method, string call, string argAccess) + { + var source = $@"using System; +using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(int x, int y); + + int Barr {{ get; }} + + int this[int x, int y] {{ get; }} + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + sub.{method}(substitute => {{var _ = {call}; }}).Do(callInfo => + {{ + {argAccess} + }}); + }} + }} +}}"; + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsNoDiagnostic_WhenAccessingArgumentWithinBounds(string method, string call, string argAccess) + { + var source = $@"using System; +using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(int x, int y); + + int Barr {{ get; }} + + int this[int x, int y] {{ get; }} + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + sub.{method}(substitute => {{var _ = {call}; }}).Do(callInfo => + {{ + {argAccess} + }}); + }} + }} +}}"; + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsNoDiagnostic_WhenManuallyCasting_ToSupportedType(string method, string call, string argAccess) + { + var source = $@"using System; +using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(int x, Bar y); + + int this[int x, Bar y] {{ get; }} + }} + + public class BarBase + {{ + }} + + public class Bar : BarBase + {{ + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + sub.{method}(substitute => {{var _ = {call}; }}).Do(callInfo => + {{ + {argAccess} + }}); + }} + }} +}}"; + + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsDiagnostic_WhenManuallyCasting_ToUnsupportedType(string method, string call, string argAccess) + { + var source = $@"using System; +using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(int x, double y); + + int Foo(int x, FooBar bar); + + int this[int x, double y] {{ get; }} + + int this[int x, FooBar bar] {{ get; }} + }} + + public class Bar + {{ + }} + + public class FooBar : Bar + {{ + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + sub.{method}(substitute => {{var _ = {call}; }}).Do(callInfo => + {{ + {argAccess} + }}); + }} + }} +}}"; + + await VerifyDiagnostic(source, CallInfoCouldNotConvertParameterAtPositionDescriptor, "Couldn't convert parameter at position 1 to type MyNamespace.Bar."); + } + + public override async Task ReportsNoDiagnostic_WhenCasting_WithArgAt_ToSupportedType(string method, string call, string argAccess) + { + var source = $@"using System; +using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(int x, Bar y); + + int this[int x, Bar y] {{ get; }} + }} + + public class BarBase + {{ + }} + + public class Bar : BarBase + {{ + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + sub.{method}(substitute => {{var _ = {call}; }}).Do(callInfo => + {{ + {argAccess} + }}); + }} + }} +}}"; + + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsDiagnostic_WhenCasting_WithArgAt_ToUnsupportedType(string method, string call, string argAccess, string message) + { + var source = $@"using System; +using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(int x, double y); + + int Foo(int x, FooBar bar); + + int this[int x, double y] {{ get; }} + + int this[int x, FooBar bar] {{ get; }} + }} + + public class Bar + {{ + }} + + public class FooBar : Bar + {{ + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + sub.{method}(substitute => {{var _ = {call}; }}).Do(callInfo => + {{ + {argAccess} + }}); + }} + }} +}}"; + + await VerifyDiagnostic(source, CallInfoCouldNotConvertParameterAtPositionDescriptor, message); + } + + public override async Task ReportsNoDiagnostic_WhenCastingElementsFromArgTypes(string method, string call, string argAccess) + { + var source = $@"using System; +using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(Bar x); + + int this[Bar x] {{ get; }} + }} + + public class Bar + {{ + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + sub.{method}(substitute => {{var _ = {call}; }}).Do(callInfo => + {{ + {argAccess} + }}); + }} + }} +}}"; + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsNoDiagnostic_WhenAssigningValueToNotRefNorOutArgumentViaIndirectCall(string method, string call, string argAccess) + { + var source = $@"using System; +using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(Bar x); + + int this[Bar x] {{ get; }} + }} + + public class Bar + {{ + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + sub.{method}(substitute => {{var _ = {call}; }}).Do(callInfo => + {{ + {argAccess} + }}); + }} + }} +}}"; + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsDiagnostic_WhenAccessingArgumentByTypeNotInInvocation(string method, string call, string argAccess, string message) + { + var source = $@"using System; +using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(int x); + + int Barr {{ get; }} + + int this[int x] {{ get; }} + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + sub.{method}(substitute => {{var _ = {call}; }}).Do(callInfo => + {{ + {argAccess} + }}); + }} + }} +}}"; + + await VerifyDiagnostic(source, CallInfoCouldNotFindArgumentToThisCallDescriptor, message); + } + + public override async Task ReportsNoDiagnostic_WhenAccessingArgumentByTypeInInInvocation(string method, string call, string argAccess) + { + var source = $@"using System; +using NSubstitute; + +namespace MyNamespace +{{ + public interface IFoo + {{ + int Bar(int x); + + int Bar(Foo x); + + int this[int x] {{ get; }} + + int this[Foo x] {{ get; }} + }} + + public class FooBase + {{ + }} + + public class Foo : FooBase + {{ + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + sub.{method}(substitute => {{var _ = {call}; }}).Do(callInfo => + {{ + {argAccess} + }}); + }} + }} +}}"; + + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsDiagnostic_WhenAccessingArgumentByTypeMultipleTimesInInvocation(string method, string call, string argAccess, string message) + { + var source = $@"using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(int x, int y); + + int this[int x, int y] {{ get; }} + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + sub.{method}(substitute => {{var _ = {call}; }}).Do(callInfo => + {{ + {argAccess} + }}); + }} + }} +}}"; + + await VerifyDiagnostic(source, CallInfoMoreThanOneArgumentOfTypeDescriptor, message); + } + + public override async Task ReportsNoDiagnostic_WhenAccessingArgumentByTypeMultipleDifferentTypesInInvocation(string method, string call) + { + var source = $@"using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(int x, double y); + + int this[int x, double y] {{ get; }} + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + sub.{method}(substitute => {{var _ = {call}; }}).Do(callInfo => + {{ + callInfo.Arg(); + }}); + }} + }} +}}"; + + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsDiagnostic_WhenAssigningValueToNotOutNorRefArgument(string method, string call) + { + var source = $@"using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(int x, double y); + + int this[int x, double y] {{ get; }} + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + sub.{method}(substitute => {{var _ = {call}; }}).Do(callInfo => + {{ + [|callInfo[1]|] = 1; + }}); + }} + }} +}}"; + + await VerifyDiagnostic(source, CallInfoArgumentIsNotOutOrRefDescriptor, "Could not set argument 1 (double) as it is not an out or ref argument."); + } + + public override async Task ReportsNoDiagnostic_WhenAssigningValueToRefArgument(string method) + { + var source = $@"using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(ref int x); + }} + + public class FooTests + {{ + public void Test() + {{ + int value = 0; + var sub = NSubstitute.Substitute.For(); + sub.{method}(substitute => {{var _ = substitute.Bar(ref value); }}).Do(callInfo => + {{ + callInfo[0] = 1; + }}); + }} + }} +}}"; + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsNoDiagnostic_WhenAssigningValueToOutArgument(string method) + { + var source = $@"using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(out int x); + }} + + public class FooTests + {{ + public void Test() + {{ + int value = 0; + var sub = NSubstitute.Substitute.For(); + sub.{method}(substitute => {{var _ = substitute.Bar(out value); }}).Do(callInfo => + {{ + callInfo[0] = 1; + }}); + }} + }} +}}"; + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsDiagnostic_WhenAssigningValueToOutOfBoundsArgument(string method) + { + var source = $@"using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(out int x); + }} + + public class FooTests + {{ + public void Test() + {{ + int value = 0; + var sub = NSubstitute.Substitute.For(); + sub.{method}(substitute => {{var _ = substitute.Bar(out value); }}).Do(callInfo => + {{ + [|callInfo[1]|] = 1; + }}); + }} + }} +}}"; + + await VerifyDiagnostic(source, CallInfoArgumentOutOfRangeDescriptor, "There is no argument at position 1"); + } + + public override async Task ReportsDiagnostic_WhenAssigningType_NotAssignableTo_Argument(string method, string left, string right, string expectedMessage) + { + var source = $@"using NSubstitute; +using System.Collections.Generic; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(out {left} x); + }} + + public class FooTests + {{ + public void Test() + {{ + {left} value = default({left}); + var sub = NSubstitute.Substitute.For(); + + sub.{method}(substitute => {{var _ = substitute.Bar(out value); }}).Do(callInfo => + {{ + [|callInfo[0]|] = {right}; + }}); + }} + }} +}}"; + + await VerifyDiagnostic(source, CallInfoArgumentSetWithIncompatibleValueDescriptor, expectedMessage); + } + + public override async Task ReportsNoDiagnostic_WhenAssigningType_AssignableTo_Argument(string method, string left, string right) + { + var source = $@"using NSubstitute; +using System.Collections.Generic; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(out {left} x); + }} + + public class FooTests + {{ + public void Test() + {{ + {left} value = default({left}); + var sub = NSubstitute.Substitute.For(); + + sub.{method}(substitute => {{var _ = substitute.Bar(out value); }}).Do(callInfo => + {{ + callInfo[0] = {right}; + }}); + }} + }} +}}"; + + await VerifyNoDiagnostic(source); + } + } +} \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/CallInfoAnalyzerTests/DoMethodPrecededByOrdinaryMethodTests.cs b/tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/CallInfoAnalyzerTests/DoMethodPrecededByOrdinaryMethodTests.cs new file mode 100644 index 00000000..2abcd9bf --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/CallInfoAnalyzerTests/DoMethodPrecededByOrdinaryMethodTests.cs @@ -0,0 +1,664 @@ +using System.Threading.Tasks; +using NSubstitute.Analyzers.Tests.Shared.Extensibility; + +namespace NSubstitute.Analyzers.Tests.CSharp.DiagnosticAnalyzerTests.CallInfoAnalyzerTests +{ + [CombinatoryData("SubstituteExtensions.When", "SubstituteExtensions.WhenForAnyArgs")] + public class DoMethodPrecededByOrdinaryMethodTests : CallInfoDiagnosticVerifier + { + public override async Task ReportsNoDiagnostics_WhenSubstituteMethodCannotBeInferred(string method, string call, string argAccess) + { + var source = $@"using System; +using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(int x); + + int Barr {{ get; }} + + int this[int x] {{ get; }} + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + var returnValue = {method}(sub, substitute => {{var _ = {call}; }}); + returnValue.Do(callInfo => + {{ + {argAccess} + }}); + }} + }} +}}"; + + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsDiagnostic_WhenAccessingArgumentOutOfBounds(string method, string call, string argAccess) + { + var source = $@"using System; +using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(int x); + + int Barr {{ get; }} + + int this[int x] {{ get; }} + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + {method}(sub, substitute => {{var _ = {call}; }}).Do(callInfo => + {{ + {argAccess} + }}); + }} + }} +}}"; + await VerifyDiagnostic(source, CallInfoArgumentOutOfRangeDescriptor, "There is no argument at position 1"); + } + + public override async Task ReportsNoDiagnostic_WhenAccessingArgumentOutOfBound_AndPositionIsNotLiteralExpression(string method, string call, string argAccess) + { + var source = $@"using System; +using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(int x, int y); + + int Barr {{ get; }} + + int this[int x, int y] {{ get; }} + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + {method}(sub, substitute => {{var _ = {call}; }}).Do(callInfo => + {{ + {argAccess} + }}); + }} + }} +}}"; + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsNoDiagnostic_WhenAccessingArgumentWithinBounds(string method, string call, string argAccess) + { + var source = $@"using System; +using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(int x, int y); + + int Barr {{ get; }} + + int this[int x, int y] {{ get; }} + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + {method}(sub, substitute => {{var _ = {call}; }}).Do(callInfo => + {{ + {argAccess} + }}); + }} + }} +}}"; + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsNoDiagnostic_WhenManuallyCasting_ToSupportedType(string method, string call, string argAccess) + { + var source = $@"using System; +using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(int x, Bar y); + + int this[int x, Bar y] {{ get; }} + }} + + public class BarBase + {{ + }} + + public class Bar : BarBase + {{ + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + {method}(sub, substitute => {{var _ = {call}; }}).Do(callInfo => + {{ + {argAccess} + }}); + }} + }} +}}"; + + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsDiagnostic_WhenManuallyCasting_ToUnsupportedType(string method, string call, string argAccess) + { + var source = $@"using System; +using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(int x, double y); + + int Foo(int x, FooBar bar); + + int this[int x, double y] {{ get; }} + + int this[int x, FooBar bar] {{ get; }} + }} + + public class Bar + {{ + }} + + public class FooBar : Bar + {{ + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + {method}(sub, substitute => {{var _ = {call}; }}).Do(callInfo => + {{ + {argAccess} + }}); + }} + }} +}}"; + + await VerifyDiagnostic(source, CallInfoCouldNotConvertParameterAtPositionDescriptor, "Couldn't convert parameter at position 1 to type MyNamespace.Bar."); + } + + public override async Task ReportsNoDiagnostic_WhenCasting_WithArgAt_ToSupportedType(string method, string call, string argAccess) + { + var source = $@"using System; +using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(int x, Bar y); + + int this[int x, Bar y] {{ get; }} + }} + + public class BarBase + {{ + }} + + public class Bar : BarBase + {{ + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + {method}(sub, substitute => {{var _ = {call}; }}).Do(callInfo => + {{ + {argAccess} + }}); + }} + }} +}}"; + + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsDiagnostic_WhenCasting_WithArgAt_ToUnsupportedType(string method, string call, string argAccess, string message) + { + var source = $@"using System; +using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(int x, double y); + + int Foo(int x, FooBar bar); + + int this[int x, double y] {{ get; }} + + int this[int x, FooBar bar] {{ get; }} + }} + + public class Bar + {{ + }} + + public class FooBar : Bar + {{ + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + {method}(sub, substitute => {{var _ = {call}; }}).Do(callInfo => + {{ + {argAccess} + }}); + }} + }} +}}"; + + await VerifyDiagnostic(source, CallInfoCouldNotConvertParameterAtPositionDescriptor, message); + } + + public override async Task ReportsNoDiagnostic_WhenCastingElementsFromArgTypes(string method, string call, string argAccess) + { + var source = $@"using System; +using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(Bar x); + + int this[Bar x] {{ get; }} + }} + + public class Bar + {{ + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + {method}(sub, substitute => {{var _ = {call}; }}).Do(callInfo => + {{ + {argAccess} + }}); + }} + }} +}}"; + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsNoDiagnostic_WhenAssigningValueToNotRefNorOutArgumentViaIndirectCall(string method, string call, string argAccess) + { + var source = $@"using System; +using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(Bar x); + + int this[Bar x] {{ get; }} + }} + + public class Bar + {{ + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + {method}(sub, substitute => {{var _ = {call}; }}).Do(callInfo => + {{ + {argAccess} + }}); + }} + }} +}}"; + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsDiagnostic_WhenAccessingArgumentByTypeNotInInvocation(string method, string call, string argAccess, string message) + { + var source = $@"using System; +using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(int x); + + int Barr {{ get; }} + + int this[int x] {{ get; }} + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + {method}(sub, substitute => {{var _ = {call}; }}).Do(callInfo => + {{ + {argAccess} + }}); + }} + }} +}}"; + + await VerifyDiagnostic(source, CallInfoCouldNotFindArgumentToThisCallDescriptor, message); + } + + public override async Task ReportsNoDiagnostic_WhenAccessingArgumentByTypeInInInvocation(string method, string call, string argAccess) + { + var source = $@"using System; +using NSubstitute; + +namespace MyNamespace +{{ + public interface IFoo + {{ + int Bar(int x); + + int Bar(Foo x); + + int this[int x] {{ get; }} + + int this[Foo x] {{ get; }} + }} + + public class FooBase + {{ + }} + + public class Foo : FooBase + {{ + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + {method}(sub, substitute => {{var _ = {call}; }}).Do(callInfo => + {{ + {argAccess} + }}); + }} + }} +}}"; + + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsDiagnostic_WhenAccessingArgumentByTypeMultipleTimesInInvocation(string method, string call, string argAccess, string message) + { + var source = $@"using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(int x, int y); + + int this[int x, int y] {{ get; }} + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + {method}(sub, substitute => {{var _ = {call}; }}).Do(callInfo => + {{ + {argAccess} + }}); + }} + }} +}}"; + + await VerifyDiagnostic(source, CallInfoMoreThanOneArgumentOfTypeDescriptor, message); + } + + public override async Task ReportsNoDiagnostic_WhenAccessingArgumentByTypeMultipleDifferentTypesInInvocation(string method, string call) + { + var source = $@"using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(int x, double y); + + int this[int x, double y] {{ get; }} + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + {method}(sub, substitute => {{var _ = {call}; }}).Do(callInfo => + {{ + callInfo.Arg(); + }}); + }} + }} +}}"; + + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsDiagnostic_WhenAssigningValueToNotOutNorRefArgument(string method, string call) + { + var source = $@"using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(int x, double y); + + int this[int x, double y] {{ get; }} + }} + + public class FooTests + {{ + public void Test() + {{ + var sub = NSubstitute.Substitute.For(); + {method}(sub, substitute => {{var _ = {call}; }}).Do(callInfo => + {{ + [|callInfo[1]|] = 1; + }}); + }} + }} +}}"; + + await VerifyDiagnostic(source, CallInfoArgumentIsNotOutOrRefDescriptor, "Could not set argument 1 (double) as it is not an out or ref argument."); + } + + public override async Task ReportsNoDiagnostic_WhenAssigningValueToRefArgument(string method) + { + var source = $@"using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(ref int x); + }} + + public class FooTests + {{ + public void Test() + {{ + int value = 0; + var sub = NSubstitute.Substitute.For(); + {method}(sub, substitute => {{var _ = substitute.Bar(ref value); }}).Do(callInfo => + {{ + callInfo[0] = 1; + }}); + }} + }} +}}"; + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsNoDiagnostic_WhenAssigningValueToOutArgument(string method) + { + var source = $@"using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(out int x); + }} + + public class FooTests + {{ + public void Test() + {{ + int value = 0; + var sub = NSubstitute.Substitute.For(); + {method}(sub, substitute => {{var _ = substitute.Bar(out value); }}).Do(callInfo => + {{ + callInfo[0] = 1; + }}); + }} + }} +}}"; + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsDiagnostic_WhenAssigningValueToOutOfBoundsArgument(string method) + { + var source = $@"using NSubstitute; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(out int x); + }} + + public class FooTests + {{ + public void Test() + {{ + int value = 0; + var sub = NSubstitute.Substitute.For(); + {method}(sub, substitute => {{var _ = substitute.Bar(out value); }}).Do(callInfo => + {{ + [|callInfo[1]|] = 1; + }}); + }} + }} +}}"; + + await VerifyDiagnostic(source, CallInfoArgumentOutOfRangeDescriptor, "There is no argument at position 1"); + } + + public override async Task ReportsDiagnostic_WhenAssigningType_NotAssignableTo_Argument(string method, string left, string right, string expectedMessage) + { + var source = $@"using NSubstitute; +using System.Collections.Generic; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(out {left} x); + }} + + public class FooTests + {{ + public void Test() + {{ + {left} value = default({left}); + var sub = NSubstitute.Substitute.For(); + + {method}(sub, substitute => {{var _ = substitute.Bar(out value); }}).Do(callInfo => + {{ + [|callInfo[0]|] = {right}; + }}); + }} + }} +}}"; + + await VerifyDiagnostic(source, CallInfoArgumentSetWithIncompatibleValueDescriptor, expectedMessage); + } + + public override async Task ReportsNoDiagnostic_WhenAssigningType_AssignableTo_Argument(string method, string left, string right) + { + var source = $@"using NSubstitute; +using System.Collections.Generic; + +namespace MyNamespace +{{ + public interface Foo + {{ + int Bar(out {left} x); + }} + + public class FooTests + {{ + public void Test() + {{ + {left} value = default({left}); + var sub = NSubstitute.Substitute.For(); + + {method}(sub, substitute => {{var _ = substitute.Bar(out value); }}).Do(callInfo => + {{ + callInfo[0] = {right}; + }}); + }} + }} +}}"; + + await VerifyNoDiagnostic(source); + } + } +} \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.Shared/Fixtures/AnalyzersConventionFixture.cs b/tests/NSubstitute.Analyzers.Tests.Shared/Fixtures/AnalyzersConventionFixture.cs index 5a31f650..7f22a5a4 100644 --- a/tests/NSubstitute.Analyzers.Tests.Shared/Fixtures/AnalyzersConventionFixture.cs +++ b/tests/NSubstitute.Analyzers.Tests.Shared/Fixtures/AnalyzersConventionFixture.cs @@ -24,8 +24,8 @@ public void AssertExportCodeFixProviderAttributeUsageFromAssemblyContaining(Type { var types = GetTypesAssignableTo(type.Assembly).ToList(); - types.Should().OnlyContain(innerType => innerType.GetCustomAttributes(true).Count() == 1, "because each code fix provider should be marked with only one attribute ExportCodeFixProviderAttribute"); - types.SelectMany(innerType => innerType.GetCustomAttributes(true)).Should() + types.Should().OnlyContain(innerType => innerType.GetCustomAttributes(false).Count() == 1, "because each code fix provider should be marked with only one attribute ExportCodeFixProviderAttribute"); + types.SelectMany(innerType => innerType.GetCustomAttributes(false)).Should() .OnlyContain( attr => attr.Languages.Length == 1 && attr.Languages.Count(lang => lang == expectedLanguage) == 1, $"because each code fix provider should support only selected language ${expectedLanguage}"); @@ -35,19 +35,13 @@ public void AssertDiagnosticAnalyzerAttributeUsageFromAssemblyContaining(Type ty { var types = GetTypesAssignableTo(type.Assembly).ToList(); - types.Should().OnlyContain(innerType => innerType.GetCustomAttributes(true).Count() == 1, "because each analyzer should be marked with only one attribute DiagnosticAnalyzerAttribute"); - types.SelectMany(innerType => innerType.GetCustomAttributes(true)).Should() + types.Should().OnlyContain(innerType => innerType.GetCustomAttributes(false).Count() == 1, "because each analyzer should be marked with only one attribute DiagnosticAnalyzerAttribute"); + types.SelectMany(innerType => innerType.GetCustomAttributes(false)).Should() .OnlyContain( attr => attr.Languages.Length == 1 && attr.Languages.Count(lang => lang == expectedLanguage) == 1, $"because each analyzer should support only selected language ${expectedLanguage}"); } - private static T CreateInstance(Type analyzer) - { - var args = analyzer.GetConstructors().First().GetParameters().Select(parameter => Substitute.For(new[] { parameter.ParameterType }, null)).ToArray(); - return (T)Activator.CreateInstance(analyzer, args); - } - private IEnumerable GetTypesAssignableTo(Assembly assembly) { var type = typeof(T); diff --git a/tests/NSubstitute.Analyzers.Tests.VisualBasic/DiagnosticAnalyzersTests/CallInfoAnalyzerTests/DoMethodPrecededByExtensionMethodTests.cs b/tests/NSubstitute.Analyzers.Tests.VisualBasic/DiagnosticAnalyzersTests/CallInfoAnalyzerTests/DoMethodPrecededByExtensionMethodTests.cs new file mode 100644 index 00000000..b1e893e3 --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.VisualBasic/DiagnosticAnalyzersTests/CallInfoAnalyzerTests/DoMethodPrecededByExtensionMethodTests.cs @@ -0,0 +1,587 @@ +using System.Threading.Tasks; +using NSubstitute.Analyzers.Tests.Shared.Extensibility; + +namespace NSubstitute.Analyzers.Tests.VisualBasic.DiagnosticAnalyzersTests.CallInfoAnalyzerTests +{ + [CombinatoryData("When", "WhenForAnyArgs")] + public class DoMethodPrecededByExtensionMethodTests : CallInfoDiagnosticVerifier + { + public override async Task ReportsNoDiagnostics_WhenSubstituteMethodCannotBeInferred(string method, string call, string argAccess) + { + var source = $@"Imports System +Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByVal x As Integer) As Integer + ReadOnly Property Barr As Integer + Default ReadOnly Property Item(ByVal x As Integer) As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + Dim returnValue = mock.{method}(Function(substitute) + Dim x = {call} + End Function) + + returnValue.Do(Function(callInfo) + {argAccess} + End Function) + End Sub + End Class +End Namespace"; + + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsDiagnostic_WhenAccessingArgumentOutOfBounds(string method, string call, string argAccess) + { + var source = $@"Imports System +Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByVal x As Integer) As Integer + ReadOnly Property Barr As Integer + Default ReadOnly Property Item(ByVal x As Integer) As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + mock.{method}(Function(substitute) + Dim x = {call} + End Function).Do(Function(callInfo) + {argAccess} + End Function) + End Sub + End Class +End Namespace +"; + await VerifyDiagnostic(source, CallInfoArgumentOutOfRangeDescriptor, "There is no argument at position 1"); + } + + public override async Task ReportsNoDiagnostic_WhenAccessingArgumentOutOfBound_AndPositionIsNotLiteralExpression(string method, string call, string argAccess) + { + var source = $@"Imports System +Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByVal x As Integer, ByVal y As Integer) As Integer + ReadOnly Property Barr As Integer + Default ReadOnly Property Item(ByVal x As Integer, ByVal y As Integer) As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + mock.{method}(Function(substitute) + Dim x = {call} + End Function).Do(Function(callInfo) + {argAccess} + End Function) + End Sub + End Class +End Namespace"; + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsNoDiagnostic_WhenAccessingArgumentWithinBounds(string method, string call, string argAccess) + { + var source = $@"Imports System +Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByVal x As Integer, ByVal y As Integer) As Integer + ReadOnly Property Barr As Integer + Default ReadOnly Property Item(ByVal x As Integer, ByVal y As Integer) As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + mock.{method}(Function(substitute) + Dim x = {call} + End Function).Do(Function(callInfo) + {argAccess} + End Function) + End Sub + End Class +End Namespace +"; + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsNoDiagnostic_WhenManuallyCasting_ToSupportedType(string method, string call, string argAccess) + { + var source = $@"Imports System +Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByVal x As Integer, ByVal y As Bar) As Integer + Default ReadOnly Property Item(ByVal x As Integer, ByVal y As Bar) As Integer + End Interface + + Public Class BarBase + End Class + + Public Class Bar + Inherits BarBase + End Class + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + mock.{method}(Function(substitute) + Dim x = {call} + End Function).Do(Function(callInfo) + {argAccess} + End Function) + End Sub + End Class +End Namespace"; + + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsDiagnostic_WhenManuallyCasting_ToUnsupportedType(string method, string call, string argAccess) + { + var source = $@"Imports System +Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByVal x As Integer, ByVal y As Double) As Integer + Function Foo(ByVal x As Integer, ByVal bar As FooBar) As Integer + Default ReadOnly Property Item(ByVal x As Integer, ByVal y As Double) As Integer + Default ReadOnly Property Item(ByVal x As Integer, ByVal bar As FooBar) As Integer + End Interface + + Public Class Bar + End Class + + Public Class FooBar + Inherits Bar + End Class + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + mock.{method}(Function(substitute) + Dim x = {call} + End Function).Do(Function(callInfo) + {argAccess} + End Function) + End Sub + End Class +End Namespace +"; + + await VerifyDiagnostic(source, CallInfoCouldNotConvertParameterAtPositionDescriptor, "Couldn't convert parameter at position 1 to type MyNamespace.Bar."); + } + + public override async Task ReportsNoDiagnostic_WhenCasting_WithArgAt_ToSupportedType(string method, string call, string argAccess) + { + var source = $@"Imports System +Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByVal x As Integer, ByVal y As Bar) As Integer + Default ReadOnly Property Item(ByVal x As Integer, ByVal y As Bar) As Integer + End Interface + + Public Class BarBase + End Class + + Public Class Bar + Inherits BarBase + End Class + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + mock.{method}(Function(substitute) + Dim x = {call} + End Function).Do(Function(callInfo) + {argAccess} + End Function) + End Sub + End Class +End Namespace"; + + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsDiagnostic_WhenCasting_WithArgAt_ToUnsupportedType(string method, string call, string argAccess, string message) + { + var source = $@"Imports System +Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByVal x As Integer, ByVal y As Double) As Integer + Function Foo(ByVal x As Integer, ByVal bar As FooBar) As Integer + Default ReadOnly Property Item(ByVal x As Integer, ByVal y As Double) As Integer + Default ReadOnly Property Item(ByVal x As Integer, ByVal bar As FooBar) As Integer + End Interface + + Public Class Bar + End Class + + Public Class FooBar + Inherits Bar + End Class + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + mock.{method}(Function(substitute) + Dim x = {call} + End Function).Do(Function(callInfo) + {argAccess} + End Function) + End Sub + End Class +End Namespace +"; + await VerifyDiagnostic(source, CallInfoCouldNotConvertParameterAtPositionDescriptor, message); + } + + public override async Task ReportsNoDiagnostic_WhenCastingElementsFromArgTypes(string method, string call, string argAccess) + { + var source = $@"Imports System +Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByVal x As Bar) As Integer + Default ReadOnly Property Item(ByVal x As Bar) As Integer + End Interface + + Public Class Bar + End Class + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + mock.{method}(Function(substitute) + Dim x = {call} + End Function).Do(Function(callInfo) + {argAccess} + End Function) + End Sub + End Class +End Namespace +"; + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsNoDiagnostic_WhenAssigningValueToNotRefNorOutArgumentViaIndirectCall(string method, string call, string argAccess) + { + var source = $@"Imports System +Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByVal x As Bar) As Integer + Default ReadOnly Property Item(ByVal x As Bar) As Integer + End Interface + + Public Class Bar + End Class + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + mock.{method}(Function(substitute) + Dim x = {call} + End Function).Do(Function(callInfo) + {argAccess} + End Function) + End Sub + End Class +End Namespace +"; + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsDiagnostic_WhenAccessingArgumentByTypeNotInInvocation(string method, string call, string argAccess, string message) + { + var source = $@"Imports System +Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByVal x As Integer) As Integer + ReadOnly Property Barr As Integer + Default ReadOnly Property Item(ByVal x As Integer) As Integer + + End Interface + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + mock.{method}(Function(substitute) + Dim x = {call} + End Function).Do(Function(callInfo) + {argAccess} + End Function) + End Sub + End Class +End Namespace +"; + + await VerifyDiagnostic(source, CallInfoCouldNotFindArgumentToThisCallDescriptor, message); + } + + public override async Task ReportsNoDiagnostic_WhenAccessingArgumentByTypeInInInvocation(string method, string call, string argAccess) + { + var source = $@"Imports System +Imports NSubstitute + +Namespace MyNamespace + Interface IFoo + Function Bar(ByVal x As Integer) As Integer + Function Bar(ByVal x As Foo) As Integer + Default ReadOnly Property Item(ByVal x As Integer) As Integer + Default ReadOnly Property Item(ByVal x As Foo) As Integer + End Interface + + Public Class FooBase + End Class + + Public Class Foo + Inherits FooBase + End Class + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of IFoo)() + mock.{method}(Function(substitute) + Dim x = {call} + End Function).Do(Function(callInfo) + {argAccess} + End Function) + End Sub + End Class +End Namespace +"; + + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsDiagnostic_WhenAccessingArgumentByTypeMultipleTimesInInvocation(string method, string call, string argAccess, string message) + { + var source = $@"Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByVal x As Integer, ByVal y As Integer) As Integer + Default ReadOnly Property Item(ByVal x As Integer, ByVal y As Integer) As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + mock.{method}(Function(substitute) + Dim x = {call} + End Function).Do(Function(callInfo) + {argAccess} + End Function) + End Sub + End Class +End Namespace +"; + await VerifyDiagnostic(source, CallInfoMoreThanOneArgumentOfTypeDescriptor, message); + } + + public override async Task ReportsNoDiagnostic_WhenAccessingArgumentByTypeMultipleDifferentTypesInInvocation(string method, string call) + { + var source = $@"Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByVal x As Integer, ByVal y As Double) As Integer + Default ReadOnly Property Item(ByVal x As Integer, ByVal y As Double) As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + mock.{method}(Function(substitute) + Dim x = {call} + End Function).Do(Function(callInfo) + callInfo.Arg(Of Integer)() + End Function) + End Sub + End Class +End Namespace +"; + + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsDiagnostic_WhenAssigningValueToNotOutNorRefArgument(string method, string call) + { + var source = $@"Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByVal x As Integer, ByVal y As Double) As Integer + Default ReadOnly Property Item(ByVal x As Integer, ByVal y As Double) As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + mock.{method}(Function(substitute) + Dim x = {call} + End Function).Do(Function(callInfo) + [|callInfo(1)|] = 1 + End Function) + End Sub + End Class +End Namespace +"; + await VerifyDiagnostic(source, CallInfoArgumentIsNotOutOrRefDescriptor, "Could not set argument 1 (Double) as it is not an out or ref argument."); + } + + public override async Task ReportsNoDiagnostic_WhenAssigningValueToRefArgument(string method) + { + var source = $@"Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByRef x As Integer) As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim value As Integer = 0 + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + mock.{method}(Function(substitute) + Dim x = substitute.Bar(value) + End Function).Do(Function(callInfo) + callInfo(0) = 1 + End Function) + End Sub + End Class +End Namespace +"; + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsNoDiagnostic_WhenAssigningValueToOutArgument(string method) + { + var source = $@"Imports NSubstitute +Imports System.Runtime.InteropServices + +Namespace MyNamespace + Interface Foo + Function Bar( ByRef x As Integer) As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim value As Integer = 0 + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + mock.{method}(Function(substitute) + Dim x = substitute.Bar(value) + End Function).Do(Function(callInfo) + callInfo(0) = 1 + End Function) + End Sub + End Class +End Namespace +"; + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsDiagnostic_WhenAssigningValueToOutOfBoundsArgument(string method) + { + var source = $@"Imports NSubstitute +Imports System.Runtime.InteropServices + +Namespace MyNamespace + Interface Foo + Function Bar( ByRef x As Integer) As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim value As Integer = 0 + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + mock.{method}(Function(substitute) + Dim x = substitute.Bar(value) + End Function).Do(Function(callInfo) + [|callInfo(1)|] = 1 + End Function) + End Sub + End Class +End Namespace +"; + await VerifyDiagnostic(source, CallInfoArgumentOutOfRangeDescriptor, "There is no argument at position 1"); + } + + public override async Task ReportsDiagnostic_WhenAssigningType_NotAssignableTo_Argument(string method, string left, string right, string expectedMessage) + { + var source = $@"Imports NSubstitute +Imports System.Runtime.InteropServices +Imports System.Collections.Generic + +Namespace MyNamespace + Interface Foo + Function Bar( ByRef x As {left}) As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim value As {left} = Nothing + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + mock.{method}(Function(substitute) + Dim x = substitute.Bar(value) + End Function).Do(Function(callInfo) + [|callInfo(0)|] = {right} + End Function) + End Sub + End Class +End Namespace +"; + + await VerifyDiagnostic(source, CallInfoArgumentSetWithIncompatibleValueDescriptor, expectedMessage); + } + + public override async Task ReportsNoDiagnostic_WhenAssigningType_AssignableTo_Argument(string method, string left, string right) + { + var source = $@"Imports NSubstitute +Imports System.Runtime.InteropServices +Imports System.Collections.Generic + +Namespace MyNamespace + Interface Foo + Function Bar( ByRef x As {left}) As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim value As {left} = Nothing + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + mock.{method}(Function(substitute) + Dim x = substitute.Bar(value) + End Function).Do(Function(callInfo) + callInfo(0) = {right} + End Function) + End Sub + End Class +End Namespace +"; + + await VerifyNoDiagnostic(source); + } + } +} \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.VisualBasic/DiagnosticAnalyzersTests/CallInfoAnalyzerTests/DoMethodPrecededByOrdinaryMethodTests.cs b/tests/NSubstitute.Analyzers.Tests.VisualBasic/DiagnosticAnalyzersTests/CallInfoAnalyzerTests/DoMethodPrecededByOrdinaryMethodTests.cs new file mode 100644 index 00000000..35f2c5be --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.VisualBasic/DiagnosticAnalyzersTests/CallInfoAnalyzerTests/DoMethodPrecededByOrdinaryMethodTests.cs @@ -0,0 +1,587 @@ +using System.Threading.Tasks; +using NSubstitute.Analyzers.Tests.Shared.Extensibility; + +namespace NSubstitute.Analyzers.Tests.VisualBasic.DiagnosticAnalyzersTests.CallInfoAnalyzerTests +{ + [CombinatoryData("SubstituteExtensions.When", "SubstituteExtensions.WhenForAnyArgs")] + public class DoMethodPrecededByOrdinaryMethodTests : CallInfoDiagnosticVerifier + { + public override async Task ReportsNoDiagnostics_WhenSubstituteMethodCannotBeInferred(string method, string call, string argAccess) + { + var source = $@"Imports System +Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByVal x As Integer) As Integer + ReadOnly Property Barr As Integer + Default ReadOnly Property Item(ByVal x As Integer) As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + Dim returnValue = {method}(mock, Function(substitute) + Dim x = {call} + End Function) + + returnValue.Do(Function(callInfo) + {argAccess} + End Function) + End Sub + End Class +End Namespace"; + + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsDiagnostic_WhenAccessingArgumentOutOfBounds(string method, string call, string argAccess) + { + var source = $@"Imports System +Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByVal x As Integer) As Integer + ReadOnly Property Barr As Integer + Default ReadOnly Property Item(ByVal x As Integer) As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + {method}(mock, Function(substitute) + Dim x = {call} + End Function).Do(Function(callInfo) + {argAccess} + End Function) + End Sub + End Class +End Namespace +"; + await VerifyDiagnostic(source, CallInfoArgumentOutOfRangeDescriptor, "There is no argument at position 1"); + } + + public override async Task ReportsNoDiagnostic_WhenAccessingArgumentOutOfBound_AndPositionIsNotLiteralExpression(string method, string call, string argAccess) + { + var source = $@"Imports System +Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByVal x As Integer, ByVal y As Integer) As Integer + ReadOnly Property Barr As Integer + Default ReadOnly Property Item(ByVal x As Integer, ByVal y As Integer) As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + {method}(mock, Function(substitute) + Dim x = {call} + End Function).Do(Function(callInfo) + {argAccess} + End Function) + End Sub + End Class +End Namespace"; + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsNoDiagnostic_WhenAccessingArgumentWithinBounds(string method, string call, string argAccess) + { + var source = $@"Imports System +Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByVal x As Integer, ByVal y As Integer) As Integer + ReadOnly Property Barr As Integer + Default ReadOnly Property Item(ByVal x As Integer, ByVal y As Integer) As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + {method}(mock, Function(substitute) + Dim x = {call} + End Function).Do(Function(callInfo) + {argAccess} + End Function) + End Sub + End Class +End Namespace +"; + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsNoDiagnostic_WhenManuallyCasting_ToSupportedType(string method, string call, string argAccess) + { + var source = $@"Imports System +Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByVal x As Integer, ByVal y As Bar) As Integer + Default ReadOnly Property Item(ByVal x As Integer, ByVal y As Bar) As Integer + End Interface + + Public Class BarBase + End Class + + Public Class Bar + Inherits BarBase + End Class + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + {method}(mock, Function(substitute) + Dim x = {call} + End Function).Do(Function(callInfo) + {argAccess} + End Function) + End Sub + End Class +End Namespace"; + + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsDiagnostic_WhenManuallyCasting_ToUnsupportedType(string method, string call, string argAccess) + { + var source = $@"Imports System +Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByVal x As Integer, ByVal y As Double) As Integer + Function Foo(ByVal x As Integer, ByVal bar As FooBar) As Integer + Default ReadOnly Property Item(ByVal x As Integer, ByVal y As Double) As Integer + Default ReadOnly Property Item(ByVal x As Integer, ByVal bar As FooBar) As Integer + End Interface + + Public Class Bar + End Class + + Public Class FooBar + Inherits Bar + End Class + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + {method}(mock, Function(substitute) + Dim x = {call} + End Function).Do(Function(callInfo) + {argAccess} + End Function) + End Sub + End Class +End Namespace +"; + + await VerifyDiagnostic(source, CallInfoCouldNotConvertParameterAtPositionDescriptor, "Couldn't convert parameter at position 1 to type MyNamespace.Bar."); + } + + public override async Task ReportsNoDiagnostic_WhenCasting_WithArgAt_ToSupportedType(string method, string call, string argAccess) + { + var source = $@"Imports System +Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByVal x As Integer, ByVal y As Bar) As Integer + Default ReadOnly Property Item(ByVal x As Integer, ByVal y As Bar) As Integer + End Interface + + Public Class BarBase + End Class + + Public Class Bar + Inherits BarBase + End Class + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + {method}(mock, Function(substitute) + Dim x = {call} + End Function).Do(Function(callInfo) + {argAccess} + End Function) + End Sub + End Class +End Namespace"; + + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsDiagnostic_WhenCasting_WithArgAt_ToUnsupportedType(string method, string call, string argAccess, string message) + { + var source = $@"Imports System +Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByVal x As Integer, ByVal y As Double) As Integer + Function Foo(ByVal x As Integer, ByVal bar As FooBar) As Integer + Default ReadOnly Property Item(ByVal x As Integer, ByVal y As Double) As Integer + Default ReadOnly Property Item(ByVal x As Integer, ByVal bar As FooBar) As Integer + End Interface + + Public Class Bar + End Class + + Public Class FooBar + Inherits Bar + End Class + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + {method}(mock, Function(substitute) + Dim x = {call} + End Function).Do(Function(callInfo) + {argAccess} + End Function) + End Sub + End Class +End Namespace +"; + await VerifyDiagnostic(source, CallInfoCouldNotConvertParameterAtPositionDescriptor, message); + } + + public override async Task ReportsNoDiagnostic_WhenCastingElementsFromArgTypes(string method, string call, string argAccess) + { + var source = $@"Imports System +Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByVal x As Bar) As Integer + Default ReadOnly Property Item(ByVal x As Bar) As Integer + End Interface + + Public Class Bar + End Class + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + {method}(mock, Function(substitute) + Dim x = {call} + End Function).Do(Function(callInfo) + {argAccess} + End Function) + End Sub + End Class +End Namespace +"; + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsNoDiagnostic_WhenAssigningValueToNotRefNorOutArgumentViaIndirectCall(string method, string call, string argAccess) + { + var source = $@"Imports System +Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByVal x As Bar) As Integer + Default ReadOnly Property Item(ByVal x As Bar) As Integer + End Interface + + Public Class Bar + End Class + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + {method}(mock, Function(substitute) + Dim x = {call} + End Function).Do(Function(callInfo) + {argAccess} + End Function) + End Sub + End Class +End Namespace +"; + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsDiagnostic_WhenAccessingArgumentByTypeNotInInvocation(string method, string call, string argAccess, string message) + { + var source = $@"Imports System +Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByVal x As Integer) As Integer + ReadOnly Property Barr As Integer + Default ReadOnly Property Item(ByVal x As Integer) As Integer + + End Interface + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + {method}(mock, Function(substitute) + Dim x = {call} + End Function).Do(Function(callInfo) + {argAccess} + End Function) + End Sub + End Class +End Namespace +"; + + await VerifyDiagnostic(source, CallInfoCouldNotFindArgumentToThisCallDescriptor, message); + } + + public override async Task ReportsNoDiagnostic_WhenAccessingArgumentByTypeInInInvocation(string method, string call, string argAccess) + { + var source = $@"Imports System +Imports NSubstitute + +Namespace MyNamespace + Interface IFoo + Function Bar(ByVal x As Integer) As Integer + Function Bar(ByVal x As Foo) As Integer + Default ReadOnly Property Item(ByVal x As Integer) As Integer + Default ReadOnly Property Item(ByVal x As Foo) As Integer + End Interface + + Public Class FooBase + End Class + + Public Class Foo + Inherits FooBase + End Class + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of IFoo)() + {method}(mock, Function(substitute) + Dim x = {call} + End Function).Do(Function(callInfo) + {argAccess} + End Function) + End Sub + End Class +End Namespace +"; + + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsDiagnostic_WhenAccessingArgumentByTypeMultipleTimesInInvocation(string method, string call, string argAccess, string message) + { + var source = $@"Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByVal x As Integer, ByVal y As Integer) As Integer + Default ReadOnly Property Item(ByVal x As Integer, ByVal y As Integer) As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + {method}(mock, Function(substitute) + Dim x = {call} + End Function).Do(Function(callInfo) + {argAccess} + End Function) + End Sub + End Class +End Namespace +"; + await VerifyDiagnostic(source, CallInfoMoreThanOneArgumentOfTypeDescriptor, message); + } + + public override async Task ReportsNoDiagnostic_WhenAccessingArgumentByTypeMultipleDifferentTypesInInvocation(string method, string call) + { + var source = $@"Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByVal x As Integer, ByVal y As Double) As Integer + Default ReadOnly Property Item(ByVal x As Integer, ByVal y As Double) As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + {method}(mock, Function(substitute) + Dim x = {call} + End Function).Do(Function(callInfo) + callInfo.Arg(Of Integer)() + End Function) + End Sub + End Class +End Namespace +"; + + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsDiagnostic_WhenAssigningValueToNotOutNorRefArgument(string method, string call) + { + var source = $@"Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByVal x As Integer, ByVal y As Double) As Integer + Default ReadOnly Property Item(ByVal x As Integer, ByVal y As Double) As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + {method}(mock, Function(substitute) + Dim x = {call} + End Function).Do(Function(callInfo) + [|callInfo(1)|] = 1 + End Function) + End Sub + End Class +End Namespace +"; + await VerifyDiagnostic(source, CallInfoArgumentIsNotOutOrRefDescriptor, "Could not set argument 1 (Double) as it is not an out or ref argument."); + } + + public override async Task ReportsNoDiagnostic_WhenAssigningValueToRefArgument(string method) + { + var source = $@"Imports NSubstitute + +Namespace MyNamespace + Interface Foo + Function Bar(ByRef x As Integer) As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim value As Integer = 0 + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + {method}(mock, Function(substitute) + Dim x = substitute.Bar(value) + End Function).Do(Function(callInfo) + callInfo(0) = 1 + End Function) + End Sub + End Class +End Namespace +"; + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsNoDiagnostic_WhenAssigningValueToOutArgument(string method) + { + var source = $@"Imports NSubstitute +Imports System.Runtime.InteropServices + +Namespace MyNamespace + Interface Foo + Function Bar( ByRef x As Integer) As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim value As Integer = 0 + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + {method}(mock, Function(substitute) + Dim x = substitute.Bar(value) + End Function).Do(Function(callInfo) + callInfo(0) = 1 + End Function) + End Sub + End Class +End Namespace +"; + await VerifyNoDiagnostic(source); + } + + public override async Task ReportsDiagnostic_WhenAssigningValueToOutOfBoundsArgument(string method) + { + var source = $@"Imports NSubstitute +Imports System.Runtime.InteropServices + +Namespace MyNamespace + Interface Foo + Function Bar( ByRef x As Integer) As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim value As Integer = 0 + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + {method}(mock, Function(substitute) + Dim x = substitute.Bar(value) + End Function).Do(Function(callInfo) + [|callInfo(1)|] = 1 + End Function) + End Sub + End Class +End Namespace +"; + await VerifyDiagnostic(source, CallInfoArgumentOutOfRangeDescriptor, "There is no argument at position 1"); + } + + public override async Task ReportsDiagnostic_WhenAssigningType_NotAssignableTo_Argument(string method, string left, string right, string expectedMessage) + { + var source = $@"Imports NSubstitute +Imports System.Runtime.InteropServices +Imports System.Collections.Generic + +Namespace MyNamespace + Interface Foo + Function Bar( ByRef x As {left}) As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim value As {left} = Nothing + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + {method}(mock, Function(substitute) + Dim x = substitute.Bar(value) + End Function).Do(Function(callInfo) + [|callInfo(0)|] = {right} + End Function) + End Sub + End Class +End Namespace +"; + + await VerifyDiagnostic(source, CallInfoArgumentSetWithIncompatibleValueDescriptor, expectedMessage); + } + + public override async Task ReportsNoDiagnostic_WhenAssigningType_AssignableTo_Argument(string method, string left, string right) + { + var source = $@"Imports NSubstitute +Imports System.Runtime.InteropServices +Imports System.Collections.Generic + +Namespace MyNamespace + Interface Foo + Function Bar( ByRef x As {left}) As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim value As {left} = Nothing + Dim mock = NSubstitute.Substitute.[For](Of Foo)() + {method}(mock, Function(substitute) + Dim x = substitute.Bar(value) + End Function).Do(Function(callInfo) + callInfo(0) = {right} + End Function) + End Sub + End Class +End Namespace +"; + + await VerifyNoDiagnostic(source); + } + } +} \ No newline at end of file