diff --git a/NSubstitute.Analyzers.sln b/NSubstitute.Analyzers.sln index edc47b6d..f2806f28 100644 --- a/NSubstitute.Analyzers.sln +++ b/NSubstitute.Analyzers.sln @@ -43,6 +43,7 @@ ProjectSection(SolutionItems) = preProject documentation\rules\NS3006.md = documentation\rules\NS3006.md documentation\rules\NS4000.md = documentation\rules\NS4000.md documentation\rules\NS5000.md = documentation\rules\NS5000.md + documentation\rules\NS5001.md = documentation\rules\NS5001.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{1629DF5F-9BC0-49C0-975E-E45C3E58EB3A}" diff --git a/benchmarks/NSubstitute.Analyzers.Benchmarks.Source.CSharp/DiagnosticsSources/ReceivedLikeUsedInReceivedInOrderDiagnosticSource.cs b/benchmarks/NSubstitute.Analyzers.Benchmarks.Source.CSharp/DiagnosticsSources/ReceivedLikeUsedInReceivedInOrderDiagnosticSource.cs new file mode 100644 index 00000000..86c947eb --- /dev/null +++ b/benchmarks/NSubstitute.Analyzers.Benchmarks.Source.CSharp/DiagnosticsSources/ReceivedLikeUsedInReceivedInOrderDiagnosticSource.cs @@ -0,0 +1,22 @@ +using NSubstitute.Analyzers.Benchmarks.Source.CSharp.Models; + +namespace NSubstitute.Analyzers.Benchmarks.Source.CSharp.DiagnosticsSources +{ + public class ReceivedLikeUsedInReceivedInOrderDiagnosticSource + { + public void NS5001_ReceivedLikeUsedInReceivedInOrderCallback() + { + var substitute = Substitute.For(); + Received.InOrder(() => + { + substitute.Received().ObjectReturningMethod(); + _ = substitute.Received().InternalObjectReturningProperty; + _ = substitute.Received()[0]; + + SubstituteExtensions.Received(substitute).ObjectReturningMethod(); + _ = SubstituteExtensions.Received(substitute).InternalObjectReturningProperty; + _ = SubstituteExtensions.Received(substitute)[0]; + }); + } + } +} \ No newline at end of file diff --git a/benchmarks/NSubstitute.Analyzers.Benchmarks.Source.VisualBasic/DiagnosticsSources/ReceivedLikeUsedInReceivedInOrderDiagnosticSource.vb b/benchmarks/NSubstitute.Analyzers.Benchmarks.Source.VisualBasic/DiagnosticsSources/ReceivedLikeUsedInReceivedInOrderDiagnosticSource.vb new file mode 100644 index 00000000..20e7b318 --- /dev/null +++ b/benchmarks/NSubstitute.Analyzers.Benchmarks.Source.VisualBasic/DiagnosticsSources/ReceivedLikeUsedInReceivedInOrderDiagnosticSource.vb @@ -0,0 +1,18 @@ +Imports NSubstitute.Analyzers.Benchmarks.Source.VisualBasic.Models + +Namespace DiagnosticsSources + Public Class ReceivedLikeUsedInReceivedInOrderDiagnosticSource + Public Sub NS5001_ReceivedLikeUsedInReceivedInOrderCallback() + Dim substitute = NSubstitute.Substitute.[For](Of Foo)() + NSubstitute.Received.InOrder(Function() + substitute.Received().ObjectReturningMethod() + Dim x = substitute.Received().InternalObjectReturningProperty + Dim y = substitute.Received()(0) + SubstituteExtensions.Received(substitute).ObjectReturningMethod() + Dim a = SubstituteExtensions.Received(substitute).InternalObjectReturningProperty + Dim b = SubstituteExtensions.Received(substitute)(0) + Throw New System.Exception() + End Function) + End Sub + End Class +End Namespace diff --git a/benchmarks/NSubstitute.Analyzers.Benchmarks/CSharp/CSharpDiagnosticAnalyzersBenchmarks.cs b/benchmarks/NSubstitute.Analyzers.Benchmarks/CSharp/CSharpDiagnosticAnalyzersBenchmarks.cs index de00cf6e..a0d134e7 100644 --- a/benchmarks/NSubstitute.Analyzers.Benchmarks/CSharp/CSharpDiagnosticAnalyzersBenchmarks.cs +++ b/benchmarks/NSubstitute.Analyzers.Benchmarks/CSharp/CSharpDiagnosticAnalyzersBenchmarks.cs @@ -25,6 +25,8 @@ public class CSharpDiagnosticAnalyzersBenchmarks : AbstractDiagnosticAnalyzersBe protected override AnalyzerBenchmark ArgumentMatcherAnalyzerBenchmark { get; } + protected override AnalyzerBenchmark ReceivedInReceivedInOrderAnalyzerBenchmark { get; } + protected override AbstractSolutionLoader SolutionLoader { get; } protected override string SourceProjectFolderName { get; } @@ -46,6 +48,7 @@ public CSharpDiagnosticAnalyzersBenchmarks() SubstituteAnalyzerBenchmark = AnalyzerBenchmark.CreateBenchmark(Solution, new SubstituteAnalyzer()); UnusedReceivedAnalyzerBenchmark = AnalyzerBenchmark.CreateBenchmark(Solution, new UnusedReceivedAnalyzer()); ArgumentMatcherAnalyzerBenchmark = AnalyzerBenchmark.CreateBenchmark(Solution, new NonSubstitutableMemberArgumentMatcherAnalyzer()); + ReceivedInReceivedInOrderAnalyzerBenchmark = AnalyzerBenchmark.CreateBenchmark(Solution, new ReceivedInReceivedInOrderAnalyzer()); } } } \ No newline at end of file diff --git a/benchmarks/NSubstitute.Analyzers.Benchmarks/Shared/AbstractDiagnosticAnalyzersBenchmarks.cs b/benchmarks/NSubstitute.Analyzers.Benchmarks/Shared/AbstractDiagnosticAnalyzersBenchmarks.cs index 1fff2eb7..109ff5b5 100644 --- a/benchmarks/NSubstitute.Analyzers.Benchmarks/Shared/AbstractDiagnosticAnalyzersBenchmarks.cs +++ b/benchmarks/NSubstitute.Analyzers.Benchmarks/Shared/AbstractDiagnosticAnalyzersBenchmarks.cs @@ -33,6 +33,8 @@ public abstract class AbstractDiagnosticAnalyzersBenchmarks protected abstract AnalyzerBenchmark ArgumentMatcherAnalyzerBenchmark { get; } + protected abstract AnalyzerBenchmark ReceivedInReceivedInOrderAnalyzerBenchmark { get; } + protected abstract AbstractSolutionLoader SolutionLoader { get; } protected abstract string SourceProjectFolderName { get; } @@ -100,6 +102,12 @@ public void ArgumentMatcherAnalyzer() ArgumentMatcherAnalyzerBenchmark.Run(); } + [Benchmark] + public void ReceivedInReceivedInOrderAnalyzer() + { + ReceivedInReceivedInOrderAnalyzerBenchmark.Run(); + } + [IterationCleanup(Target = nameof(ArgumentMatcherAnalyzer))] public void CleanUp() { diff --git a/benchmarks/NSubstitute.Analyzers.Benchmarks/VisualBasic/VisualBasicDiagnosticAnalyzersBenchmarks.cs b/benchmarks/NSubstitute.Analyzers.Benchmarks/VisualBasic/VisualBasicDiagnosticAnalyzersBenchmarks.cs index de1100c2..0838b9e1 100644 --- a/benchmarks/NSubstitute.Analyzers.Benchmarks/VisualBasic/VisualBasicDiagnosticAnalyzersBenchmarks.cs +++ b/benchmarks/NSubstitute.Analyzers.Benchmarks/VisualBasic/VisualBasicDiagnosticAnalyzersBenchmarks.cs @@ -25,6 +25,8 @@ public class VisualBasicDiagnosticAnalyzersBenchmarks : AbstractDiagnosticAnalyz protected override AnalyzerBenchmark ArgumentMatcherAnalyzerBenchmark { get; } + protected override AnalyzerBenchmark ReceivedInReceivedInOrderAnalyzerBenchmark { get; } + protected override AbstractSolutionLoader SolutionLoader { get; } protected override string SourceProjectFolderName { get; } @@ -46,6 +48,7 @@ public VisualBasicDiagnosticAnalyzersBenchmarks() SubstituteAnalyzerBenchmark = AnalyzerBenchmark.CreateBenchmark(Solution, new SubstituteAnalyzer()); UnusedReceivedAnalyzerBenchmark = AnalyzerBenchmark.CreateBenchmark(Solution, new UnusedReceivedAnalyzer()); ArgumentMatcherAnalyzerBenchmark = AnalyzerBenchmark.CreateBenchmark(Solution, new NonSubstitutableMemberArgumentMatcherAnalyzer()); + ReceivedInReceivedInOrderAnalyzerBenchmark = AnalyzerBenchmark.CreateBenchmark(Solution, new ReceivedInReceivedInOrderAnalyzer()); } } } \ No newline at end of file diff --git a/documentation/rules/NS5001.md b/documentation/rules/NS5001.md new file mode 100644 index 00000000..3d733d0b --- /dev/null +++ b/documentation/rules/NS5001.md @@ -0,0 +1,83 @@ +# NS5001 + + + + + + + + + + +
CheckIdNS5001
CategoryUsage
+ +## Cause + +Usage of received-like method in `Received.InOrder` callback. + +## Rule description + +A violation of this rule occurs when any of the following are used inside a `Received.InOrder` callback: + +- `Received()` +- `ReceivedWithAnyArgs()` +- `DidNotReceive()` +- `DidNotReceiveWithAnyArgs()` + +Calls within `Received.InOrder` are already checked to ensure they were received in the expected order. Individual received-like assertions should be moved outside the `Received.InOrder` callback. + +## How to fix violations + +To fix a violation of this rule, remove received-like method calls from `Received.InOrder` callback. + +For example: + +````c# +// Incorrect: +Received.InOrder(() => +{ + sub.Received().Baz(); + sub.Received().Bar(); +}) + +// Correct: +Received.InOrder(() => +{ + sub.Baz(); + sub.Bar(); +}) +```` + +Alternatively, move any received-like methods outside of `Received.InOrder` block if ordering is not important: + +````c# +// Incorrect: +Received.InOrder(() => +{ + sub.Baz(); + sub.Zap(); + sub.DidNotReceive().Bar(); +}) + +// Correct: +Received.InOrder(() => +{ + sub.Baz(); + sub.Zap(); +}) +sub.DidNotReceive().Bar(); +```` + +## How to suppress violations + +This warning can be suppressed by disabling the warning in the **ruleset** file for the project. +The warning can also be suppressed programmatically for an assembly: +````c# +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "NS5001:Received-like method used in Received.InOrder block.", Justification = "Reviewed")] +```` + +Or for a specific code block: +````c# +#pragma warning disable NS5001 // Received-like method used in Received.InOrder block. +// the code which produces warning +#pragma warning restore NS5001 // Received-like method used in Received.InOrder block. diff --git a/documentation/rules/README.md b/documentation/rules/README.md index 4cab1147..e44bda4a 100644 --- a/documentation/rules/README.md +++ b/documentation/rules/README.md @@ -24,4 +24,5 @@ | [NS3005](NS3005.md) | Argument specification | Assigning call argument which is not ref nor out argument. | | [NS3006](NS3006.md) | Argument specification | Conflicting assignments to out/ref arguments. | | [NS4000](NS4000.md) | Call configuration | Calling substitute from within `Returns` block. | -| [NS5000](NS5000.md) | Usage | Checking received calls without specifying member. | \ No newline at end of file +| [NS5000](NS5000.md) | Usage | Checking received calls without specifying member. | +| [NS5001](NS5001.md) | Usage | Usage of received-like method in `Received.InOrder` callback. | diff --git a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/ReceivedInReceivedInOrderAnalyzer.cs b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/ReceivedInReceivedInOrderAnalyzer.cs new file mode 100644 index 00000000..ee7b4017 --- /dev/null +++ b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/ReceivedInReceivedInOrderAnalyzer.cs @@ -0,0 +1,19 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using NSubstitute.Analyzers.Shared.DiagnosticAnalyzers; + +namespace NSubstitute.Analyzers.CSharp.DiagnosticAnalyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + internal sealed class ReceivedInReceivedInOrderAnalyzer : AbstractReceivedInReceivedInOrderAnalyzer + { + public ReceivedInReceivedInOrderAnalyzer() + : base(SubstitutionNodeFinder.Instance, CSharp.DiagnosticDescriptorsProvider.Instance) + { + } + + protected override SyntaxKind InvocationExpressionKind { get; } = SyntaxKind.InvocationExpression; + } +} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/AbstractDiagnosticDescriptorsProvider.cs b/src/NSubstitute.Analyzers.Shared/AbstractDiagnosticDescriptorsProvider.cs index 70b90944..4ce35fd8 100644 --- a/src/NSubstitute.Analyzers.Shared/AbstractDiagnosticDescriptorsProvider.cs +++ b/src/NSubstitute.Analyzers.Shared/AbstractDiagnosticDescriptorsProvider.cs @@ -49,5 +49,7 @@ internal class AbstractDiagnosticDescriptorsProvider : IDiagnosticDescriptors public DiagnosticDescriptor ConflictingArgumentAssignments { get; } = DiagnosticDescriptors.ConflictingArgumentAssignments; public DiagnosticDescriptor NonSubstitutableMemberArgumentMatcherUsage { get; } = DiagnosticDescriptors.NonSubstitutableMemberArgumentMatcherUsage; + + public DiagnosticDescriptor ReceivedUsedInReceivedInOrder { get; } = DiagnosticDescriptors.ReceivedUsedInReceivedInOrder; } } \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractReceivedInReceivedInOrderAnalyzer.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractReceivedInReceivedInOrderAnalyzer.cs new file mode 100644 index 00000000..e618f0c7 --- /dev/null +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractReceivedInReceivedInOrderAnalyzer.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using NSubstitute.Analyzers.Shared.Extensions; + +namespace NSubstitute.Analyzers.Shared.DiagnosticAnalyzers +{ + internal abstract class AbstractReceivedInReceivedInOrderAnalyzer : AbstractDiagnosticAnalyzer + where TSyntaxKind : struct + where TInvocationExpressionSyntax : SyntaxNode + { + private readonly ISubstitutionNodeFinder _substitutionNodeFinder; + private readonly Action _analyzeInvocationAction; + + public override ImmutableArray SupportedDiagnostics { get; } + + protected AbstractReceivedInReceivedInOrderAnalyzer( + ISubstitutionNodeFinder substitutionNodeFinder, + IDiagnosticDescriptorsProvider diagnosticDescriptorsProvider) + : base(diagnosticDescriptorsProvider) + { + _substitutionNodeFinder = substitutionNodeFinder; + _analyzeInvocationAction = AnalyzeInvocation; + SupportedDiagnostics = ImmutableArray.Create(diagnosticDescriptorsProvider.ReceivedUsedInReceivedInOrder); + } + + protected abstract TSyntaxKind InvocationExpressionKind { get; } + + protected override void InitializeAnalyzer(AnalysisContext context) + { + context.RegisterSyntaxNodeAction(_analyzeInvocationAction, InvocationExpressionKind); + } + + private void AnalyzeInvocation(SyntaxNodeAnalysisContext syntaxNodeContext) + { + var invocationExpression = (TInvocationExpressionSyntax)syntaxNodeContext.Node; + var methodSymbolInfo = syntaxNodeContext.SemanticModel.GetSymbolInfo(invocationExpression); + + if (methodSymbolInfo.Symbol?.Kind != SymbolKind.Method) + { + return; + } + + if (methodSymbolInfo.Symbol.IsReceivedInOrderMethod() == false) + { + return; + } + + foreach (var syntaxNode in _substitutionNodeFinder.FindForReceivedInOrderExpression( + syntaxNodeContext, + invocationExpression, + (IMethodSymbol)methodSymbolInfo.Symbol)) + { + var symbolInfo = syntaxNodeContext.SemanticModel.GetSymbolInfo(syntaxNode); + + if (symbolInfo.Symbol.IsReceivedLikeMethod() == false) + { + continue; + } + + var diagnostic = Diagnostic.Create( + DiagnosticDescriptorsProvider.ReceivedUsedInReceivedInOrder, + syntaxNode.GetLocation(), + symbolInfo.Symbol.Name); + + syntaxNodeContext.ReportDiagnostic(diagnostic); + } + } + } +} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticDescriptors.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticDescriptors.cs index 03734eb6..aa44f8f2 100644 --- a/src/NSubstitute.Analyzers.Shared/DiagnosticDescriptors.cs +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticDescriptors.cs @@ -197,6 +197,13 @@ internal class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); + public static DiagnosticDescriptor ReceivedUsedInReceivedInOrder { get; } = CreateDiagnosticDescriptor( + name: nameof(ReceivedUsedInReceivedInOrder), + id: DiagnosticIdentifiers.ReceivedUsedInReceivedInOrder, + category: DiagnosticCategory.Usage.GetDisplayName(), + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + private static DiagnosticDescriptor CreateDiagnosticDescriptor( string name, string id, string category, DiagnosticSeverity defaultSeverity, bool isEnabledByDefault) { diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticIdentifiers.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticIdentifiers.cs index 75d9a204..ffb39f12 100644 --- a/src/NSubstitute.Analyzers.Shared/DiagnosticIdentifiers.cs +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticIdentifiers.cs @@ -28,5 +28,6 @@ internal class DiagnosticIdentifiers public const string ReEntrantSubstituteCall = "NS4000"; public const string UnusedReceived = "NS5000"; + public const string ReceivedUsedInReceivedInOrder = "NS5001"; } } \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/Extensions/SubstituteSymbolExtensions.cs b/src/NSubstitute.Analyzers.Shared/Extensions/SubstituteSymbolExtensions.cs index c5b75056..99646a58 100644 --- a/src/NSubstitute.Analyzers.Shared/Extensions/SubstituteSymbolExtensions.cs +++ b/src/NSubstitute.Analyzers.Shared/Extensions/SubstituteSymbolExtensions.cs @@ -48,6 +48,11 @@ public static bool IsReceivedLikeMethod(this ISymbol symbol) IsMember(symbol, MetadataNames.ReceivedWithQuantityMethodNames); } + public static bool IsReceivedInOrderMethod(this ISymbol symbol) + { + return IsMember(symbol, MetadataNames.NSubstituteInOrderMethod, MetadataNames.NSubstituteReceivedFullTypeName); + } + public static bool IsWhenLikeMethod(this ISymbol symbol) { return IsMember(symbol, MetadataNames.WhenMethodNames); diff --git a/src/NSubstitute.Analyzers.Shared/IDiagnosticDescriptorsProvider.cs b/src/NSubstitute.Analyzers.Shared/IDiagnosticDescriptorsProvider.cs index 25f5b265..b336f964 100644 --- a/src/NSubstitute.Analyzers.Shared/IDiagnosticDescriptorsProvider.cs +++ b/src/NSubstitute.Analyzers.Shared/IDiagnosticDescriptorsProvider.cs @@ -49,5 +49,7 @@ internal interface IDiagnosticDescriptorsProvider DiagnosticDescriptor ConflictingArgumentAssignments { get; } DiagnosticDescriptor NonSubstitutableMemberArgumentMatcherUsage { get; } + + DiagnosticDescriptor ReceivedUsedInReceivedInOrder { get; } } } \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/MetadataNames.cs b/src/NSubstitute.Analyzers.Shared/MetadataNames.cs index 77617102..d9d99277 100644 --- a/src/NSubstitute.Analyzers.Shared/MetadataNames.cs +++ b/src/NSubstitute.Analyzers.Shared/MetadataNames.cs @@ -27,6 +27,8 @@ internal class MetadataNames public const string NSubstituteReceivedWithAnyArgsMethod = "ReceivedWithAnyArgs"; public const string NSubstituteDidNotReceiveMethod = "DidNotReceive"; public const string NSubstituteDidNotReceiveWithAnyArgsMethod = "DidNotReceiveWithAnyArgs"; + public const string NSubstituteInOrderMethod = "InOrder"; + public const string NSubstituteReceivedFullTypeName = "NSubstitute.Received"; public const string NSubstituteForMethod = "For"; public const string NSubstituteForPartsOfMethod = "ForPartsOf"; public const string SubstituteFactoryCreate = "Create"; diff --git a/src/NSubstitute.Analyzers.Shared/Resources.Designer.cs b/src/NSubstitute.Analyzers.Shared/Resources.Designer.cs index bb435f63..ea3d04c2 100644 --- a/src/NSubstitute.Analyzers.Shared/Resources.Designer.cs +++ b/src/NSubstitute.Analyzers.Shared/Resources.Designer.cs @@ -425,5 +425,23 @@ internal static string NonSubstitutableMemberArgumentMatcherUsageTitle { return ResourceManager.GetString("NonSubstitutableMemberArgumentMatcherUsageTitle", resourceCulture); } } + + internal static string ReceivedUsedInReceivedInOrderDescription { + get { + return ResourceManager.GetString("ReceivedUsedInReceivedInOrderDescription", resourceCulture); + } + } + + internal static string ReceivedUsedInReceivedInOrderMessageFormat { + get { + return ResourceManager.GetString("ReceivedUsedInReceivedInOrderMessageFormat", resourceCulture); + } + } + + internal static string ReceivedUsedInReceivedInOrderTitle { + get { + return ResourceManager.GetString("ReceivedUsedInReceivedInOrderTitle", resourceCulture); + } + } } } diff --git a/src/NSubstitute.Analyzers.Shared/Resources.resx b/src/NSubstitute.Analyzers.Shared/Resources.resx index 95dea413..d74334bf 100644 --- a/src/NSubstitute.Analyzers.Shared/Resources.resx +++ b/src/NSubstitute.Analyzers.Shared/Resources.resx @@ -375,4 +375,17 @@ Argument matcher used with a non-virtual member of a class. The title of the diagnostic. + + + Received-like method used in Received.InOrder block. + An optional longer localizable description of the diagnostic. + + + {0} method used in Received.InOrder block. + The format-able message the diagnostic displays. + + + Received-like method used in Received.InOrder block. + The title of the diagnostic. + \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/ReceivedInReceivedInOrderAnalyzer.cs b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/ReceivedInReceivedInOrderAnalyzer.cs new file mode 100644 index 00000000..ba8de16f --- /dev/null +++ b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/ReceivedInReceivedInOrderAnalyzer.cs @@ -0,0 +1,19 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.VisualBasic; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; +using NSubstitute.Analyzers.Shared.DiagnosticAnalyzers; + +namespace NSubstitute.Analyzers.VisualBasic.DiagnosticAnalyzers +{ + [DiagnosticAnalyzer(LanguageNames.VisualBasic)] + internal sealed class ReceivedInReceivedInOrderAnalyzer : AbstractReceivedInReceivedInOrderAnalyzer + { + public ReceivedInReceivedInOrderAnalyzer() + : base(SubstitutionNodeFinder.Instance, VisualBasic.DiagnosticDescriptorsProvider.Instance) + { + } + + protected override SyntaxKind InvocationExpressionKind { get; } = SyntaxKind.InvocationExpression; + } +} \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/ReceivedInReceivedInOrderAnalyzerTests/ReceivedAsExtensionMethodTests.cs b/tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/ReceivedInReceivedInOrderAnalyzerTests/ReceivedAsExtensionMethodTests.cs new file mode 100644 index 00000000..e04eefcf --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/ReceivedInReceivedInOrderAnalyzerTests/ReceivedAsExtensionMethodTests.cs @@ -0,0 +1,250 @@ +using System.Threading.Tasks; +using NSubstitute.Analyzers.Tests.Shared.Extensibility; + +namespace NSubstitute.Analyzers.Tests.CSharp.DiagnosticAnalyzerTests.ReceivedInReceivedInOrderAnalyzerTests +{ + [CombinatoryData( + "Received(Quantity.None())", + "Received(Quantity.None())", + "Received()", + "Received()", + "ReceivedWithAnyArgs(Quantity.None())", + "ReceivedWithAnyArgs(Quantity.None())", + "ReceivedWithAnyArgs()", + "ReceivedWithAnyArgs()", + "DidNotReceive()", + "DidNotReceive()", + "DidNotReceiveWithAnyArgs()", + "DidNotReceiveWithAnyArgs()")] + public class ReceivedAsExtensionMethodTests : ReceivedInReceivedInOrderDiagnosticVerifier + { + public override async Task ReportsDiagnostic_WhenUsingReceivedLikeMethodInReceivedInOrderBlock_ForMethod(string method) + { + var source = $@"using NSubstitute; +using NSubstitute.ReceivedExtensions; + +namespace MyNamespace +{{ + public interface IFoo + {{ + int Bar(); + }} + + public class FooTests + {{ + public void Test() + {{ + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + {{ + [|substitute.{method}|].Bar(); + }}); + }} + }} +}}"; + + await VerifyDiagnostic(source, method); + } + + public override async Task ReportsDiagnostic_WhenUsingReceivedLikeMethodInReceivedInOrderBlock_ForProperty(string method) + { + var source = $@"using NSubstitute; +using NSubstitute.ReceivedExtensions; + +namespace MyNamespace +{{ + public interface IFoo + {{ + int Bar {{ get; }} + }} + + public class FooTests + {{ + public void Test() + {{ + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + {{ + _ = [|substitute.{method}|].Bar; + }}); + }} + }} +}}"; + + await VerifyDiagnostic(source, method); + } + + public override async Task ReportsDiagnostic_WhenUsingReceivedLikeMethodInReceivedInOrderBlock_ForIndexer(string method) + { + var source = $@"using NSubstitute; +using NSubstitute.ReceivedExtensions; + +namespace MyNamespace +{{ + public interface IFoo + {{ + int this[int x] {{ get; }} + }} + + public class FooTests + {{ + public void Test() + {{ + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + {{ + _ = [|substitute.{method}|][0]; + }}); + }} + }} +}}"; + + await VerifyDiagnostic(source, method); + } + + public override async Task ReportsNoDiagnostic_WhenUsingReceivedLikeMethodOutsideOfReceivedInOrderBlock(string method) + { + var source = $@"using NSubstitute; +using NSubstitute.ReceivedExtensions; + +namespace MyNamespace +{{ + public interface IFoo + {{ + int Bar(); + + int Foo {{ get; }} + + int this[int x] {{ get; }} + }} + + public class FooTests + {{ + public void Test() + {{ + var substitute = NSubstitute.Substitute.For(); + substitute.{method}.Bar(); + _ = substitute.{method}[0]; + _ = substitute.{method}.Foo; + + Received.InOrder(() => + {{ + _ = substitute[0]; + _ = substitute.Foo; + substitute.Bar(); + }}); + }} + }} +}}"; + + await VerifyNoDiagnostic(source); + } + + [CombinatoryData( + "Received(Quantity.None())", + "Received(Quantity.None())", + "Received(1, 1)", + "Received(1, 1)", + "ReceivedWithAnyArgs(Quantity.None())", + "ReceivedWithAnyArgs(Quantity.None())", + "ReceivedWithAnyArgs(1, 1)", + "ReceivedWithAnyArgs(1, 1)", + "DidNotReceive(1, 1)", + "DidNotReceive(1, 1)", + "DidNotReceiveWithAnyArgs(1, 1)", + "DidNotReceiveWithAnyArgs(1, 1)")] + public override async Task ReportsNoDiagnostics_WhenUsingUnfortunatelyNamedMethod(string method) + { + var source = $@" + +namespace NSubstitute +{{ + public class Quantity + {{ + public static Quantity None() => null; + }} + + public class Foo + {{ + public int Bar() + {{ + return 1; + }} + }} + + public static class SubstituteExtensions + {{ + public static T Received(this T substitute, int x, int y) + {{ + return default(T); + }} + + public static T ReceivedWithAnyArgs(this T substitute, int x, int y) + {{ + return default(T); + }} + + public static T DidNotReceive(this T substitute, int x, int y) + {{ + return default(T); + }} + + public static T DidNotReceiveWithAnyArgs(this T substitute, int x, int y) + {{ + return default(T); + }} + }} + + public static class ReceivedExtensions + {{ + public static T Received(this T substitute, Quantity x) + {{ + return default(T); + }} + + public static T ReceivedWithAnyArgs(this T substitute, Quantity x) + {{ + return default(T); + }} + + public static T DidNotReceive(this T substitute, Quantity x) + {{ + return default(T); + }} + + public static T DidNotReceiveWithAnyArgs(this T substitute, Quantity x) + {{ + return default(T); + }} + }} + + public class FooTests + {{ + public void Test() + {{ + Foo substitute = null; + Received.InOrder(() => + {{ + substitute.{method}.Bar(); + }}); + }} + }} +}}"; + await VerifyNoDiagnostic(source); + } + + private static string GetPlainMethodName(string methodName) + { + return methodName.Replace("", string.Empty) + .Replace("Quantity.None()", string.Empty) + .Replace("()", string.Empty); + } + + private async Task VerifyDiagnostic(string source, string methodName) + { + var plainMethodName = GetPlainMethodName(methodName); + + await VerifyDiagnostic(source, Descriptor, $"{plainMethodName} method used in Received.InOrder block."); + } + } +} \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/ReceivedInReceivedInOrderAnalyzerTests/ReceivedAsOrdinaryMethodTests.cs b/tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/ReceivedInReceivedInOrderAnalyzerTests/ReceivedAsOrdinaryMethodTests.cs new file mode 100644 index 00000000..c813b48b --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/ReceivedInReceivedInOrderAnalyzerTests/ReceivedAsOrdinaryMethodTests.cs @@ -0,0 +1,256 @@ +using System.Threading.Tasks; +using NSubstitute.Analyzers.Tests.Shared.Extensibility; + +namespace NSubstitute.Analyzers.Tests.CSharp.DiagnosticAnalyzerTests.ReceivedInReceivedInOrderAnalyzerTests +{ + [CombinatoryData( + "ReceivedExtensions.Received(substitute, Quantity.None())", + "ReceivedExtensions.Received(substitute, Quantity.None())", + "SubstituteExtensions.Received(substitute)", + "SubstituteExtensions.Received(substitute)", + "ReceivedExtensions.ReceivedWithAnyArgs(substitute, Quantity.None())", + "ReceivedExtensions.ReceivedWithAnyArgs(substitute, Quantity.None())", + "SubstituteExtensions.ReceivedWithAnyArgs(substitute)", + "SubstituteExtensions.ReceivedWithAnyArgs(substitute)", + "SubstituteExtensions.DidNotReceive(substitute)", + "SubstituteExtensions.DidNotReceive(substitute)", + "SubstituteExtensions.DidNotReceiveWithAnyArgs(substitute)", + "SubstituteExtensions.DidNotReceiveWithAnyArgs(substitute)")] + public class ReceivedAsOrdinaryMethodTests : ReceivedInReceivedInOrderDiagnosticVerifier + { + public override async Task ReportsDiagnostic_WhenUsingReceivedLikeMethodInReceivedInOrderBlock_ForMethod(string method) + { + var source = $@"using NSubstitute; +using NSubstitute.ReceivedExtensions; + +namespace MyNamespace +{{ + public interface IFoo + {{ + int Bar(); + }} + + public class FooTests + {{ + public void Test() + {{ + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + {{ + [|{method}|].Bar(); + }}); + }} + }} +}}"; + + await VerifyDiagnostic(source, method); + } + + public override async Task ReportsDiagnostic_WhenUsingReceivedLikeMethodInReceivedInOrderBlock_ForProperty(string method) + { + var source = $@"using NSubstitute; +using NSubstitute.ReceivedExtensions; + +namespace MyNamespace +{{ + public interface IFoo + {{ + int Bar {{ get; }} + }} + + public class FooTests + {{ + public void Test() + {{ + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + {{ + _ = [|{method}|].Bar; + }}); + }} + }} +}}"; + + await VerifyDiagnostic(source, method); + } + + public override async Task ReportsDiagnostic_WhenUsingReceivedLikeMethodInReceivedInOrderBlock_ForIndexer(string method) + { + var source = $@"using NSubstitute; +using NSubstitute.ReceivedExtensions; + +namespace MyNamespace +{{ + public interface IFoo + {{ + int this[int x] {{ get; }} + }} + + public class FooTests + {{ + public void Test() + {{ + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + {{ + _ = [|{method}|][0]; + }}); + }} + }} +}}"; + + await VerifyDiagnostic(source, method); + } + + public override async Task ReportsNoDiagnostic_WhenUsingReceivedLikeMethodOutsideOfReceivedInOrderBlock(string method) + { + var source = $@"using NSubstitute; +using NSubstitute.ReceivedExtensions; + +namespace MyNamespace +{{ + public interface IFoo + {{ + int Bar(); + + int Foo {{ get; }} + + int this[int x] {{ get; }} + }} + + public class FooTests + {{ + public void Test() + {{ + var substitute = NSubstitute.Substitute.For(); + {method}.Bar(); + _ = {method}[0]; + _ = {method}.Foo; + + Received.InOrder(() => + {{ + _ = substitute[0]; + _ = substitute.Foo; + substitute.Bar(); + }}); + }} + }} +}}"; + + await VerifyNoDiagnostic(source); + } + + [CombinatoryData( + "ReceivedExtensions.Received(substitute, Quantity.None())", + "ReceivedExtensions.Received(substitute, Quantity.None())", + "SubstituteExtensions.Received(substitute, 1, 1)", + "SubstituteExtensions.Received(substitute, 1, 1)", + "ReceivedExtensions.ReceivedWithAnyArgs(substitute, Quantity.None())", + "ReceivedExtensions.ReceivedWithAnyArgs(substitute, Quantity.None())", + "SubstituteExtensions.ReceivedWithAnyArgs(substitute, 1, 1)", + "SubstituteExtensions.ReceivedWithAnyArgs(substitute, 1, 1)", + "SubstituteExtensions.DidNotReceive(substitute, 1, 1)", + "SubstituteExtensions.DidNotReceive(substitute, 1, 1)", + "SubstituteExtensions.DidNotReceiveWithAnyArgs(substitute, 1, 1)", + "SubstituteExtensions.DidNotReceiveWithAnyArgs(substitute, 1, 1)")] + public override async Task ReportsNoDiagnostics_WhenUsingUnfortunatelyNamedMethod(string method) + { + var source = $@" + +namespace NSubstitute +{{ + public class Quantity + {{ + public static Quantity None() => null; + }} + + public class Foo + {{ + public int Bar() + {{ + return 1; + }} + }} + + public static class SubstituteExtensions + {{ + public static T Received(this T substitute, int x, int y) + {{ + return default(T); + }} + + public static T ReceivedWithAnyArgs(this T substitute, int x, int y) + {{ + return default(T); + }} + + public static T DidNotReceive(this T substitute, int x, int y) + {{ + return default(T); + }} + + public static T DidNotReceiveWithAnyArgs(this T substitute, int x, int y) + {{ + return default(T); + }} + }} + + public static class ReceivedExtensions + {{ + public static T Received(this T substitute, Quantity x) + {{ + return default(T); + }} + + public static T ReceivedWithAnyArgs(this T substitute, Quantity x) + {{ + return default(T); + }} + + public static T DidNotReceive(this T substitute, Quantity x) + {{ + return default(T); + }} + + public static T DidNotReceiveWithAnyArgs(this T substitute, Quantity x) + {{ + return default(T); + }} + }} + + public class FooTests + {{ + public void Test() + {{ + Foo substitute = null; + Received.InOrder(() => + {{ + {method}.Bar(); + }}); + }} + }} +}}"; + await VerifyNoDiagnostic(source); + } + + private static string GetPlainMethodName(string methodName) + { + var plainMethodName = methodName.Replace("", string.Empty) + .Replace("(substitute, Quantity.None())", string.Empty) + .Replace("(substitute)", string.Empty); + + var planMethodNameWithoutNamespace = + plainMethodName.Replace("ReceivedExtensions.", string.Empty) + .Replace("SubstituteExtensions.", string.Empty); + + return planMethodNameWithoutNamespace; + } + + private async Task VerifyDiagnostic(string source, string methodName) + { + var plainMethodName = GetPlainMethodName(methodName); + + await VerifyDiagnostic(source, Descriptor, $"{plainMethodName} method used in Received.InOrder block."); + } + } +} diff --git a/tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/ReceivedInReceivedInOrderAnalyzerTests/ReceivedInReceivedInOrderDiagnosticVerifier.cs b/tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/ReceivedInReceivedInOrderAnalyzerTests/ReceivedInReceivedInOrderDiagnosticVerifier.cs new file mode 100644 index 00000000..e4a20471 --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/ReceivedInReceivedInOrderAnalyzerTests/ReceivedInReceivedInOrderDiagnosticVerifier.cs @@ -0,0 +1,42 @@ +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using NSubstitute.Analyzers.CSharp; +using NSubstitute.Analyzers.CSharp.DiagnosticAnalyzers; +using NSubstitute.Analyzers.Shared; +using NSubstitute.Analyzers.Tests.Shared.DiagnosticAnalyzers; +using NSubstitute.Analyzers.Tests.Shared.Extensibility; +using Xunit; + +namespace NSubstitute.Analyzers.Tests.CSharp.DiagnosticAnalyzerTests.ReceivedInReceivedInOrderAnalyzerTests +{ + public abstract class ReceivedInReceivedInOrderDiagnosticVerifier : CSharpDiagnosticVerifier, IReceivedInReceivedInOrderDiagnosticVerifier + { + protected DiagnosticDescriptor Descriptor { get; } = DiagnosticDescriptors.ReceivedUsedInReceivedInOrder; + + [CombinatoryTheory] + [InlineData] + public abstract Task ReportsDiagnostic_WhenUsingReceivedLikeMethodInReceivedInOrderBlock_ForMethod(string method); + + [CombinatoryTheory] + [InlineData] + public abstract Task ReportsDiagnostic_WhenUsingReceivedLikeMethodInReceivedInOrderBlock_ForProperty(string method); + + [CombinatoryTheory] + [InlineData] + public abstract Task ReportsDiagnostic_WhenUsingReceivedLikeMethodInReceivedInOrderBlock_ForIndexer(string method); + + [CombinatoryTheory] + [InlineData] + public abstract Task ReportsNoDiagnostic_WhenUsingReceivedLikeMethodOutsideOfReceivedInOrderBlock(string method); + + [CombinatoryTheory] + [InlineData] + public abstract Task ReportsNoDiagnostics_WhenUsingUnfortunatelyNamedMethod(string method); + + protected override DiagnosticAnalyzer GetDiagnosticAnalyzer() + { + return new ReceivedInReceivedInOrderAnalyzer(); + } + } +} \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.Shared/DiagnosticAnalyzers/IReceivedInReceivedInOrderDiagnosticVerifier.cs b/tests/NSubstitute.Analyzers.Tests.Shared/DiagnosticAnalyzers/IReceivedInReceivedInOrderDiagnosticVerifier.cs new file mode 100644 index 00000000..3c4968d6 --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.Shared/DiagnosticAnalyzers/IReceivedInReceivedInOrderDiagnosticVerifier.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; + +namespace NSubstitute.Analyzers.Tests.Shared.DiagnosticAnalyzers +{ + public interface IReceivedInReceivedInOrderDiagnosticVerifier + { + Task ReportsDiagnostic_WhenUsingReceivedLikeMethodInReceivedInOrderBlock_ForMethod(string method); + + Task ReportsDiagnostic_WhenUsingReceivedLikeMethodInReceivedInOrderBlock_ForProperty(string method); + + Task ReportsDiagnostic_WhenUsingReceivedLikeMethodInReceivedInOrderBlock_ForIndexer(string method); + + Task ReportsNoDiagnostic_WhenUsingReceivedLikeMethodOutsideOfReceivedInOrderBlock(string method); + + Task ReportsNoDiagnostics_WhenUsingUnfortunatelyNamedMethod(string method); + } +} \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.VisualBasic/DiagnosticAnalyzersTests/ReceivedInReceivedInOrderAnalyzerTests/ReceivedAsExtensionMethodTests.cs b/tests/NSubstitute.Analyzers.Tests.VisualBasic/DiagnosticAnalyzersTests/ReceivedInReceivedInOrderAnalyzerTests/ReceivedAsExtensionMethodTests.cs new file mode 100644 index 00000000..886488be --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.VisualBasic/DiagnosticAnalyzersTests/ReceivedInReceivedInOrderAnalyzerTests/ReceivedAsExtensionMethodTests.cs @@ -0,0 +1,212 @@ +using System.Threading.Tasks; +using NSubstitute.Analyzers.Tests.Shared.Extensibility; + +namespace NSubstitute.Analyzers.Tests.VisualBasic.DiagnosticAnalyzersTests.ReceivedInReceivedInOrderAnalyzerTests +{ + [CombinatoryData( + "Received(Quantity.None())", + "Received()", + "ReceivedWithAnyArgs(Quantity.None())", + "ReceivedWithAnyArgs()", + "DidNotReceive()", + "DidNotReceiveWithAnyArgs()")] + public class ReceivedAsExtensionMethodTests : ReceivedInReceivedInOrderDiagnosticVerifier + { + public override async Task ReportsDiagnostic_WhenUsingReceivedLikeMethodInReceivedInOrderBlock_ForMethod(string method) + { + var source = $@"Imports NSubstitute +Imports NSubstitute.ReceivedExtensions + +Namespace MyNamespace + Public Interface IFoo + Function Bar() As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of IFoo)() + NSubstitute.Received.InOrder(Function() + [|substitute.{method}|].Bar() + End Function) + End Sub + End Class +End Namespace +"; + + await VerifyDiagnostic(source, method); + } + + public override async Task ReportsDiagnostic_WhenUsingReceivedLikeMethodInReceivedInOrderBlock_ForProperty(string method) + { + var source = $@"Imports NSubstitute +Imports NSubstitute.ReceivedExtensions + +Namespace MyNamespace + Public Interface IFoo + ReadOnly Property Bar As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of IFoo)() + NSubstitute.Received.InOrder(Function() + Dim x = [|substitute.{method}|].Bar + End Function) + End Sub + End Class +End Namespace +"; + + await VerifyDiagnostic(source, method); + } + + public override async Task ReportsDiagnostic_WhenUsingReceivedLikeMethodInReceivedInOrderBlock_ForIndexer(string method) + { + var source = $@"Imports NSubstitute +Imports NSubstitute.ReceivedExtensions + +Namespace MyNamespace + Public Interface IFoo + ReadOnly Property Bar As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of IFoo)() + NSubstitute.Received.InOrder(Function() + Dim x = [|substitute.{method}|].Bar + End Function) + End Sub + End Class +End Namespace +"; + + await VerifyDiagnostic(source, method); + } + + public override async Task ReportsNoDiagnostic_WhenUsingReceivedLikeMethodOutsideOfReceivedInOrderBlock(string method) + { + var source = $@"Imports NSubstitute +Imports NSubstitute.ReceivedExtensions + +Namespace MyNamespace + Public Interface IFoo + Function Bar() As Integer + ReadOnly Property Foo 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 IFoo)() + substitute.{method}.Bar() + Dim x = substitute.{method}(0) + Dim y = substitute.{method}.Foo + NSubstitute.Received.InOrder(Function() + Dim a = substitute(0) + Dim b = substitute.Foo + substitute.Bar() + End Function) + End Sub + End Class +End Namespace +"; + + await VerifyNoDiagnostic(source); + } + + [CombinatoryData( + "Received(Quantity.None())", + "Received(1, 1)", + "ReceivedWithAnyArgs(Quantity.None())", + "ReceivedWithAnyArgs(1, 1)", + "DidNotReceive(1, 1)", + "DidNotReceiveWithAnyArgs(1, 1)")] + public override async Task ReportsNoDiagnostics_WhenUsingUnfortunatelyNamedMethod(string method) + { + var source = $@"Imports System +Imports System.Runtime.CompilerServices + +Namespace NSubstitute + Public Class Quantity + Public Shared Function None() As Quantity + Return Nothing + End Function + End Class + + Public Class Foo + Public Function Bar() As Integer + Return 1 + End Function + End Class + + Module SubstituteExtensions + + Function Received(Of T)(ByVal substitute As T, ByVal x As Integer, ByVal y As Integer) As T + Return Nothing + End Function + + + Function ReceivedWithAnyArgs(Of T)(ByVal substitute As T, ByVal x As Integer, ByVal y As Integer) As T + Return Nothing + End Function + + + Function DidNotReceive(Of T)(ByVal substitute As T, ByVal x As Integer, ByVal y As Integer) As T + Return Nothing + End Function + + + Function DidNotReceiveWithAnyArgs(Of T)(ByVal substitute As T, ByVal x As Integer, ByVal y As Integer) As T + Return Nothing + End Function + End Module + + Module ReceivedExtensions + + Function Received(Of T)(ByVal substitute As T, ByVal x As Quantity) As T + Return Nothing + End Function + + + Function ReceivedWithAnyArgs(Of T)(ByVal substitute As T, ByVal x As Quantity) As T + Return Nothing + End Function + + + Function DidNotReceive(Of T)(ByVal substitute As T, ByVal x As Quantity) As T + Return Nothing + End Function + + + Function DidNotReceiveWithAnyArgs(Of T)(ByVal substitute As T, ByVal x As Quantity) As T + Return Nothing + End Function + End Module + + Public Class FooTests + Public Sub Test() + Dim substitute As Foo = Nothing + substitute.{method}.Bar() + End Sub + End Class +End Namespace +"; + await VerifyNoDiagnostic(source); + } + + private static string GetPlainMethodName(string methodName) + { + return methodName.Replace("", string.Empty) + .Replace("Quantity.None()", string.Empty) + .Replace("()", string.Empty); + } + + private async Task VerifyDiagnostic(string source, string methodName) + { + var plainMethodName = GetPlainMethodName(methodName); + + await VerifyDiagnostic(source, Descriptor, $"{plainMethodName} method used in Received.InOrder block."); + } + } +} \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.VisualBasic/DiagnosticAnalyzersTests/ReceivedInReceivedInOrderAnalyzerTests/ReceivedAsOrdinaryMethodTests.cs b/tests/NSubstitute.Analyzers.Tests.VisualBasic/DiagnosticAnalyzersTests/ReceivedInReceivedInOrderAnalyzerTests/ReceivedAsOrdinaryMethodTests.cs new file mode 100644 index 00000000..c99ad35d --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.VisualBasic/DiagnosticAnalyzersTests/ReceivedInReceivedInOrderAnalyzerTests/ReceivedAsOrdinaryMethodTests.cs @@ -0,0 +1,229 @@ +using System.Threading.Tasks; +using NSubstitute.Analyzers.Tests.Shared.Extensibility; + +namespace NSubstitute.Analyzers.Tests.VisualBasic.DiagnosticAnalyzersTests.ReceivedInReceivedInOrderAnalyzerTests +{ + [CombinatoryData( + "ReceivedExtensions.Received(substitute, Quantity.None())", + "ReceivedExtensions.Received(Of IFoo)(substitute, Quantity.None())", + "SubstituteExtensions.Received(substitute)", + "SubstituteExtensions.Received(Of IFoo)(substitute)", + "ReceivedExtensions.ReceivedWithAnyArgs(substitute, Quantity.None())", + "ReceivedExtensions.ReceivedWithAnyArgs(Of IFoo)(substitute, Quantity.None())", + "SubstituteExtensions.ReceivedWithAnyArgs(substitute)", + "SubstituteExtensions.ReceivedWithAnyArgs(Of IFoo)(substitute)", + "SubstituteExtensions.DidNotReceive(substitute)", + "SubstituteExtensions.DidNotReceive(Of IFoo)(substitute)", + "SubstituteExtensions.DidNotReceiveWithAnyArgs(substitute)", + "SubstituteExtensions.DidNotReceiveWithAnyArgs(Of IFoo)(substitute)")] + public class ReceivedAsOrdinaryMethodTests : ReceivedInReceivedInOrderDiagnosticVerifier + { + public override async Task ReportsDiagnostic_WhenUsingReceivedLikeMethodInReceivedInOrderBlock_ForMethod(string method) + { + var source = $@"Imports NSubstitute +Imports NSubstitute.ReceivedExtensions + +Namespace MyNamespace + Public Interface IFoo + Function Bar() As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of IFoo)() + NSubstitute.Received.InOrder(Function() + [|{method}|].Bar() + End Function) + End Sub + End Class +End Namespace +"; + + await VerifyDiagnostic(source, method); + } + + public override async Task ReportsDiagnostic_WhenUsingReceivedLikeMethodInReceivedInOrderBlock_ForProperty(string method) + { + var source = $@"Imports NSubstitute +Imports NSubstitute.ReceivedExtensions + +Namespace MyNamespace + Public Interface IFoo + ReadOnly Property Bar As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of IFoo)() + NSubstitute.Received.InOrder(Function() + Dim x = [|{method}|].Bar + End Function) + End Sub + End Class +End Namespace +"; + + await VerifyDiagnostic(source, method); + } + + public override async Task ReportsDiagnostic_WhenUsingReceivedLikeMethodInReceivedInOrderBlock_ForIndexer(string method) + { + var source = $@"Imports NSubstitute +Imports NSubstitute.ReceivedExtensions + +Namespace MyNamespace + Public Interface IFoo + ReadOnly Property Bar As Integer + End Interface + + Public Class FooTests + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of IFoo)() + NSubstitute.Received.InOrder(Function() + Dim x = [|{method}|].Bar + End Function) + End Sub + End Class +End Namespace +"; + + await VerifyDiagnostic(source, method); + } + + public override async Task ReportsNoDiagnostic_WhenUsingReceivedLikeMethodOutsideOfReceivedInOrderBlock(string method) + { + var source = $@"Imports NSubstitute +Imports NSubstitute.ReceivedExtensions + +Namespace MyNamespace + Public Interface IFoo + Function Bar() As Integer + ReadOnly Property Foo 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 IFoo)() + {method}.Bar() + Dim x = {method}(0) + Dim y = {method}.Foo + NSubstitute.Received.InOrder(Function() + Dim a = substitute(0) + Dim b = substitute.Foo + substitute.Bar() + End Function) + End Sub + End Class +End Namespace +"; + + await VerifyNoDiagnostic(source); + } + + [CombinatoryData( + "ReceivedExtensions.Received(substitute, Quantity.None())", + "ReceivedExtensions.Received(Of Foo)(substitute, Quantity.None())", + "SubstituteExtensions.Received(substitute, 1, 1)", + "SubstituteExtensions.Received(Of Foo)(substitute, 1, 1)", + "ReceivedExtensions.ReceivedWithAnyArgs(substitute, Quantity.None())", + "ReceivedExtensions.ReceivedWithAnyArgs(Of Foo)(substitute, Quantity.None())", + "SubstituteExtensions.ReceivedWithAnyArgs(substitute, 1, 1)", + "SubstituteExtensions.ReceivedWithAnyArgs(Of Foo)(substitute, 1, 1)", + "SubstituteExtensions.DidNotReceive(substitute, 1, 1)", + "SubstituteExtensions.DidNotReceive(Of Foo)(substitute, 1, 1)", + "SubstituteExtensions.DidNotReceiveWithAnyArgs(substitute, 1, 1)", + "SubstituteExtensions.DidNotReceiveWithAnyArgs(Of Foo)(substitute, 1, 1)")] + public override async Task ReportsNoDiagnostics_WhenUsingUnfortunatelyNamedMethod(string method) + { + var source = $@"Imports System +Imports System.Runtime.CompilerServices + +Namespace NSubstitute + Public Class Quantity + Public Shared Function None() As Quantity + Return Nothing + End Function + End Class + + Public Class Foo + Public Function Bar() As Integer + Return 1 + End Function + End Class + + Module SubstituteExtensions + + Function Received(Of T)(ByVal substitute As T, ByVal x As Integer, ByVal y As Integer) As T + Return Nothing + End Function + + + Function ReceivedWithAnyArgs(Of T)(ByVal substitute As T, ByVal x As Integer, ByVal y As Integer) As T + Return Nothing + End Function + + + Function DidNotReceive(Of T)(ByVal substitute As T, ByVal x As Integer, ByVal y As Integer) As T + Return Nothing + End Function + + + Function DidNotReceiveWithAnyArgs(Of T)(ByVal substitute As T, ByVal x As Integer, ByVal y As Integer) As T + Return Nothing + End Function + End Module + + Module ReceivedExtensions + + Function Received(Of T)(ByVal substitute As T, ByVal x As Quantity) As T + Return Nothing + End Function + + + Function ReceivedWithAnyArgs(Of T)(ByVal substitute As T, ByVal x As Quantity) As T + Return Nothing + End Function + + + Function DidNotReceive(Of T)(ByVal substitute As T, ByVal x As Quantity) As T + Return Nothing + End Function + + + Function DidNotReceiveWithAnyArgs(Of T)(ByVal substitute As T, ByVal x As Quantity) As T + Return Nothing + End Function + End Module + + Public Class FooTests + Public Sub Test() + Dim substitute As Foo = Nothing + {method}.Bar() + End Sub + End Class +End Namespace +"; + await VerifyNoDiagnostic(source); + } + + private static string GetPlainMethodName(string methodName) + { + var plainMethodName = methodName.Replace("(Of IFoo)", string.Empty) + .Replace("(substitute, Quantity.None())", string.Empty) + .Replace("(substitute)", string.Empty); + + var planMethodNameWithoutNamespace = plainMethodName.Replace("SubstituteExtensions.", string.Empty) + .Replace("ReceivedExtensions.", string.Empty); + + return planMethodNameWithoutNamespace; + } + + private async Task VerifyDiagnostic(string source, string methodName) + { + var plainMethodName = GetPlainMethodName(methodName); + + await VerifyDiagnostic(source, Descriptor, $"{plainMethodName} method used in Received.InOrder block."); + } + } +} diff --git a/tests/NSubstitute.Analyzers.Tests.VisualBasic/DiagnosticAnalyzersTests/ReceivedInReceivedInOrderAnalyzerTests/ReceivedInReceivedInOrderDiagnosticVerifier.cs b/tests/NSubstitute.Analyzers.Tests.VisualBasic/DiagnosticAnalyzersTests/ReceivedInReceivedInOrderAnalyzerTests/ReceivedInReceivedInOrderDiagnosticVerifier.cs new file mode 100644 index 00000000..233d1e02 --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.VisualBasic/DiagnosticAnalyzersTests/ReceivedInReceivedInOrderAnalyzerTests/ReceivedInReceivedInOrderDiagnosticVerifier.cs @@ -0,0 +1,42 @@ +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using NSubstitute.Analyzers.Shared; +using NSubstitute.Analyzers.Tests.Shared.DiagnosticAnalyzers; +using NSubstitute.Analyzers.Tests.Shared.Extensibility; +using NSubstitute.Analyzers.VisualBasic; +using NSubstitute.Analyzers.VisualBasic.DiagnosticAnalyzers; +using Xunit; + +namespace NSubstitute.Analyzers.Tests.VisualBasic.DiagnosticAnalyzersTests.ReceivedInReceivedInOrderAnalyzerTests +{ + public abstract class ReceivedInReceivedInOrderDiagnosticVerifier : VisualBasicDiagnosticVerifier, IReceivedInReceivedInOrderDiagnosticVerifier + { + protected DiagnosticDescriptor Descriptor { get; } = DiagnosticDescriptors.ReceivedUsedInReceivedInOrder; + + [CombinatoryTheory] + [InlineData] + public abstract Task ReportsDiagnostic_WhenUsingReceivedLikeMethodInReceivedInOrderBlock_ForMethod(string method); + + [CombinatoryTheory] + [InlineData] + public abstract Task ReportsDiagnostic_WhenUsingReceivedLikeMethodInReceivedInOrderBlock_ForProperty(string method); + + [CombinatoryTheory] + [InlineData] + public abstract Task ReportsDiagnostic_WhenUsingReceivedLikeMethodInReceivedInOrderBlock_ForIndexer(string method); + + [CombinatoryTheory] + [InlineData] + public abstract Task ReportsNoDiagnostic_WhenUsingReceivedLikeMethodOutsideOfReceivedInOrderBlock(string method); + + [CombinatoryTheory] + [InlineData] + public abstract Task ReportsNoDiagnostics_WhenUsingUnfortunatelyNamedMethod(string method); + + protected override DiagnosticAnalyzer GetDiagnosticAnalyzer() + { + return new ReceivedInReceivedInOrderAnalyzer(); + } + } +} \ No newline at end of file