diff --git a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoAnalyzer.cs b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoAnalyzer.cs index e72b88a1..303fe63a 100644 --- a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoAnalyzer.cs +++ b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoAnalyzer.cs @@ -1,5 +1,8 @@ +using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; +using System.Reflection; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -11,6 +14,11 @@ namespace NSubstitute.Analyzers.CSharp.DiagnosticAnalyzers [DiagnosticAnalyzer(LanguageNames.CSharp)] internal class CallInfoAnalyzer : AbstractCallInfoAnalyzer { + private static ImmutableArray callHierarchy = ImmutableArray.Create( + typeof(MemberAccessExpressionSyntax), + typeof(InvocationExpressionSyntax), + typeof(MemberAccessExpressionSyntax)); + public CallInfoAnalyzer() : base(new DiagnosticDescriptorsProvider()) { @@ -20,15 +28,38 @@ public CallInfoAnalyzer() protected override SyntaxNode GetSubstituteCall(IMethodSymbol methodSymbol, InvocationExpressionSyntax invocationExpressionSyntax) { - switch (methodSymbol.MethodKind) + if (methodSymbol.IsStatic) { - case MethodKind.ReducedExtension: - return invocationExpressionSyntax.Expression.DescendantNodes().First(); - case MethodKind.Ordinary: - return invocationExpressionSyntax.ArgumentList.Arguments.First().Expression; - default: - return null; + switch (methodSymbol.MethodKind) + { + case MethodKind.ReducedExtension: + return invocationExpressionSyntax.Expression.DescendantNodes().First(); + case MethodKind.Ordinary: + return invocationExpressionSyntax.ArgumentList.Arguments.First().Expression; + default: + return null; + } } + + // TODO fix + using (var descendantNodesEnumerator = invocationExpressionSyntax.DescendantNodes().GetEnumerator()) + { + var hierarchyEnumerator = callHierarchy.GetEnumerator(); + while (hierarchyEnumerator.MoveNext() && descendantNodesEnumerator.MoveNext()) + { + if (descendantNodesEnumerator.Current.GetType().GetTypeInfo().IsAssignableFrom(hierarchyEnumerator.Current.GetTypeInfo()) == false) + { + return null; + } + } + + if (hierarchyEnumerator.MoveNext() == false && descendantNodesEnumerator.MoveNext()) + { + return descendantNodesEnumerator.Current; + } + } + + return null; } protected override IEnumerable GetArgumentExpressions(InvocationExpressionSyntax invocationExpressionSyntax) diff --git a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoCallFinder.cs b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoCallFinder.cs index 4cda5fbd..a27436c1 100644 --- a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoCallFinder.cs +++ b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/CallInfoCallFinder.cs @@ -39,7 +39,7 @@ public override void VisitInvocationExpression(InvocationExpressionSyntax node) { var symbolInfo = _semanticModel.GetSymbolInfo(node); - if (symbolInfo.Symbol != null && symbolInfo.Symbol.ContainingType.ToString().Equals(MetadataNames.NSubstituteCoreFullTypeName)) + if (symbolInfo.Symbol != null && symbolInfo.Symbol.ContainingType.ToString().Equals(MetadataNames.NSubstituteCallInfoFullTypeName)) { switch (symbolInfo.Symbol.Name) { @@ -58,7 +58,7 @@ public override void VisitInvocationExpression(InvocationExpressionSyntax node) public override void VisitElementAccessExpression(ElementAccessExpressionSyntax node) { var symbolInfo = ModelExtensions.GetSymbolInfo(_semanticModel, node).Symbol ?? ModelExtensions.GetSymbolInfo(_semanticModel, node.Expression).Symbol; - if (symbolInfo != null && symbolInfo.ContainingType.ToString().Equals(MetadataNames.NSubstituteCoreFullTypeName)) + if (symbolInfo != null && symbolInfo.ContainingType.ToString().Equals(MetadataNames.NSubstituteCallInfoFullTypeName)) { DirectIndexerAccesses.Add(node); } diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractCallInfoAnalyzer.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractCallInfoAnalyzer.cs index a640608f..e37ca15e 100644 --- a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractCallInfoAnalyzer.cs +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractCallInfoAnalyzer.cs @@ -29,7 +29,8 @@ protected AbstractCallInfoAnalyzer(IDiagnosticDescriptorsProvider diagnosticDesc [MetadataNames.NSubstituteReturnsMethod] = MetadataNames.NSubstituteSubstituteExtensionsFullTypeName, [MetadataNames.NSubstituteReturnsForAnyArgsMethod] = MetadataNames.NSubstituteSubstituteExtensionsFullTypeName, [MetadataNames.NSubstituteThrowsMethod] = MetadataNames.NSubstituteExceptionExtensionsFullTypeName, - [MetadataNames.NSubstituteThrowsForAnyArgsMethod] = MetadataNames.NSubstituteExceptionExtensionsFullTypeName + [MetadataNames.NSubstituteThrowsForAnyArgsMethod] = MetadataNames.NSubstituteExceptionExtensionsFullTypeName, + [MetadataNames.NSubstituteAndDoesMethod] = MetadataNames.NSubstituteConfiguredCallFullTypeName }.ToImmutableDictionary(); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( @@ -265,10 +266,11 @@ private bool AnalyzeAssignment(SyntaxNodeAnalysisContext syntaxNodeContext, ILis private bool SupportsCallInfo(SyntaxNodeAnalysisContext syntaxNodeContext, TInvocationExpressionSyntax syntax, IMethodSymbol methodSymbol) { + // TODO Simplify var allArguments = GetArgumentExpressions(syntax); var argumentsForAnalysis = methodSymbol.MethodKind == MethodKind.ReducedExtension ? allArguments - : allArguments.Skip(1); + : methodSymbol.IsStatic ? allArguments.Skip(1) : allArguments; if (MethodNames.TryGetValue(methodSymbol.Name, out var typeName) == false) { diff --git a/src/NSubstitute.Analyzers.Shared/Extensions/TypeInfoExtensions.cs b/src/NSubstitute.Analyzers.Shared/Extensions/TypeInfoExtensions.cs index d0bd943c..f7b90b1c 100644 --- a/src/NSubstitute.Analyzers.Shared/Extensions/TypeInfoExtensions.cs +++ b/src/NSubstitute.Analyzers.Shared/Extensions/TypeInfoExtensions.cs @@ -12,7 +12,8 @@ public static bool IsCallInfoDelegate(this TypeInfo typeInfo, SemanticModel sema var isCalledViaDelegate = typeSymbol != null && typeSymbol.TypeKind == TypeKind.Delegate && typeSymbol is INamedTypeSymbol namedTypeSymbol && - namedTypeSymbol.ConstructedFrom.Equals(semanticModel.Compilation.GetTypeByMetadataName("System.Func`2")) && + (namedTypeSymbol.ConstructedFrom.Equals(semanticModel.Compilation.GetTypeByMetadataName("System.Func`2")) || + namedTypeSymbol.ConstructedFrom.Equals(semanticModel.Compilation.GetTypeByMetadataName("System.Action`1"))) && IsCallInfoParameter(namedTypeSymbol.TypeArguments.First()); return isCalledViaDelegate; @@ -21,7 +22,7 @@ typeSymbol is INamedTypeSymbol namedTypeSymbol && private static bool IsCallInfoParameter(ITypeSymbol symbol) { return symbol.ContainingAssembly?.Name.Equals(MetadataNames.NSubstituteAssemblyName, StringComparison.OrdinalIgnoreCase) == true && - symbol.ToString().Equals(MetadataNames.NSubstituteCoreFullTypeName, StringComparison.OrdinalIgnoreCase) == true; + symbol.ToString().Equals(MetadataNames.NSubstituteCallInfoFullTypeName, StringComparison.OrdinalIgnoreCase) == true; } } } \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/MetadataNames.cs b/src/NSubstitute.Analyzers.Shared/MetadataNames.cs index 6b0949b9..064cf126 100644 --- a/src/NSubstitute.Analyzers.Shared/MetadataNames.cs +++ b/src/NSubstitute.Analyzers.Shared/MetadataNames.cs @@ -6,13 +6,15 @@ internal class MetadataNames public const string NSubstituteSubstituteExtensionsFullTypeName = "NSubstitute.SubstituteExtensions"; public const string NSubstituteReturnsExtensionsFullTypeName = "NSubstitute.ReturnsExtensions.ReturnsExtensions"; public const string NSubstituteExceptionExtensionsFullTypeName = "NSubstitute.ExceptionExtensions.ExceptionExtensions"; - public const string NSubstituteCoreFullTypeName = "NSubstitute.Core.CallInfo"; + public const string NSubstituteCallInfoFullTypeName = "NSubstitute.Core.CallInfo"; + public const string NSubstituteConfiguredCallFullTypeName = "NSubstitute.Core.ConfiguredCall"; public const string NSubstituteSubstituteFullTypeName = "NSubstitute.Substitute"; public const string NSubstituteFactoryFullTypeName = "NSubstitute.Core.ISubstituteFactory"; public const string NSubstituteReturnsMethod = "Returns"; public const string NSubstituteReturnsForAnyArgsMethod = "ReturnsForAnyArgs"; public const string NSubstituteThrowsMethod = "Throws"; public const string NSubstituteThrowsForAnyArgsMethod = "ThrowsForAnyArgs"; + public const string NSubstituteAndDoesMethod = "AndDoes"; public const string NSubstituteReturnsNullMethod = "ReturnsNull"; public const string NSubstituteReturnsNullForAnyArgsMethod = "ReturnsNullForAnyArgs"; public const string NSubstituteDoMethod = "Do"; diff --git a/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/CallInfoAnalyzer.cs b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/CallInfoAnalyzer.cs index e9a9c135..ee8619b4 100644 --- a/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/CallInfoAnalyzer.cs +++ b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/CallInfoAnalyzer.cs @@ -1,5 +1,8 @@ +using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; +using System.Reflection; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.VisualBasic; @@ -11,6 +14,11 @@ namespace NSubstitute.Analyzers.VisualBasic.DiagnosticAnalyzers [DiagnosticAnalyzer(LanguageNames.VisualBasic)] internal class CallInfoAnalyzer : AbstractCallInfoAnalyzer { + private static ImmutableArray callHierarchy = ImmutableArray.Create( + typeof(MemberAccessExpressionSyntax), + typeof(InvocationExpressionSyntax), + typeof(MemberAccessExpressionSyntax)); + public CallInfoAnalyzer() : base(new DiagnosticDescriptorsProvider()) { @@ -20,15 +28,38 @@ public CallInfoAnalyzer() protected override SyntaxNode GetSubstituteCall(IMethodSymbol methodSymbol, InvocationExpressionSyntax invocationExpressionSyntax) { - switch (methodSymbol.MethodKind) + if (methodSymbol.IsStatic) + { + switch (methodSymbol.MethodKind) + { + case MethodKind.ReducedExtension: + return invocationExpressionSyntax.Expression.DescendantNodes().First(); + case MethodKind.Ordinary: + return invocationExpressionSyntax.ArgumentList.Arguments.First().GetExpression(); + default: + return null; + } + } + + // TODO fix + using (var descendantNodesEnumerator = invocationExpressionSyntax.DescendantNodes().GetEnumerator()) { - case MethodKind.ReducedExtension: - return invocationExpressionSyntax.Expression.DescendantNodes().First(); - case MethodKind.Ordinary: - return invocationExpressionSyntax.ArgumentList.Arguments.First().GetExpression(); - default: - return null; + var hierarchyEnumerator = callHierarchy.GetEnumerator(); + while (hierarchyEnumerator.MoveNext() && descendantNodesEnumerator.MoveNext()) + { + if (descendantNodesEnumerator.Current.GetType().GetTypeInfo().IsAssignableFrom(hierarchyEnumerator.Current.GetTypeInfo()) == false) + { + return null; + } + } + + if (hierarchyEnumerator.MoveNext() == false && descendantNodesEnumerator.MoveNext()) + { + return descendantNodesEnumerator.Current; + } } + + return null; } protected override IEnumerable GetArgumentExpressions(InvocationExpressionSyntax invocationExpressionSyntax) diff --git a/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/CallInfoCallFinder.cs b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/CallInfoCallFinder.cs index 0ebda4d8..a9f6841f 100644 --- a/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/CallInfoCallFinder.cs +++ b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/CallInfoCallFinder.cs @@ -39,7 +39,7 @@ public override void VisitInvocationExpression(InvocationExpressionSyntax node) { var symbol = _semanticModel.GetSymbolInfo(node).Symbol; - if (symbol != null && symbol.ContainingType.ToString().Equals(MetadataNames.NSubstituteCoreFullTypeName)) + if (symbol != null && symbol.ContainingType.ToString().Equals(MetadataNames.NSubstituteCallInfoFullTypeName)) { switch (symbol.Name) { @@ -59,7 +59,7 @@ public override void VisitInvocationExpression(InvocationExpressionSyntax node) { var expressionSymbol = _semanticModel.GetSymbolInfo(node.Expression).Symbol; - if (expressionSymbol != null && expressionSymbol.ContainingType.ToString().Equals(MetadataNames.NSubstituteCoreFullTypeName)) + if (expressionSymbol != null && expressionSymbol.ContainingType.ToString().Equals(MetadataNames.NSubstituteCallInfoFullTypeName)) { DirectIndexerAccesses.Add(node); } diff --git a/tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/CallInfoAnalyzerTests/AndDoesMethodTests.cs b/tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/CallInfoAnalyzerTests/AndDoesMethodTests.cs new file mode 100644 index 00000000..9a9a6630 --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/CallInfoAnalyzerTests/AndDoesMethodTests.cs @@ -0,0 +1,663 @@ +using System.Threading.Tasks; +using NSubstitute.Analyzers.Tests.Shared.Extensibility; + +namespace NSubstitute.Analyzers.Tests.CSharp.DiagnosticAnalyzerTests.CallInfoAnalyzerTests +{ + [CombinatoryData("AndDoes")] + public class AndDoesMethodTests : 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 substitute = NSubstitute.Substitute.For(); + var returnedValue = {call}; + returnedValue.Returns(1).{method}(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 substitute = NSubstitute.Substitute.For(); + {call}.Returns(1).{method}(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 substitute = NSubstitute.Substitute.For(); + {call}.Returns(1).{method}(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 substitute = NSubstitute.Substitute.For(); + {call}.Returns(1).{method}(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 substitute = NSubstitute.Substitute.For(); + {call}.Returns(1).{method}(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 substitute = NSubstitute.Substitute.For(); + {call}.Returns(1).{method}(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 substitute = NSubstitute.Substitute.For(); + {call}.Returns(1).{method}(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 substitute = NSubstitute.Substitute.For(); + {call}.Returns(1).{method}(callInfo => + {{ + {argAccess} + }}); + }} + }} +}}"; + + await VerifyDiagnostic(source, CallInfoCouldNotConvertParameterAtPositionDescriptor, message); + } + + public override async Task ReportsNoDiagnostic_WhenCastingElementsFromArgTypes(string method, string callInfo, 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 substitute = NSubstitute.Substitute.For(); + {callInfo}.Returns(1).{method}(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 substitute = NSubstitute.Substitute.For(); + {call}.Returns(1).{method}(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 substitute = NSubstitute.Substitute.For(); + {call}.Returns(1).{method}(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 substitute = NSubstitute.Substitute.For(); + {call}.Returns(1).{method}(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 substitute = NSubstitute.Substitute.For(); + {call}.Returns(1).{method}(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 substitute = NSubstitute.Substitute.For(); + {call}.Returns(1).{method}(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 substitute = NSubstitute.Substitute.For(); + {call}.Returns(1).{method}(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 substitute = NSubstitute.Substitute.For(); + substitute.Bar(ref value).Returns(1).{method}(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 substitute = NSubstitute.Substitute.For(); + substitute.Bar(out value).Returns(1).{method}(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 substitute = NSubstitute.Substitute.For(); + substitute.Bar(out value).Returns(1).{method}(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 substitute = NSubstitute.Substitute.For(); + substitute.Bar(out value).Returns(1).{method}(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 substitute = NSubstitute.Substitute.For(); + substitute.Bar(out value).Returns(1).{method}(callInfo => + {{ + callInfo[0] = {right}; + }}); + }} + }} +}}"; + + await VerifyNoDiagnostic(source); + } + } +} \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.VisualBasic/DiagnosticAnalyzersTests/CallInfoAnalyzerTests/AndDoesMethodTests.cs b/tests/NSubstitute.Analyzers.Tests.VisualBasic/DiagnosticAnalyzersTests/CallInfoAnalyzerTests/AndDoesMethodTests.cs new file mode 100644 index 00000000..af52f309 --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.VisualBasic/DiagnosticAnalyzersTests/CallInfoAnalyzerTests/AndDoesMethodTests.cs @@ -0,0 +1,546 @@ +using System.Threading.Tasks; +using NSubstitute.Analyzers.Tests.Shared.Extensibility; + +namespace NSubstitute.Analyzers.Tests.VisualBasic.DiagnosticAnalyzersTests.CallInfoAnalyzerTests +{ + [CombinatoryData("Returns", "ReturnsForAnyArgs")] + public class AndDoesMethodTests : 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 substitute = NSubstitute.Substitute.[For](Of Foo)() + Dim returnedValue = {call} + returnedValue.Returns(1).{method}(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 substitute = NSubstitute.Substitute.[For](Of Foo)() + {call}.Returns(1).{method}(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 substitute = NSubstitute.Substitute.[For](Of Foo)() + {call}.Returns(1).{method}(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 substitute = NSubstitute.Substitute.[For](Of Foo)() + {call}.Returns(1).{method}(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 substitute = NSubstitute.Substitute.[For](Of Foo)() + {call}.Returns(1).{method}(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 substitute = NSubstitute.Substitute.[For](Of Foo)() + {call}.Returns(1).{method}(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 substitute = NSubstitute.Substitute.[For](Of Foo)() + {call}.Returns(1).{method}(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 substitute = NSubstitute.Substitute.[For](Of Foo)() + {call}.Returns(1).{method}(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 substitute = NSubstitute.Substitute.[For](Of Foo)() + {call}.Returns(1).{method}(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 substitute = NSubstitute.Substitute.[For](Of Foo)() + {call}.Returns(1).{method}(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 substitute = NSubstitute.Substitute.[For](Of Foo)() + {call}.Returns(1).{method}(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 substitute = NSubstitute.Substitute.[For](Of IFoo)() + {call}.Returns(1).{method}(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 substitute = NSubstitute.Substitute.[For](Of Foo)() + {call}.Returns(1).{method}(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 substitute = NSubstitute.Substitute.[For](Of Foo)() + {call}.Returns(1).{method}(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 substitute = NSubstitute.Substitute.[For](Of Foo)() + {call}.Returns(1).{method}(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 substitute = NSubstitute.Substitute.[For](Of Foo)() + substitute.Bar(value).Returns(1).{method}(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 substitute = NSubstitute.Substitute.[For](Of Foo)() + substitute.Bar(value).Returns(1).{method}(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 substitute = NSubstitute.Substitute.[For](Of Foo)() + substitute.Bar(value).Returns(1).{method}(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 substitute = NSubstitute.Substitute.[For](Of Foo)() + substitute.Bar(value).Returns(1).{method}(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 substitute = NSubstitute.Substitute.[For](Of Foo)() + substitute.Bar(value).Returns(1).{method}(Function(callInfo) + callInfo(0) = {right} + End Function) + End Sub + End Class +End Namespace +"; + + await VerifyNoDiagnostic(source); + } + } +} \ No newline at end of file