diff --git a/NSubstitute.Analyzers.sln b/NSubstitute.Analyzers.sln index d4c0b1bf..f0a59b68 100644 --- a/NSubstitute.Analyzers.sln +++ b/NSubstitute.Analyzers.sln @@ -25,7 +25,8 @@ ProjectSection(SolutionItems) = preProject documentation\rules\NS1001.md = documentation\rules\NS1001.md documentation\rules\NS1002.md = documentation\rules\NS1002.md documentation\rules\NS1003.md = documentation\rules\NS1003.md - documentation\rules\NS1003.md = documentation\rules\NS1004.md + documentation\rules\NS1004.md = documentation\rules\NS1004.md + documentation\rules\NS1005.md = documentation\rules\NS1005.md documentation\rules\NS2000.md = documentation\rules\NS2000.md documentation\rules\NS2001.md = documentation\rules\NS2001.md documentation\rules\NS2002.md = documentation\rules\NS2002.md diff --git a/benchmarks/NSubstitute.Analyzers.Benchmarks.Source.CSharp/DiagnosticsSources/NonSubstitutableMemberReceivedInOrderDiagnosticSource.cs b/benchmarks/NSubstitute.Analyzers.Benchmarks.Source.CSharp/DiagnosticsSources/NonSubstitutableMemberReceivedInOrderDiagnosticSource.cs new file mode 100644 index 00000000..9fe8f702 --- /dev/null +++ b/benchmarks/NSubstitute.Analyzers.Benchmarks.Source.CSharp/DiagnosticsSources/NonSubstitutableMemberReceivedInOrderDiagnosticSource.cs @@ -0,0 +1,40 @@ +using NSubstitute.Analyzers.Benchmarks.Source.CSharp.Models; + +namespace NSubstitute.Analyzers.Benchmarks.Source.CSharp.DiagnosticsSources +{ + public class NonSubstitutableMemberReceivedInOrderDiagnosticSource + { + public void NS1005_NonVirtualSetupSpecification() + { + var substitute = Substitute.For(); + + Received.InOrder(() => + { + substitute.ObjectReturningMethod(); + var methodItem = substitute.ObjectReturningMethod(); + substitute.ObjectReturningMethodWithArguments(substitute.IntReturningProperty, 1, 1m); + _ = substitute.IntReturningProperty; + var propertyItem = substitute.IntReturningProperty; + _ = substitute[0]; + var indexerItem = substitute[0]; + var otherIndexerItem = substitute[0]; + var usedIndexerItem = otherIndexerItem; + }); + } + + public void NS1003_InternalVirtualSetupSpecification() + { + var substitute = Substitute.For(); + + substitute.InternalObjectReturningMethod(); + var methodItem = substitute.InternalObjectReturningMethod(); + substitute.InternalObjectReturningMethodWithArguments(substitute.IntReturningProperty); + _ = substitute.InternalObjectReturningProperty; + var propertyItem = substitute.InternalObjectReturningProperty; + _ = substitute[0]; + var indexerItem = substitute[0, 0, 0]; + var otherIndexerItem = substitute[0, 0, 0]; + var usedIndexerItem = otherIndexerItem; + } + } +} \ No newline at end of file diff --git a/benchmarks/NSubstitute.Analyzers.Benchmarks/CSharp/CSharpDiagnosticAnalyzersBenchmarks.cs b/benchmarks/NSubstitute.Analyzers.Benchmarks/CSharp/CSharpDiagnosticAnalyzersBenchmarks.cs index d12877db..29594c71 100644 --- a/benchmarks/NSubstitute.Analyzers.Benchmarks/CSharp/CSharpDiagnosticAnalyzersBenchmarks.cs +++ b/benchmarks/NSubstitute.Analyzers.Benchmarks/CSharp/CSharpDiagnosticAnalyzersBenchmarks.cs @@ -17,6 +17,8 @@ public class CSharpDiagnosticAnalyzersBenchmarks : AbstractDiagnosticAnalyzersBe protected override AnalyzerBenchmark NonSubstitutableMemberWhenAnalyzerBenchmark { get; } + protected override AnalyzerBenchmark NonSubstitutableMemberReceivedInOrderAnalyzerBenchmark { get; } + protected override AnalyzerBenchmark ReEntrantSetupAnalyzerBenchmark { get; } protected override AnalyzerBenchmark SubstituteAnalyzerBenchmark { get; } @@ -46,6 +48,7 @@ public CSharpDiagnosticAnalyzersBenchmarks() NonSubstitutableMemberAnalyzerBenchmark = AnalyzerBenchmark.CreateBenchmark(Solution, new NonSubstitutableMemberAnalyzer()); NonSubstitutableMemberReceivedAnalyzerBenchmark = AnalyzerBenchmark.CreateBenchmark(Solution, new NonSubstitutableMemberReceivedAnalyzer()); NonSubstitutableMemberWhenAnalyzerBenchmark = AnalyzerBenchmark.CreateBenchmark(Solution, new NonSubstitutableMemberWhenAnalyzer()); + NonSubstitutableMemberReceivedInOrderAnalyzerBenchmark = AnalyzerBenchmark.CreateBenchmark(Solution, new NonSubstitutableMemberReceivedInOrderAnalyzer()); ReEntrantSetupAnalyzerBenchmark = AnalyzerBenchmark.CreateBenchmark(Solution, new ReEntrantSetupAnalyzer()); SubstituteAnalyzerBenchmark = AnalyzerBenchmark.CreateBenchmark(Solution, new SubstituteAnalyzer()); UnusedReceivedAnalyzerBenchmark = AnalyzerBenchmark.CreateBenchmark(Solution, new UnusedReceivedAnalyzer()); diff --git a/benchmarks/NSubstitute.Analyzers.Benchmarks/Shared/AbstractDiagnosticAnalyzersBenchmarks.cs b/benchmarks/NSubstitute.Analyzers.Benchmarks/Shared/AbstractDiagnosticAnalyzersBenchmarks.cs index 530413b5..74eb3220 100644 --- a/benchmarks/NSubstitute.Analyzers.Benchmarks/Shared/AbstractDiagnosticAnalyzersBenchmarks.cs +++ b/benchmarks/NSubstitute.Analyzers.Benchmarks/Shared/AbstractDiagnosticAnalyzersBenchmarks.cs @@ -24,6 +24,8 @@ public abstract class AbstractDiagnosticAnalyzersBenchmarks protected abstract AnalyzerBenchmark NonSubstitutableMemberWhenAnalyzerBenchmark { get; } + protected abstract AnalyzerBenchmark NonSubstitutableMemberReceivedInOrderAnalyzerBenchmark { get; } + protected abstract AnalyzerBenchmark ReEntrantSetupAnalyzerBenchmark { get; } protected abstract AnalyzerBenchmark SubstituteAnalyzerBenchmark { get; } @@ -79,6 +81,12 @@ public void NonSubstitutableMemberWhenAnalyzer() NonSubstitutableMemberWhenAnalyzerBenchmark.Run(); } + [Benchmark] + public void NonSubstitutableMemberReceivedInOrderAnalyzer() + { + NonSubstitutableMemberReceivedInOrderAnalyzerBenchmark.Run(); + } + [Benchmark] public void ReEntrantSetupAnalyzer() { diff --git a/benchmarks/NSubstitute.Analyzers.Benchmarks/VisualBasic/VisualBasicDiagnosticAnalyzersBenchmarks.cs b/benchmarks/NSubstitute.Analyzers.Benchmarks/VisualBasic/VisualBasicDiagnosticAnalyzersBenchmarks.cs index 54f78794..2751e427 100644 --- a/benchmarks/NSubstitute.Analyzers.Benchmarks/VisualBasic/VisualBasicDiagnosticAnalyzersBenchmarks.cs +++ b/benchmarks/NSubstitute.Analyzers.Benchmarks/VisualBasic/VisualBasicDiagnosticAnalyzersBenchmarks.cs @@ -17,6 +17,8 @@ public class VisualBasicDiagnosticAnalyzersBenchmarks : AbstractDiagnosticAnalyz protected override AnalyzerBenchmark NonSubstitutableMemberWhenAnalyzerBenchmark { get; } + protected override AnalyzerBenchmark NonSubstitutableMemberReceivedInOrderAnalyzerBenchmark { get; } + protected override AnalyzerBenchmark ReEntrantSetupAnalyzerBenchmark { get; } protected override AnalyzerBenchmark SubstituteAnalyzerBenchmark { get; } @@ -46,6 +48,7 @@ public VisualBasicDiagnosticAnalyzersBenchmarks() NonSubstitutableMemberAnalyzerBenchmark = AnalyzerBenchmark.CreateBenchmark(Solution, new NonSubstitutableMemberAnalyzer()); NonSubstitutableMemberReceivedAnalyzerBenchmark = AnalyzerBenchmark.CreateBenchmark(Solution, new NonSubstitutableMemberReceivedAnalyzer()); NonSubstitutableMemberWhenAnalyzerBenchmark = AnalyzerBenchmark.CreateBenchmark(Solution, new NonSubstitutableMemberWhenAnalyzer()); + NonSubstitutableMemberReceivedInOrderAnalyzerBenchmark = AnalyzerBenchmark.CreateBenchmark(Solution, new NonSubstitutableMemberReceivedInOrderAnalyzer()); ReEntrantSetupAnalyzerBenchmark = AnalyzerBenchmark.CreateBenchmark(Solution, new ReEntrantSetupAnalyzer()); SubstituteAnalyzerBenchmark = AnalyzerBenchmark.CreateBenchmark(Solution, new SubstituteAnalyzer()); UnusedReceivedAnalyzerBenchmark = AnalyzerBenchmark.CreateBenchmark(Solution, new UnusedReceivedAnalyzer()); diff --git a/documentation/rules/NS1005.md b/documentation/rules/NS1005.md new file mode 100644 index 00000000..85ca0c2f --- /dev/null +++ b/documentation/rules/NS1005.md @@ -0,0 +1,39 @@ +# NS1005 + + + + + + + + + + +
CheckIdNS1005
CategoryNon-substitutable member
+ +## Cause + +Checking call order for non-virtual member of a class. + +## Rule description + +A violation of this rule occurs when `Received.InOrder` checks call order for non-virtual member of a class. + +## How to fix violations + +To fix a violation of this rule, make the member of your class virtual or substitute for interface. + +## How to suppress violations + +This warning can be suppressed by disabling the warning in the **ruleset** file for the project or by suppressing it (for selected members) in `nsubstitute.json` file. See the [configuration](../Configuration.md) section for details on how to set this up. +The warning can also be suppressed programmatically for an assembly: +````c# +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Non-substitutable member", "NS1005:Non-virtual setup specification.", Justification = "Reviewed")] +```` + +Or for a specific code block: +````c# +#pragma warning disable NS1005 // Non-virtual setup specification. +// the code which produces warning +#pragma warning restore NS1005 // Non-virtual setup specification. +```` diff --git a/documentation/rules/README.md b/documentation/rules/README.md index 3c96186f..b30638fa 100644 --- a/documentation/rules/README.md +++ b/documentation/rules/README.md @@ -8,6 +8,7 @@ | [NS1002](NS1002.md) | Non-substitutable member | Substituting for non-virtual member of a class. | | [NS1003](NS1003.md) | Non-substitutable member | Substituting for an internal member of a class without proxies having visibility into internal members. | | [NS1004](NS1004.md) | Non-substitutable member | Argument matcher used with a non-virtual member of a class. | +| [NS1005](NS1005.md) | Non-substitutable member | Checking call order for non-virtual member of a class. | | [NS2000](NS2000.md) | Substitute creation | Substitute.ForPartsOf used with interface or delegate. | | [NS2001](NS2001.md) | Substitute creation | NSubstitute used with class which does not expose public or protected constructor. | | [NS2002](NS2002.md) | Substitute creation | NSubstitute used with class which does not expose parameterless constructor. | @@ -26,4 +27,4 @@ | [NS4000](NS4000.md) | Call configuration | Calling substitute from within `Returns` block. | | [NS5000](NS5000.md) | Usage | Checking received calls without specifying member. | | [NS5001](NS5001.md) | Usage | Usage of received-like method in `Received.InOrder` callback. | -| [NS5002](NS5002.md) | Usage | Usage of async callback in `Received.InOrder` method. | +| [NS5002](NS5002.md) | Usage | Usage of async callback in `Received.InOrder` method. | \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/NonSubstitutableMemberAnalysis.cs b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/NonSubstitutableMemberAnalysis.cs new file mode 100644 index 00000000..1533df62 --- /dev/null +++ b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/NonSubstitutableMemberAnalysis.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using NSubstitute.Analyzers.Shared.DiagnosticAnalyzers; + +namespace NSubstitute.Analyzers.CSharp.DiagnosticAnalyzers +{ + internal class NonSubstitutableMemberAnalysis : AbstractNonSubstitutableMemberAnalysis + { + public static NonSubstitutableMemberAnalysis Instance { get; } = new NonSubstitutableMemberAnalysis(); + + private NonSubstitutableMemberAnalysis() + { + } + + protected override ImmutableHashSet KnownNonVirtualSyntaxKinds { get; } = ImmutableHashSet.Create( + typeof(LiteralExpressionSyntax)); + } +} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/NonSubstitutableMemberAnalyzer.cs b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/NonSubstitutableMemberAnalyzer.cs index fa91c670..d343a464 100644 --- a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/NonSubstitutableMemberAnalyzer.cs +++ b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/NonSubstitutableMemberAnalyzer.cs @@ -1,12 +1,8 @@ -using System; -using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -using NSubstitute.Analyzers.CSharp.Extensions; using NSubstitute.Analyzers.Shared.DiagnosticAnalyzers; namespace NSubstitute.Analyzers.CSharp.DiagnosticAnalyzers @@ -16,9 +12,6 @@ internal sealed class NonSubstitutableMemberAnalyzer : AbstractNonSubstitutableM { protected override SyntaxKind InvocationExpressionKind { get; } = SyntaxKind.InvocationExpression; - protected override ImmutableHashSet KnownNonVirtualSyntaxKinds { get; } = ImmutableHashSet.Create( - typeof(LiteralExpressionSyntax)); - protected override ImmutableHashSet SupportedMemberAccesses { get; } = ImmutableHashSet.Create( (int)SyntaxKind.InvocationExpression, (int)SyntaxKind.SimpleMemberAccessExpression, @@ -30,7 +23,7 @@ internal sealed class NonSubstitutableMemberAnalyzer : AbstractNonSubstitutableM (int)SyntaxKind.StringLiteralExpression); public NonSubstitutableMemberAnalyzer() - : base(NSubstitute.Analyzers.CSharp.DiagnosticDescriptorsProvider.Instance, SubstitutionNodeFinder.Instance) + : base(NSubstitute.Analyzers.CSharp.DiagnosticDescriptorsProvider.Instance, SubstitutionNodeFinder.Instance, NonSubstitutableMemberAnalysis.Instance) { } } diff --git a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/NonSubstitutableMemberArgumentMatcherAnalyzer.cs b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/NonSubstitutableMemberArgumentMatcherAnalyzer.cs index 6caf2076..bc2a2f9b 100644 --- a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/NonSubstitutableMemberArgumentMatcherAnalyzer.cs +++ b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/NonSubstitutableMemberArgumentMatcherAnalyzer.cs @@ -54,7 +54,7 @@ internal sealed class NonSubstitutableMemberArgumentMatcherAnalyzer : AbstractNo (int)SyntaxKind.VariableDeclarator)); public NonSubstitutableMemberArgumentMatcherAnalyzer() - : base(NSubstitute.Analyzers.CSharp.DiagnosticDescriptorsProvider.Instance) + : base(NonSubstitutableMemberAnalysis.Instance, NSubstitute.Analyzers.CSharp.DiagnosticDescriptorsProvider.Instance) { } diff --git a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/NonSubstitutableMemberReceivedAnalyzer.cs b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/NonSubstitutableMemberReceivedAnalyzer.cs index 3d200c66..e1fd7ddf 100644 --- a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/NonSubstitutableMemberReceivedAnalyzer.cs +++ b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/NonSubstitutableMemberReceivedAnalyzer.cs @@ -19,7 +19,7 @@ internal sealed class NonSubstitutableMemberReceivedAnalyzer : AbstractNonSubsti protected override SyntaxKind InvocationExpressionKind { get; } = SyntaxKind.InvocationExpression; public NonSubstitutableMemberReceivedAnalyzer() - : base(NSubstitute.Analyzers.CSharp.DiagnosticDescriptorsProvider.Instance) + : base(NSubstitute.Analyzers.CSharp.DiagnosticDescriptorsProvider.Instance, NonSubstitutableMemberAnalysis.Instance) { } } diff --git a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/NonSubstitutableMemberReceivedInOrderAnalyzer.cs b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/NonSubstitutableMemberReceivedInOrderAnalyzer.cs new file mode 100644 index 00000000..abf7d106 --- /dev/null +++ b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/NonSubstitutableMemberReceivedInOrderAnalyzer.cs @@ -0,0 +1,25 @@ +using System.Collections.Immutable; +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 NonSubstitutableMemberReceivedInOrderAnalyzer : AbstractNonSubstitutableMemberReceivedInOrderAnalyzer + { + private static ImmutableArray IgnoredPaths { get; } = + ImmutableArray.Create((int)SyntaxKind.Argument, (int)SyntaxKind.VariableDeclarator, (int)SyntaxKind.AddAssignmentExpression); + + public NonSubstitutableMemberReceivedInOrderAnalyzer() + : base(SubstitutionNodeFinder.Instance, NonSubstitutableMemberAnalysis.Instance, NSubstitute.Analyzers.CSharp.DiagnosticDescriptorsProvider.Instance) + { + } + + protected override SyntaxKind InvocationExpressionKind { get; } = SyntaxKind.InvocationExpression; + + protected override ImmutableArray IgnoredAncestorPaths { get; } = IgnoredPaths; + } +} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/NonSubstitutableMemberWhenAnalyzer.cs b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/NonSubstitutableMemberWhenAnalyzer.cs index c7ed0db8..8d342810 100644 --- a/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/NonSubstitutableMemberWhenAnalyzer.cs +++ b/src/NSubstitute.Analyzers.CSharp/DiagnosticAnalyzers/NonSubstitutableMemberWhenAnalyzer.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; @@ -13,7 +10,7 @@ namespace NSubstitute.Analyzers.CSharp.DiagnosticAnalyzers internal sealed class NonSubstitutableMemberWhenAnalyzer : AbstractNonSubstitutableMemberWhenAnalyzer { public NonSubstitutableMemberWhenAnalyzer() - : base(NSubstitute.Analyzers.CSharp.DiagnosticDescriptorsProvider.Instance, SubstitutionNodeFinder.Instance) + : base(NSubstitute.Analyzers.CSharp.DiagnosticDescriptorsProvider.Instance, SubstitutionNodeFinder.Instance, NonSubstitutableMemberAnalysis.Instance) { } diff --git a/src/NSubstitute.Analyzers.CSharp/Resources.Designer.cs b/src/NSubstitute.Analyzers.CSharp/Resources.Designer.cs index 363d9e13..6682dd32 100644 --- a/src/NSubstitute.Analyzers.CSharp/Resources.Designer.cs +++ b/src/NSubstitute.Analyzers.CSharp/Resources.Designer.cs @@ -1,7 +1,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -13,46 +12,32 @@ namespace NSubstitute.Analyzers.CSharp { using System.Reflection; - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [System.Diagnostics.DebuggerNonUserCodeAttribute()] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { - private static global::System.Resources.ResourceManager resourceMan; + private static System.Resources.ResourceManager resourceMan; - private static global::System.Globalization.CultureInfo resourceCulture; + private static System.Globalization.CultureInfo resourceCulture; - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Resources() { } - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Resources.ResourceManager ResourceManager { get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("NSubstitute.Analyzers.CSharp.Resources", typeof(Resources).GetTypeInfo().Assembly); + if (object.Equals(null, resourceMan)) { + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("NSubstitute.Analyzers.CSharp.Resources", typeof(Resources).GetTypeInfo().Assembly); resourceMan = temp; } return resourceMan; } } - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -61,57 +46,45 @@ internal Resources() { } } - /// - /// Looks up a localized string similar to Internal member {0} can not be intercepted without InternalsVisibleToAttribute.. - /// - internal static string InternalSetupSpecificationMessageFormat { + internal static string NonVirtualSetupSpecificationMessageFormat { get { - return ResourceManager.GetString("InternalSetupSpecificationMessageFormat", resourceCulture); + return ResourceManager.GetString("NonVirtualSetupSpecificationMessageFormat", resourceCulture); } } - /// - /// Looks up a localized string similar to Member {0} can not be intercepted. Only interface members and virtual, overriding, and abstract members can be intercepted.. - /// - internal static string NonVirtualReceivedSetupSpecificationMessageFormat { + internal static string SubstituteForInternalMemberMessageFormat { get { - return ResourceManager.GetString("NonVirtualReceivedSetupSpecificationMessageFormat", resourceCulture); + return ResourceManager.GetString("SubstituteForInternalMemberMessageFormat", resourceCulture); } } - /// - /// Looks up a localized string similar to Member {0} can not be intercepted. Only interface members and virtual, overriding, and abstract members can be intercepted.. - /// - internal static string NonVirtualSetupSpecificationMessageFormat { + internal static string NonVirtualReceivedSetupSpecificationMessageFormat { get { - return ResourceManager.GetString("NonVirtualSetupSpecificationMessageFormat", resourceCulture); + return ResourceManager.GetString("NonVirtualReceivedSetupSpecificationMessageFormat", resourceCulture); } } - /// - /// Looks up a localized string similar to Member {0} can not be intercepted. Only interface members and virtual, overriding, and abstract members can be intercepted.. - /// internal static string NonVirtualWhenSetupSpecificationMessageFormat { get { return ResourceManager.GetString("NonVirtualWhenSetupSpecificationMessageFormat", resourceCulture); } } - /// - /// Looks up a localized string similar to {0}() is set with a method that itself calls {1}. This can cause problems with NSubstitute. Consider replacing with a lambda: {0}(x => {2}).. - /// + internal static string NonVirtualReceivedInOrderSetupSpecificationMessageFormat { + get { + return ResourceManager.GetString("NonVirtualReceivedInOrderSetupSpecificationMessageFormat", resourceCulture); + } + } + internal static string ReEntrantSubstituteCallMessageFormat { get { return ResourceManager.GetString("ReEntrantSubstituteCallMessageFormat", resourceCulture); } } - /// - /// Looks up a localized string similar to Can not substitute for internal type. To substitute for internal type expose your type to DynamicProxyGenAssembly2 via [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]. - /// - internal static string SubstituteForInternalMemberMessageFormat { + internal static string InternalSetupSpecificationMessageFormat { get { - return ResourceManager.GetString("SubstituteForInternalMemberMessageFormat", resourceCulture); + return ResourceManager.GetString("InternalSetupSpecificationMessageFormat", resourceCulture); } } } diff --git a/src/NSubstitute.Analyzers.CSharp/Resources.resx b/src/NSubstitute.Analyzers.CSharp/Resources.resx index 4723c646..421ed1a9 100644 --- a/src/NSubstitute.Analyzers.CSharp/Resources.resx +++ b/src/NSubstitute.Analyzers.CSharp/Resources.resx @@ -133,6 +133,10 @@ Member {0} can not be intercepted. Only interface members and virtual, overriding, and abstract members can be intercepted. The format-able message the diagnostic displays. + + Member {0} can not be intercepted. Only interface members and virtual, overriding, and abstract members can be intercepted. + The format-able message the diagnostic displays. + {0}() is set with a method that itself calls {1}. This can cause problems with NSubstitute. Consider replacing with a lambda: {0}(x => {2}). The format-able message the diagnostic displays. diff --git a/src/NSubstitute.Analyzers.Shared/AbstractDiagnosticDescriptorsProvider.cs b/src/NSubstitute.Analyzers.Shared/AbstractDiagnosticDescriptorsProvider.cs index f4262804..e91746be 100644 --- a/src/NSubstitute.Analyzers.Shared/AbstractDiagnosticDescriptorsProvider.cs +++ b/src/NSubstitute.Analyzers.Shared/AbstractDiagnosticDescriptorsProvider.cs @@ -6,6 +6,8 @@ internal class AbstractDiagnosticDescriptorsProvider : IDiagnosticDescriptors { public DiagnosticDescriptor NonVirtualSetupSpecification { get; } = DiagnosticDescriptors.NonVirtualSetupSpecification; + public DiagnosticDescriptor NonVirtualReceivedInOrderSetupSpecification { get; } = DiagnosticDescriptors.NonVirtualReceivedInOrderSetupSpecification; + public DiagnosticDescriptor InternalSetupSpecification { get; } = DiagnosticDescriptors.InternalSetupSpecification; public DiagnosticDescriptor UnusedReceived { get; } = DiagnosticDescriptors.UnusedReceived; diff --git a/src/NSubstitute.Analyzers.Shared/CodeFixProviders/AbstractNonSubstitutableMemberSuppressDiagnosticsCodeFixProvider.cs b/src/NSubstitute.Analyzers.Shared/CodeFixProviders/AbstractNonSubstitutableMemberSuppressDiagnosticsCodeFixProvider.cs index 800606ab..01b588ab 100644 --- a/src/NSubstitute.Analyzers.Shared/CodeFixProviders/AbstractNonSubstitutableMemberSuppressDiagnosticsCodeFixProvider.cs +++ b/src/NSubstitute.Analyzers.Shared/CodeFixProviders/AbstractNonSubstitutableMemberSuppressDiagnosticsCodeFixProvider.cs @@ -4,6 +4,8 @@ namespace NSubstitute.Analyzers.Shared.CodeFixProviders { internal class AbstractNonSubstitutableMemberSuppressDiagnosticsCodeFixProvider : AbstractSuppressDiagnosticsCodeFixProvider { - public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(DiagnosticIdentifiers.NonVirtualSetupSpecification); + public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create( + DiagnosticIdentifiers.NonVirtualSetupSpecification, + DiagnosticIdentifiers.NonVirtualReceivedInOrderSetupSpecification); } } \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractDiagnosticAnalyzer.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractDiagnosticAnalyzer.cs index 9a46081a..71e10f78 100644 --- a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractDiagnosticAnalyzer.cs +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractDiagnosticAnalyzer.cs @@ -24,62 +24,5 @@ public sealed override void Initialize(AnalysisContext context) } protected abstract void InitializeAnalyzer(AnalysisContext context); - - protected void TryReportDiagnostic(SyntaxNodeAnalysisContext syntaxNodeContext, Diagnostic diagnostic, ISymbol symbol) - { - if (IsSuppressed(syntaxNodeContext, diagnostic, symbol)) - { - return; - } - - syntaxNodeContext.ReportDiagnostic(diagnostic); - } - - protected bool IsSuppressed(SyntaxNodeAnalysisContext syntaxNodeContext, Diagnostic diagnostic, ISymbol symbol) - { - return symbol != null && IsSuppressed(syntaxNodeContext, symbol, diagnostic.Id); - } - - private bool IsSuppressed(SyntaxNodeAnalysisContext syntaxNodeContext, ISymbol symbol, string diagnosticId) - { - var analyzersSettings = syntaxNodeContext.GetSettings(CancellationToken.None); - - if (analyzersSettings.Suppressions.Count == 0) - { - return false; - } - - var possibleSymbols = GetPossibleSymbols(symbol).ToImmutableHashSet(); - - return analyzersSettings.Suppressions.Where(suppression => suppression.Rules.Contains(diagnosticId)) - .SelectMany(suppression => DocumentationCommentId.GetSymbolsForDeclarationId(suppression.Target, syntaxNodeContext.Compilation)) - .Any(possibleSymbols.Contains); - } - - private IEnumerable GetPossibleSymbols(ISymbol symbol) - { - yield return symbol; - yield return symbol.ContainingType; - yield return symbol.ContainingNamespace; - - if (symbol is IMethodSymbol methodSymbol) - { - yield return methodSymbol.ConstructedFrom; - if (methodSymbol.ReducedFrom != null) - { - yield return methodSymbol.ReducedFrom; - } - } - - if (symbol.ContainingType is INamedTypeSymbol namedTypeSymbol) - { - yield return namedTypeSymbol.ConstructedFrom; - } - - if (symbol is IPropertySymbol propertySymbol) - { - yield return propertySymbol.OriginalDefinition; - } - } } } \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableMemberAnalysis.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableMemberAnalysis.cs new file mode 100644 index 00000000..1ec6ba04 --- /dev/null +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableMemberAnalysis.cs @@ -0,0 +1,88 @@ +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 AbstractNonSubstitutableMemberAnalysis : INonSubstitutableMemberAnalysis + { + protected abstract ImmutableHashSet KnownNonVirtualSyntaxKinds { get; } + + public NonSubstitutableMemberAnalysisResult Analyze( + in SyntaxNodeAnalysisContext syntaxNodeContext, + SyntaxNode accessedMember, + ISymbol symbol = null) + { + var accessedSymbol = symbol ?? syntaxNodeContext.SemanticModel.GetSymbolInfo(accessedMember).Symbol; + + if (accessedSymbol == null) + { + return new NonSubstitutableMemberAnalysisResult( + nonVirtualMemberSubstitution: KnownNonVirtualSyntaxKinds.Contains(accessedMember.GetType()), + internalMemberSubstitution: false, + symbol: null, + member: accessedMember, + memberName: accessedMember.ToString()); + } + + var canBeSubstituted = CanBeSubstituted(syntaxNodeContext, accessedMember, accessedSymbol); + + if (canBeSubstituted == false) + { + return new NonSubstitutableMemberAnalysisResult( + nonVirtualMemberSubstitution: true, + internalMemberSubstitution: false, + symbol: accessedSymbol, + member: accessedMember, + memberName: accessedSymbol.Name); + } + + if (accessedSymbol.MemberVisibleToProxyGenerator() == false) + { + return new NonSubstitutableMemberAnalysisResult( + nonVirtualMemberSubstitution: false, + internalMemberSubstitution: true, + symbol: accessedSymbol, + member: accessedMember, + memberName: accessedSymbol.Name); + } + + return new NonSubstitutableMemberAnalysisResult( + nonVirtualMemberSubstitution: false, + internalMemberSubstitution: false, + symbol: accessedSymbol, + member: accessedMember, + memberName: accessedSymbol.Name); + } + + protected virtual bool CanBeSubstituted( + SyntaxNodeAnalysisContext syntaxNodeContext, + SyntaxNode accessedMember, + ISymbol symbol) + { + return !KnownNonVirtualSyntaxKinds.Contains(accessedMember.GetType()) && + CanBeSubstituted(symbol); + } + + private static bool CanBeSubstituted(ISymbol symbol) + { + return IsInterfaceMember(symbol) || IsVirtual(symbol); + } + + private static bool IsInterfaceMember(ISymbol symbol) + { + return symbol.ContainingType?.TypeKind == TypeKind.Interface; + } + + private static bool IsVirtual(ISymbol symbol) + { + var isVirtual = symbol.IsVirtual + || (symbol.IsOverride && !symbol.IsSealed) + || symbol.IsAbstract; + + return isVirtual; + } + } +} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableMemberAnalyzer.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableMemberAnalyzer.cs index efc8e65e..8591ea78 100644 --- a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableMemberAnalyzer.cs +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableMemberAnalyzer.cs @@ -1,14 +1,12 @@ using System; -using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using NSubstitute.Analyzers.Shared.Extensions; namespace NSubstitute.Analyzers.Shared.DiagnosticAnalyzers { - internal abstract class AbstractNonSubstitutableMemberAnalyzer : AbstractDiagnosticAnalyzer + internal abstract class AbstractNonSubstitutableMemberAnalyzer : AbstractNonSubstitutableSetupAnalyzer where TInvocationExpressionSyntax : SyntaxNode where TSyntaxKind : struct { @@ -20,35 +18,27 @@ internal abstract class AbstractNonSubstitutableMemberAnalyzer SupportedMemberAccesses { get; } - protected abstract ImmutableHashSet KnownNonVirtualSyntaxKinds { get; } - protected abstract TSyntaxKind InvocationExpressionKind { get; } protected AbstractNonSubstitutableMemberAnalyzer( IDiagnosticDescriptorsProvider diagnosticDescriptorsProvider, - ISubstitutionNodeFinder substitutionNodeFinder) - : base(diagnosticDescriptorsProvider) + ISubstitutionNodeFinder substitutionNodeFinder, + INonSubstitutableMemberAnalysis nonSubstitutableMemberAnalysis) + : base(diagnosticDescriptorsProvider, nonSubstitutableMemberAnalysis) { _analyzeInvocationAction = AnalyzeInvocation; _substitutionNodeFinder = substitutionNodeFinder; SupportedDiagnostics = ImmutableArray.Create(DiagnosticDescriptorsProvider.NonVirtualSetupSpecification, DiagnosticDescriptorsProvider.InternalSetupSpecification); + NonVirtualSetupDescriptor = diagnosticDescriptorsProvider.NonVirtualSetupSpecification; } + protected override DiagnosticDescriptor NonVirtualSetupDescriptor { get; } + protected override void InitializeAnalyzer(AnalysisContext context) { context.RegisterSyntaxNodeAction(_analyzeInvocationAction, InvocationExpressionKind); } - protected virtual bool? CanBeSetuped(SyntaxNodeAnalysisContext syntaxNodeContext, SyntaxNode accessedMember, SymbolInfo symbolInfo) - { - if (KnownNonVirtualSyntaxKinds.Contains(accessedMember.GetType())) - { - return false; - } - - return symbolInfo.Symbol?.CanBeSetuped(); - } - private void AnalyzeInvocation(SyntaxNodeAnalysisContext syntaxNodeContext) { var invocationExpression = syntaxNodeContext.Node; @@ -76,28 +66,7 @@ private void AnalyzeMember(SyntaxNodeAnalysisContext syntaxNodeContext, SyntaxNo return; } - var accessedSymbol = syntaxNodeContext.SemanticModel.GetSymbolInfo(accessedMember); - - var canBeSetuped = CanBeSetuped(syntaxNodeContext, accessedMember, accessedSymbol); - if (canBeSetuped.HasValue && canBeSetuped == false) - { - var diagnostic = Diagnostic.Create( - DiagnosticDescriptorsProvider.NonVirtualSetupSpecification, - accessedMember.GetLocation(), - accessedSymbol.Symbol?.Name ?? accessedMember.ToString()); - - TryReportDiagnostic(syntaxNodeContext, diagnostic, accessedSymbol.Symbol); - } - - if (accessedSymbol.Symbol != null && canBeSetuped.HasValue && canBeSetuped == true && accessedSymbol.Symbol.MemberVisibleToProxyGenerator() == false) - { - var diagnostic = Diagnostic.Create( - DiagnosticDescriptorsProvider.InternalSetupSpecification, - accessedMember.GetLocation(), - accessedSymbol.Symbol.Name); - - syntaxNodeContext.ReportDiagnostic(diagnostic); - } + Analyze(syntaxNodeContext, accessedMember); } private bool IsValidForAnalysis(SyntaxNode accessedMember) diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableMemberArgumentMatcherAnalyzer.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableMemberArgumentMatcherAnalyzer.cs index c9387c87..9bdbfced 100644 --- a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableMemberArgumentMatcherAnalyzer.cs +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableMemberArgumentMatcherAnalyzer.cs @@ -12,15 +12,20 @@ internal abstract class AbstractNonSubstitutableMemberArgumentMatcherAnalyzer _analyzeInvocationAction; + private readonly INonSubstitutableMemberAnalysis _nonSubstitutableMemberAnalysis; + protected abstract ImmutableArray> AllowedAncestorPaths { get; } protected abstract ImmutableArray> IgnoredAncestorPaths { get; } protected abstract TSyntaxKind InvocationExpressionKind { get; } - protected AbstractNonSubstitutableMemberArgumentMatcherAnalyzer(IDiagnosticDescriptorsProvider diagnosticDescriptorsProvider) + protected AbstractNonSubstitutableMemberArgumentMatcherAnalyzer( + INonSubstitutableMemberAnalysis nonSubstitutableMemberAnalysis, + IDiagnosticDescriptorsProvider diagnosticDescriptorsProvider) : base(diagnosticDescriptorsProvider) { + _nonSubstitutableMemberAnalysis = nonSubstitutableMemberAnalysis; _analyzeInvocationAction = AnalyzeInvocation; SupportedDiagnostics = ImmutableArray.Create(DiagnosticDescriptorsProvider.NonSubstitutableMemberArgumentMatcherUsage); } @@ -81,15 +86,15 @@ private void AnalyzeArgLikeMethod(SyntaxNodeAnalysisContext syntaxNodeContext, T return; } - var canBeSetuped = enclosingExpressionSymbol.CanBeSetuped(); + var analysisResult = _nonSubstitutableMemberAnalysis.Analyze(syntaxNodeContext, enclosingExpression); - if (canBeSetuped == false || enclosingExpressionSymbol.MemberVisibleToProxyGenerator() == false) + if (analysisResult.CanBeSubstituted == false) { var diagnostic = Diagnostic.Create( DiagnosticDescriptorsProvider.NonSubstitutableMemberArgumentMatcherUsage, argInvocationExpression.GetLocation()); - TryReportDiagnostic(syntaxNodeContext, diagnostic, enclosingExpressionSymbol); + syntaxNodeContext.TryReportDiagnostic(diagnostic, enclosingExpressionSymbol); } } diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableMemberReceivedAnalyzer.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableMemberReceivedAnalyzer.cs index bd649251..cdd9b246 100644 --- a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableMemberReceivedAnalyzer.cs +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableMemberReceivedAnalyzer.cs @@ -1,14 +1,12 @@ using System; using System.Collections.Immutable; -using System.Linq; -using System.Reflection; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using NSubstitute.Analyzers.Shared.Extensions; namespace NSubstitute.Analyzers.Shared.DiagnosticAnalyzers { - internal abstract class AbstractNonSubstitutableMemberReceivedAnalyzer : AbstractDiagnosticAnalyzer + internal abstract class AbstractNonSubstitutableMemberReceivedAnalyzer : AbstractNonSubstitutableSetupAnalyzer where TSyntaxKind : struct where TMemberAccessExpressionSyntax : SyntaxNode { @@ -16,24 +14,34 @@ internal abstract class AbstractNonSubstitutableMemberReceivedAnalyzer SupportedDiagnostics { get; } - protected AbstractNonSubstitutableMemberReceivedAnalyzer(IDiagnosticDescriptorsProvider diagnosticDescriptorsProvider) - : base(diagnosticDescriptorsProvider) + protected AbstractNonSubstitutableMemberReceivedAnalyzer( + IDiagnosticDescriptorsProvider diagnosticDescriptorsProvider, + INonSubstitutableMemberAnalysis nonSubstitutableMemberAnalysis) + : base(diagnosticDescriptorsProvider, nonSubstitutableMemberAnalysis) { _analyzeInvocationAction = AnalyzeInvocation; SupportedDiagnostics = ImmutableArray.Create( DiagnosticDescriptorsProvider.NonVirtualReceivedSetupSpecification, DiagnosticDescriptorsProvider.InternalSetupSpecification); + NonVirtualSetupDescriptor = diagnosticDescriptorsProvider.NonVirtualReceivedSetupSpecification; } protected abstract ImmutableHashSet PossibleParentsRawKinds { get; } protected abstract TSyntaxKind InvocationExpressionKind { get; } + protected override DiagnosticDescriptor NonVirtualSetupDescriptor { get; } + protected override void InitializeAnalyzer(AnalysisContext context) { context.RegisterSyntaxNodeAction(_analyzeInvocationAction, InvocationExpressionKind); } + protected override Location GetSubstitutionNodeActualLocation(in NonSubstitutableMemberAnalysisResult analysisResult) + { + return analysisResult.Member.GetSubstitutionNodeActualLocation(analysisResult.Symbol); + } + private void AnalyzeInvocation(SyntaxNodeAnalysisContext syntaxNodeContext) { var invocationExpression = syntaxNodeContext.Node; @@ -65,37 +73,12 @@ private void AnalyzeInvocation(SyntaxNodeAnalysisContext syntaxNodeContext) return; } - var canBeSetuped = symbolInfo.Symbol.CanBeSetuped(); - - if (canBeSetuped == false) - { - var diagnostic = Diagnostic.Create( - DiagnosticDescriptorsProvider.NonVirtualReceivedSetupSpecification, - GetSubstitutionNodeActualLocation(parentNode, symbolInfo.Symbol), - symbolInfo.Symbol.Name); - - syntaxNodeContext.ReportDiagnostic(diagnostic); - } - - if (canBeSetuped && symbolInfo.Symbol != null && symbolInfo.Symbol.MemberVisibleToProxyGenerator() == false) - { - var diagnostic = Diagnostic.Create( - DiagnosticDescriptorsProvider.InternalSetupSpecification, - GetSubstitutionNodeActualLocation(parentNode, symbolInfo.Symbol), - symbolInfo.Symbol.Name); - - syntaxNodeContext.ReportDiagnostic(diagnostic); - } + Analyze(syntaxNodeContext, parentNode, symbolInfo.Symbol); } private SyntaxNode GetKnownParent(SyntaxNode receivedSyntaxNode) { return PossibleParentsRawKinds.Contains(receivedSyntaxNode.Parent.RawKind) ? receivedSyntaxNode.Parent : null; } - - private Location GetSubstitutionNodeActualLocation(SyntaxNode syntaxNode, ISymbol symbol) - { - return syntaxNode.GetSubstitutionNodeActualLocation(symbol); - } } } \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableMemberReceivedInOrderAnalyzer.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableMemberReceivedInOrderAnalyzer.cs new file mode 100644 index 00000000..302ba8ab --- /dev/null +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableMemberReceivedInOrderAnalyzer.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using NSubstitute.Analyzers.Shared.Extensions; + +namespace NSubstitute.Analyzers.Shared.DiagnosticAnalyzers +{ + internal abstract class AbstractNonSubstitutableMemberReceivedInOrderAnalyzer : AbstractNonSubstitutableSetupAnalyzer + where TSyntaxKind : struct + where TInvocationExpressionSyntax : SyntaxNode + where TMemberAccessExpressionSyntax : SyntaxNode + where TBlockStatementSyntax : SyntaxNode + { + public override ImmutableArray SupportedDiagnostics { get; } + + protected abstract TSyntaxKind InvocationExpressionKind { get; } + + protected abstract ImmutableArray IgnoredAncestorPaths { get; } + + private readonly Action _analyzeInvocationAction; + private readonly ISubstitutionNodeFinder _substitutionNodeFinder; + + protected AbstractNonSubstitutableMemberReceivedInOrderAnalyzer( + ISubstitutionNodeFinder substitutionNodeFinder, + INonSubstitutableMemberAnalysis nonSubstitutableMemberAnalysis, + IDiagnosticDescriptorsProvider diagnosticDescriptorsProvider) + : base(diagnosticDescriptorsProvider, nonSubstitutableMemberAnalysis) + { + _substitutionNodeFinder = substitutionNodeFinder; + SupportedDiagnostics = ImmutableArray.Create( + DiagnosticDescriptorsProvider.InternalSetupSpecification, + DiagnosticDescriptorsProvider.NonVirtualReceivedInOrderSetupSpecification); + _analyzeInvocationAction = AnalyzeInvocation; + NonVirtualSetupDescriptor = diagnosticDescriptorsProvider.NonVirtualReceivedInOrderSetupSpecification; + } + + protected override DiagnosticDescriptor NonVirtualSetupDescriptor { get; } + + protected sealed override void InitializeAnalyzer(AnalysisContext context) + { + context.RegisterSyntaxNodeAction(_analyzeInvocationAction, InvocationExpressionKind); + } + + protected override Location GetSubstitutionNodeActualLocation(in NonSubstitutableMemberAnalysisResult analysisResult) + { + return analysisResult.Member.GetSubstitutionNodeActualLocation(analysisResult.Symbol); + } + + 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).Where(node => ShouldAnalyzeNode(syntaxNodeContext.SemanticModel, node))) + { + var symbolInfo = syntaxNodeContext.SemanticModel.GetSymbolInfo(syntaxNode); + + if (symbolInfo.Symbol == null) + { + return; + } + + Analyze(syntaxNodeContext, syntaxNode, symbolInfo.Symbol); + } + } + + private bool ShouldAnalyzeNode(SemanticModel semanticModel, SyntaxNode syntaxNode) + { + var maybeIgnoredExpression = FindIgnoredEnclosingExpression(syntaxNode); + if (maybeIgnoredExpression == null) + { + return true; + } + + if (syntaxNode.Parent is TMemberAccessExpressionSyntax || semanticModel.GetOperation(syntaxNode.Parent) is IMemberReferenceOperation) + { + return false; + } + + var operation = semanticModel.GetOperation(maybeIgnoredExpression); + + if (operation is IArgumentOperation && + operation.Parent is IInvocationOperation invocationOperation && + invocationOperation.TargetMethod.IsReceivedInOrderMethod()) + { + return true; + } + + if (operation.IsEventAssignmentOperation()) + { + return false; + } + + var symbol = GetVariableDeclaratorSymbol(operation); + + if (symbol == null) + { + return false; + } + + var blockStatementSyntax = + maybeIgnoredExpression.Ancestors().OfType().FirstOrDefault(); + + if (blockStatementSyntax == null) + { + return false; + } + + var dataFlowAnalysis = semanticModel.AnalyzeDataFlow(blockStatementSyntax); + return !dataFlowAnalysis.ReadInside.Contains(symbol); + } + + private static ILocalSymbol GetVariableDeclaratorSymbol(IOperation operation) + { + switch (operation) + { + case IVariableDeclaratorOperation declarator: + return declarator.Symbol; + case IVariableDeclarationOperation declarationOperation: + return declarationOperation.Declarators.FirstOrDefault()?.Symbol; + default: + return null; + } + } + + private SyntaxNode FindIgnoredEnclosingExpression(SyntaxNode syntaxNode) + { + return syntaxNode.Ancestors().FirstOrDefault(ancestor => IgnoredAncestorPaths.Contains(ancestor.RawKind)); + } + } +} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableMemberWhenAnalyzer.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableMemberWhenAnalyzer.cs index 3cfe1fc3..7e67eb0c 100644 --- a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableMemberWhenAnalyzer.cs +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableMemberWhenAnalyzer.cs @@ -6,7 +6,7 @@ namespace NSubstitute.Analyzers.Shared.DiagnosticAnalyzers { - internal abstract class AbstractNonSubstitutableMemberWhenAnalyzer : AbstractDiagnosticAnalyzer + internal abstract class AbstractNonSubstitutableMemberWhenAnalyzer : AbstractNonSubstitutableSetupAnalyzer where TInvocationExpressionSyntax : SyntaxNode where TSyntaxKind : struct, Enum { @@ -20,16 +20,20 @@ internal abstract class AbstractNonSubstitutableMemberWhenAnalyzer substitutionNodeFinder) - : base(diagnosticDescriptorsProvider) + ISubstitutionNodeFinder substitutionNodeFinder, + INonSubstitutableMemberAnalysis nonSubstitutableMemberAnalysis) + : base(diagnosticDescriptorsProvider, nonSubstitutableMemberAnalysis) { _substitutionNodeFinder = substitutionNodeFinder; _analyzeInvocationAction = AnalyzeInvocation; SupportedDiagnostics = ImmutableArray.Create( DiagnosticDescriptorsProvider.NonVirtualWhenSetupSpecification, DiagnosticDescriptorsProvider.InternalSetupSpecification); + NonVirtualSetupDescriptor = diagnosticDescriptorsProvider.NonVirtualWhenSetupSpecification; } + protected override DiagnosticDescriptor NonVirtualSetupDescriptor { get; } + protected override void InitializeAnalyzer(AnalysisContext context) { context.RegisterSyntaxNodeAction(_analyzeInvocationAction, InvocationExpressionKind); @@ -58,26 +62,7 @@ private void AnalyzeInvocation(SyntaxNodeAnalysisContext syntaxNodeContext) var symbolInfo = syntaxNodeContext.SemanticModel.GetSymbolInfo(analysedSyntax); if (symbolInfo.Symbol != null) { - var canBeSetuped = symbolInfo.Symbol.CanBeSetuped(); - if (canBeSetuped == false) - { - var diagnostic = Diagnostic.Create( - DiagnosticDescriptorsProvider.NonVirtualWhenSetupSpecification, - analysedSyntax.GetLocation(), - symbolInfo.Symbol.Name); - - syntaxNodeContext.ReportDiagnostic(diagnostic); - } - - if (canBeSetuped && symbolInfo.Symbol.MemberVisibleToProxyGenerator() == false) - { - var diagnostic = Diagnostic.Create( - DiagnosticDescriptorsProvider.InternalSetupSpecification, - analysedSyntax.GetLocation(), - symbolInfo.Symbol.Name); - - syntaxNodeContext.ReportDiagnostic(diagnostic); - } + Analyze(syntaxNodeContext, analysedSyntax, symbolInfo.Symbol); } } } diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableSetupAnalyzer.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableSetupAnalyzer.cs new file mode 100644 index 00000000..05fa20e8 --- /dev/null +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/AbstractNonSubstitutableSetupAnalyzer.cs @@ -0,0 +1,64 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using NSubstitute.Analyzers.Shared.Extensions; + +namespace NSubstitute.Analyzers.Shared.DiagnosticAnalyzers +{ + internal abstract class AbstractNonSubstitutableSetupAnalyzer : AbstractDiagnosticAnalyzer + { + private readonly INonSubstitutableMemberAnalysis _nonSubstitutableMemberAnalysis; + + protected abstract DiagnosticDescriptor NonVirtualSetupDescriptor { get; } + + private readonly DiagnosticDescriptor _internalSetupSpecificationDescriptor; + + protected AbstractNonSubstitutableSetupAnalyzer( + IDiagnosticDescriptorsProvider diagnosticDescriptorsProvider, + INonSubstitutableMemberAnalysis nonSubstitutableMemberAnalysis) + : base(diagnosticDescriptorsProvider) + { + _nonSubstitutableMemberAnalysis = nonSubstitutableMemberAnalysis; + _internalSetupSpecificationDescriptor = diagnosticDescriptorsProvider.InternalSetupSpecification; + } + + protected void Analyze(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext, SyntaxNode syntaxNode, ISymbol symbol = null) + { + var analysisResult = _nonSubstitutableMemberAnalysis.Analyze(syntaxNodeAnalysisContext, syntaxNode, symbol); + + if (analysisResult.CanBeSubstituted == false) + { + ReportDiagnostics(syntaxNodeAnalysisContext, in analysisResult); + } + } + + protected virtual Location GetSubstitutionNodeActualLocation(in NonSubstitutableMemberAnalysisResult analysisResult) + { + return analysisResult.Member.GetLocation(); + } + + private void ReportDiagnostics( + SyntaxNodeAnalysisContext context, + in NonSubstitutableMemberAnalysisResult analysisResult) + { + var location = GetSubstitutionNodeActualLocation(analysisResult); + if (analysisResult.NonVirtualMemberSubstitution) + { + var diagnostic = Diagnostic.Create( + NonVirtualSetupDescriptor, + location, + analysisResult.MemberName); + context.TryReportDiagnostic(diagnostic, analysisResult.Symbol); + } + + if (analysisResult.InternalMemberSubstitution) + { + var diagnostic = Diagnostic.Create( + _internalSetupSpecificationDescriptor, + location, + analysisResult.MemberName); + + context.ReportDiagnostic(diagnostic); + } + } + } +} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/INonSubstitutableMemberAnalysis.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/INonSubstitutableMemberAnalysis.cs new file mode 100644 index 00000000..14525d7e --- /dev/null +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/INonSubstitutableMemberAnalysis.cs @@ -0,0 +1,13 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace NSubstitute.Analyzers.Shared.DiagnosticAnalyzers +{ + internal interface INonSubstitutableMemberAnalysis + { + NonSubstitutableMemberAnalysisResult Analyze( + in SyntaxNodeAnalysisContext syntaxNodeContext, + SyntaxNode accessedMember, + ISymbol symbol = null); + } +} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/NonSubstitutableMemberAnalysisResult.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/NonSubstitutableMemberAnalysisResult.cs new file mode 100644 index 00000000..490bad8a --- /dev/null +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticAnalyzers/NonSubstitutableMemberAnalysisResult.cs @@ -0,0 +1,34 @@ +using Microsoft.CodeAnalysis; + +namespace NSubstitute.Analyzers.Shared.DiagnosticAnalyzers +{ + internal readonly struct NonSubstitutableMemberAnalysisResult + { + public bool CanBeSubstituted { get; } + + public bool NonVirtualMemberSubstitution { get; } + + public bool InternalMemberSubstitution { get; } + + public ISymbol Symbol { get; } + + public string MemberName { get; } + + public SyntaxNode Member { get; } + + public NonSubstitutableMemberAnalysisResult( + bool nonVirtualMemberSubstitution, + bool internalMemberSubstitution, + ISymbol symbol, + SyntaxNode member, + string memberName) + { + NonVirtualMemberSubstitution = nonVirtualMemberSubstitution; + InternalMemberSubstitution = internalMemberSubstitution; + Member = member; + MemberName = memberName; + Symbol = symbol; + CanBeSubstituted = !NonVirtualMemberSubstitution && !InternalMemberSubstitution; + } + } +} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticDescriptors.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticDescriptors.cs index 84acb506..7b1bde25 100644 --- a/src/NSubstitute.Analyzers.Shared/DiagnosticDescriptors.cs +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticDescriptors.cs @@ -46,6 +46,14 @@ internal class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); + public static DiagnosticDescriptor NonVirtualReceivedInOrderSetupSpecification { get; } = + CreateDiagnosticDescriptor( + name: nameof(NonVirtualReceivedInOrderSetupSpecification), + id: DiagnosticIdentifiers.NonVirtualReceivedInOrderSetupSpecification, + category: DiagnosticCategory.NonVirtualSubstitution.GetDisplayName(), + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + public static DiagnosticDescriptor PartialSubstituteForUnsupportedType { get; } = CreateDiagnosticDescriptor( name: nameof(PartialSubstituteForUnsupportedType), diff --git a/src/NSubstitute.Analyzers.Shared/DiagnosticIdentifiers.cs b/src/NSubstitute.Analyzers.Shared/DiagnosticIdentifiers.cs index 982f9661..529d8e7f 100644 --- a/src/NSubstitute.Analyzers.Shared/DiagnosticIdentifiers.cs +++ b/src/NSubstitute.Analyzers.Shared/DiagnosticIdentifiers.cs @@ -7,6 +7,7 @@ internal class DiagnosticIdentifiers public const string NonVirtualWhenSetupSpecification = "NS1002"; public const string InternalSetupSpecification = "NS1003"; public const string NonSubstitutableMemberArgumentMatcherUsage = "NS1004"; + public const string NonVirtualReceivedInOrderSetupSpecification = "NS1005"; public const string PartialSubstituteForUnsupportedType = "NS2000"; public const string SubstituteForWithoutAccessibleConstructor = "NS2001"; diff --git a/src/NSubstitute.Analyzers.Shared/Extensions/IOperationExtensions.cs b/src/NSubstitute.Analyzers.Shared/Extensions/IOperationExtensions.cs new file mode 100644 index 00000000..e9526e9c --- /dev/null +++ b/src/NSubstitute.Analyzers.Shared/Extensions/IOperationExtensions.cs @@ -0,0 +1,14 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Operations; + +namespace NSubstitute.Analyzers.Shared.Extensions +{ + internal static class IOperationExtensions + { + public static bool IsEventAssignmentOperation(this IOperation operation) + { + return operation is IAssignmentOperation assignmentOperation && + assignmentOperation.Kind == OperationKind.EventAssignment; + } + } +} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/Extensions/ISymbolExtensions.cs b/src/NSubstitute.Analyzers.Shared/Extensions/ISymbolExtensions.cs index 6f6de933..1150616b 100644 --- a/src/NSubstitute.Analyzers.Shared/Extensions/ISymbolExtensions.cs +++ b/src/NSubstitute.Analyzers.Shared/Extensions/ISymbolExtensions.cs @@ -5,11 +5,6 @@ namespace NSubstitute.Analyzers.Shared.Extensions { internal static class ISymbolExtensions { - public static bool CanBeSetuped(this ISymbol symbol) - { - return IsInterfaceMember(symbol) || IsVirtual(symbol); - } - public static bool MemberVisibleToProxyGenerator(this ISymbol symbol) { return symbol.DeclaredAccessibility != Accessibility.Internal || symbol.InternalsVisibleToProxyGenerator(); @@ -46,19 +41,5 @@ public static bool IsLocal(this ISymbol symbol) { return symbol != null && symbol.Kind == SymbolKind.Local; } - - private static bool IsInterfaceMember(ISymbol symbol) - { - return symbol?.ContainingType?.TypeKind == TypeKind.Interface; - } - - private static bool IsVirtual(ISymbol symbol) - { - var isVirtual = symbol.IsVirtual - || (symbol.IsOverride && !symbol.IsSealed) - || symbol.IsAbstract; - - return isVirtual; - } } } \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/Extensions/SyntaxNodeAnalysisContextExtensions.cs b/src/NSubstitute.Analyzers.Shared/Extensions/SyntaxNodeAnalysisContextExtensions.cs index 91765c8f..238349ec 100644 --- a/src/NSubstitute.Analyzers.Shared/Extensions/SyntaxNodeAnalysisContextExtensions.cs +++ b/src/NSubstitute.Analyzers.Shared/Extensions/SyntaxNodeAnalysisContextExtensions.cs @@ -1,4 +1,8 @@ -using System.Threading; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using NSubstitute.Analyzers.Shared.Settings; @@ -10,5 +14,72 @@ internal static AnalyzersSettings GetSettings(this SyntaxNodeAnalysisContext con { return context.Options.GetSettings(cancellationToken); } + + internal static void TryReportDiagnostic( + this SyntaxNodeAnalysisContext syntaxNodeContext, + Diagnostic diagnostic, + ISymbol symbol) + { + if (IsSuppressed(syntaxNodeContext, diagnostic, symbol)) + { + return; + } + + syntaxNodeContext.ReportDiagnostic(diagnostic); + } + + private static bool IsSuppressed( + SyntaxNodeAnalysisContext syntaxNodeContext, + Diagnostic diagnostic, + ISymbol symbol) + { + return symbol != null && IsSuppressed(syntaxNodeContext, symbol, diagnostic.Id); + } + + private static bool IsSuppressed( + SyntaxNodeAnalysisContext syntaxNodeContext, + ISymbol symbol, + string diagnosticId) + { + var analyzersSettings = syntaxNodeContext.GetSettings(CancellationToken.None); + + if (analyzersSettings.Suppressions.Count == 0) + { + return false; + } + + var possibleSymbols = GetPossibleSymbols(symbol).ToImmutableHashSet(); + + return analyzersSettings.Suppressions.Where(suppression => suppression.Rules.Contains(diagnosticId)) + .SelectMany(suppression => + DocumentationCommentId.GetSymbolsForDeclarationId(suppression.Target, syntaxNodeContext.Compilation)) + .Any(possibleSymbols.Contains); + } + + private static IEnumerable GetPossibleSymbols(ISymbol symbol) + { + yield return symbol; + yield return symbol.ContainingType; + yield return symbol.ContainingNamespace; + + if (symbol is IMethodSymbol methodSymbol) + { + yield return methodSymbol.ConstructedFrom; + if (methodSymbol.ReducedFrom != null) + { + yield return methodSymbol.ReducedFrom; + } + } + + if (symbol.ContainingType is INamedTypeSymbol namedTypeSymbol) + { + yield return namedTypeSymbol.ConstructedFrom; + } + + if (symbol is IPropertySymbol propertySymbol) + { + yield return propertySymbol.OriginalDefinition; + } + } } } \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.Shared/IDiagnosticDescriptorsProvider.cs b/src/NSubstitute.Analyzers.Shared/IDiagnosticDescriptorsProvider.cs index fa88ddc3..121ca752 100644 --- a/src/NSubstitute.Analyzers.Shared/IDiagnosticDescriptorsProvider.cs +++ b/src/NSubstitute.Analyzers.Shared/IDiagnosticDescriptorsProvider.cs @@ -6,6 +6,12 @@ internal interface IDiagnosticDescriptorsProvider { DiagnosticDescriptor NonVirtualSetupSpecification { get; } + DiagnosticDescriptor NonVirtualReceivedSetupSpecification { get; } + + DiagnosticDescriptor NonVirtualWhenSetupSpecification { get; } + + DiagnosticDescriptor NonVirtualReceivedInOrderSetupSpecification { get; } + DiagnosticDescriptor InternalSetupSpecification { get; } DiagnosticDescriptor UnusedReceived { get; } @@ -28,10 +34,6 @@ internal interface IDiagnosticDescriptorsProvider DiagnosticDescriptor SubstituteConstructorArgumentsForDelegate { get; } - DiagnosticDescriptor NonVirtualReceivedSetupSpecification { get; } - - DiagnosticDescriptor NonVirtualWhenSetupSpecification { get; } - DiagnosticDescriptor ReEntrantSubstituteCall { get; } DiagnosticDescriptor CallInfoArgumentOutOfRange { get; } diff --git a/src/NSubstitute.Analyzers.Shared/Resources.Designer.cs b/src/NSubstitute.Analyzers.Shared/Resources.Designer.cs index 61fb22ef..4cd95ebc 100644 --- a/src/NSubstitute.Analyzers.Shared/Resources.Designer.cs +++ b/src/NSubstitute.Analyzers.Shared/Resources.Designer.cs @@ -1,17 +1,15 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // //------------------------------------------------------------------------------ -using System.Reflection; - namespace NSubstitute.Analyzers.Shared { using System; + using System.Reflection; [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] @@ -461,5 +459,17 @@ internal static string AsyncCallbackUsedInReceivedInOrderTitle { return ResourceManager.GetString("AsyncCallbackUsedInReceivedInOrderTitle", resourceCulture); } } + + internal static string NonVirtualReceivedInOrderSetupSpecificationDescription { + get { + return ResourceManager.GetString("NonVirtualReceivedInOrderSetupSpecificationDescription", resourceCulture); + } + } + + internal static string NonVirtualReceivedInOrderSetupSpecificationTitle { + get { + return ResourceManager.GetString("NonVirtualReceivedInOrderSetupSpecificationTitle", resourceCulture); + } + } } } diff --git a/src/NSubstitute.Analyzers.Shared/Resources.resx b/src/NSubstitute.Analyzers.Shared/Resources.resx index 4c90689a..01f1a7c1 100644 --- a/src/NSubstitute.Analyzers.Shared/Resources.resx +++ b/src/NSubstitute.Analyzers.Shared/Resources.resx @@ -401,4 +401,12 @@ Async callback used in Received.InOrder method. The title of the diagnostic. + + Non-virtual members can not be intercepted. + An optional longer localizable description of the diagnostic. + + + Non-virtual setup specification. + The title of the diagnostic. + \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/NonSubstitutableMemberAnalysis.cs b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/NonSubstitutableMemberAnalysis.cs new file mode 100644 index 00000000..d6987950 --- /dev/null +++ b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/NonSubstitutableMemberAnalysis.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; +using NSubstitute.Analyzers.Shared.DiagnosticAnalyzers; + +namespace NSubstitute.Analyzers.VisualBasic.DiagnosticAnalyzers +{ + internal class NonSubstitutableMemberAnalysis : AbstractNonSubstitutableMemberAnalysis + { + public static NonSubstitutableMemberAnalysis Instance { get; } = new NonSubstitutableMemberAnalysis(); + + private NonSubstitutableMemberAnalysis() + { + } + + protected override ImmutableHashSet KnownNonVirtualSyntaxKinds { get; } = ImmutableHashSet.Create( + typeof(LiteralExpressionSyntax)); + } +} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/NonSubstitutableMemberAnalyzer.cs b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/NonSubstitutableMemberAnalyzer.cs index 829e880f..461d9e0f 100644 --- a/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/NonSubstitutableMemberAnalyzer.cs +++ b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/NonSubstitutableMemberAnalyzer.cs @@ -13,9 +13,6 @@ internal sealed class NonSubstitutableMemberAnalyzer : AbstractNonSubstitutableM { protected override SyntaxKind InvocationExpressionKind { get; } = SyntaxKind.InvocationExpression; - protected override ImmutableHashSet KnownNonVirtualSyntaxKinds { get; } = ImmutableHashSet.Create( - typeof(LiteralExpressionSyntax)); - protected override ImmutableHashSet SupportedMemberAccesses { get; } = ImmutableHashSet.Create( (int)SyntaxKind.InvocationExpression, (int)SyntaxKind.SimpleMemberAccessExpression, @@ -26,7 +23,7 @@ internal sealed class NonSubstitutableMemberAnalyzer : AbstractNonSubstitutableM (int)SyntaxKind.StringLiteralExpression); public NonSubstitutableMemberAnalyzer() - : base(NSubstitute.Analyzers.VisualBasic.DiagnosticDescriptorsProvider.Instance, SubstitutionNodeFinder.Instance) + : base(NSubstitute.Analyzers.VisualBasic.DiagnosticDescriptorsProvider.Instance, SubstitutionNodeFinder.Instance, NonSubstitutableMemberAnalysis.Instance) { } } diff --git a/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/NonSubstitutableMemberArgumentMatcherAnalyzer.cs b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/NonSubstitutableMemberArgumentMatcherAnalyzer.cs index b729f87f..37e99372 100644 --- a/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/NonSubstitutableMemberArgumentMatcherAnalyzer.cs +++ b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/NonSubstitutableMemberArgumentMatcherAnalyzer.cs @@ -49,7 +49,7 @@ internal sealed class NonSubstitutableMemberArgumentMatcherAnalyzer : AbstractNo (int)SyntaxKind.VariableDeclarator)); public NonSubstitutableMemberArgumentMatcherAnalyzer() - : base(NSubstitute.Analyzers.VisualBasic.DiagnosticDescriptorsProvider.Instance) + : base(NonSubstitutableMemberAnalysis.Instance, NSubstitute.Analyzers.VisualBasic.DiagnosticDescriptorsProvider.Instance) { } diff --git a/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/NonSubstitutableMemberReceivedAnalyzer.cs b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/NonSubstitutableMemberReceivedAnalyzer.cs index 816ebdeb..a086e1a0 100644 --- a/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/NonSubstitutableMemberReceivedAnalyzer.cs +++ b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/NonSubstitutableMemberReceivedAnalyzer.cs @@ -17,7 +17,7 @@ internal sealed class NonSubstitutableMemberReceivedAnalyzer : AbstractNonSubsti protected override SyntaxKind InvocationExpressionKind { get; } = SyntaxKind.InvocationExpression; public NonSubstitutableMemberReceivedAnalyzer() - : base(NSubstitute.Analyzers.VisualBasic.DiagnosticDescriptorsProvider.Instance) + : base(NSubstitute.Analyzers.VisualBasic.DiagnosticDescriptorsProvider.Instance, NonSubstitutableMemberAnalysis.Instance) { } } diff --git a/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/NonSubstitutableMemberReceivedInOrderAnalyzer.cs b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/NonSubstitutableMemberReceivedInOrderAnalyzer.cs new file mode 100644 index 00000000..a66aa2aa --- /dev/null +++ b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/NonSubstitutableMemberReceivedInOrderAnalyzer.cs @@ -0,0 +1,27 @@ +using System.Collections.Immutable; +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 NonSubstitutableMemberReceivedInOrderAnalyzer : AbstractNonSubstitutableMemberReceivedInOrderAnalyzer + { + private static ImmutableArray IgnoredPaths { get; } = ImmutableArray.Create( + (int)SyntaxKind.SimpleArgument, + (int)SyntaxKind.VariableDeclarator, + (int)SyntaxKind.AddAssignmentStatement); + + public NonSubstitutableMemberReceivedInOrderAnalyzer() + : base(SubstitutionNodeFinder.Instance, NonSubstitutableMemberAnalysis.Instance, NSubstitute.Analyzers.VisualBasic.DiagnosticDescriptorsProvider.Instance) + { + } + + protected override SyntaxKind InvocationExpressionKind { get; } = SyntaxKind.InvocationExpression; + + protected override ImmutableArray IgnoredAncestorPaths { get; } = IgnoredPaths; + } +} \ No newline at end of file diff --git a/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/NonSubstitutableMemberWhenAnalyzer.cs b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/NonSubstitutableMemberWhenAnalyzer.cs index 107b292a..77514ac4 100644 --- a/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/NonSubstitutableMemberWhenAnalyzer.cs +++ b/src/NSubstitute.Analyzers.VisualBasic/DiagnosticAnalyzers/NonSubstitutableMemberWhenAnalyzer.cs @@ -10,7 +10,7 @@ namespace NSubstitute.Analyzers.VisualBasic.DiagnosticAnalyzers internal sealed class NonSubstitutableMemberWhenAnalyzer : AbstractNonSubstitutableMemberWhenAnalyzer { public NonSubstitutableMemberWhenAnalyzer() - : base(NSubstitute.Analyzers.VisualBasic.DiagnosticDescriptorsProvider.Instance, SubstitutionNodeFinder.Instance) + : base(NSubstitute.Analyzers.VisualBasic.DiagnosticDescriptorsProvider.Instance, SubstitutionNodeFinder.Instance, NonSubstitutableMemberAnalysis.Instance) { } diff --git a/src/NSubstitute.Analyzers.VisualBasic/Resources.Designer.cs b/src/NSubstitute.Analyzers.VisualBasic/Resources.Designer.cs index b72eb3d3..553d1713 100644 --- a/src/NSubstitute.Analyzers.VisualBasic/Resources.Designer.cs +++ b/src/NSubstitute.Analyzers.VisualBasic/Resources.Designer.cs @@ -1,7 +1,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -13,46 +12,32 @@ namespace NSubstitute.Analyzers.VisualBasic { using System.Reflection; - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [System.Diagnostics.DebuggerNonUserCodeAttribute()] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { - private static global::System.Resources.ResourceManager resourceMan; + private static System.Resources.ResourceManager resourceMan; - private static global::System.Globalization.CultureInfo resourceCulture; + private static System.Globalization.CultureInfo resourceCulture; - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Resources() { } - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Resources.ResourceManager ResourceManager { get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("NSubstitute.Analyzers.VisualBasic.Resources", typeof(Resources).GetTypeInfo().Assembly); + if (object.Equals(null, resourceMan)) { + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("NSubstitute.Analyzers.VisualBasic.Resources", typeof(Resources).GetTypeInfo().Assembly); resourceMan = temp; } return resourceMan; } } - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -61,57 +46,45 @@ internal Resources() { } } - /// - /// Looks up a localized string similar to Friend member {0} can not be intercepted without InternalsVisibleToAttribute.. - /// - internal static string InternalSetupSpecificationMessageFormat { + internal static string NonVirtualSetupSpecificationMessageFormat { get { - return ResourceManager.GetString("InternalSetupSpecificationMessageFormat", resourceCulture); + return ResourceManager.GetString("NonVirtualSetupSpecificationMessageFormat", resourceCulture); } } - /// - /// Looks up a localized string similar to Member {0} can not be intercepted. Only interface members and overrideable, overriding, and must override members can be intercepted.. - /// - internal static string NonVirtualReceivedSetupSpecificationMessageFormat { + internal static string SubstituteForInternalMemberMessageFormat { get { - return ResourceManager.GetString("NonVirtualReceivedSetupSpecificationMessageFormat", resourceCulture); + return ResourceManager.GetString("SubstituteForInternalMemberMessageFormat", resourceCulture); } } - /// - /// Looks up a localized string similar to Member {0} can not be intercepted. Only interface members and overrideable, overriding, and must override members can be intercepted.. - /// - internal static string NonVirtualSetupSpecificationMessageFormat { + internal static string NonVirtualReceivedSetupSpecificationMessageFormat { get { - return ResourceManager.GetString("NonVirtualSetupSpecificationMessageFormat", resourceCulture); + return ResourceManager.GetString("NonVirtualReceivedSetupSpecificationMessageFormat", resourceCulture); } } - /// - /// Looks up a localized string similar to Member {0} can not be intercepted. Only interface members and overrideable, overriding, and must override members can be intercepted.. - /// internal static string NonVirtualWhenSetupSpecificationMessageFormat { get { return ResourceManager.GetString("NonVirtualWhenSetupSpecificationMessageFormat", resourceCulture); } } - /// - /// Looks up a localized string similar to {0}() is set with a method that itself calls {1}. This can cause problems with NSubstitute. Consider replacing with a lambda: {0}(Function(x) {2}).. - /// + internal static string NonVirtualReceivedInOrderSetupSpecificationMessageFormat { + get { + return ResourceManager.GetString("NonVirtualReceivedInOrderSetupSpecificationMessageFormat", resourceCulture); + } + } + internal static string ReEntrantSubstituteCallMessageFormat { get { return ResourceManager.GetString("ReEntrantSubstituteCallMessageFormat", resourceCulture); } } - /// - /// Looks up a localized string similar to Can not substitute for internal type. To substitute for internal type expose your type to DynamicProxyGenAssembly2 via <Assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")>. - /// - internal static string SubstituteForInternalMemberMessageFormat { + internal static string InternalSetupSpecificationMessageFormat { get { - return ResourceManager.GetString("SubstituteForInternalMemberMessageFormat", resourceCulture); + return ResourceManager.GetString("InternalSetupSpecificationMessageFormat", resourceCulture); } } } diff --git a/src/NSubstitute.Analyzers.VisualBasic/Resources.resx b/src/NSubstitute.Analyzers.VisualBasic/Resources.resx index a803f6db..c82e64bf 100644 --- a/src/NSubstitute.Analyzers.VisualBasic/Resources.resx +++ b/src/NSubstitute.Analyzers.VisualBasic/Resources.resx @@ -133,6 +133,10 @@ Member {0} can not be intercepted. Only interface members and overrideable, overriding, and must override members can be intercepted. The format-able message the diagnostic displays. + + Member {0} can not be intercepted. Only interface members and overrideable, overriding, and must override members can be intercepted. + The format-able message the diagnostic displays. + {0}() is set with a method that itself calls {1}. This can cause problems with NSubstitute. Consider replacing with a lambda: {0}(Function(x) {2}). The format-able message the diagnostic displays. diff --git a/tests/NSubstitute.Analyzers.Tests.CSharp/CodeFixProviderTests/NonSubstitutableMemberReceivedInOrderSuppressDiagnosticsCodeFixProviderTests/NonSubstitutableMemberReceivedInOrderSuppressDiagnosticsCodeFixVerifier.cs b/tests/NSubstitute.Analyzers.Tests.CSharp/CodeFixProviderTests/NonSubstitutableMemberReceivedInOrderSuppressDiagnosticsCodeFixProviderTests/NonSubstitutableMemberReceivedInOrderSuppressDiagnosticsCodeFixVerifier.cs new file mode 100644 index 00000000..fbd18163 --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.CSharp/CodeFixProviderTests/NonSubstitutableMemberReceivedInOrderSuppressDiagnosticsCodeFixProviderTests/NonSubstitutableMemberReceivedInOrderSuppressDiagnosticsCodeFixVerifier.cs @@ -0,0 +1,266 @@ +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using NSubstitute.Analyzers.CSharp.CodeFixProviders; +using NSubstitute.Analyzers.CSharp.DiagnosticAnalyzers; +using NSubstitute.Analyzers.Shared; +using NSubstitute.Analyzers.Tests.Shared.CodeFixProviders; +using Xunit; + +namespace NSubstitute.Analyzers.Tests.CSharp.CodeFixProviderTests.NonSubstitutableMemberReceivedInOrderSuppressDiagnosticsCodeFixProviderTests +{ + public class NonSubstitutableMemberReceivedInOrderSuppressDiagnosticsCodeFixVerifier : CSharpSuppressDiagnosticSettingsVerifier, INonSubstitutableMemberSuppressDiagnosticsCodeFixVerifier + { + protected override CodeFixProvider CodeFixProvider { get; } = new NonSubstitutableMemberSuppressDiagnosticsCodeFixProvider(); + + protected override DiagnosticAnalyzer DiagnosticAnalyzer { get; } = new NonSubstitutableMemberReceivedInOrderAnalyzer(); + + [Fact] + public async Task SuppressesDiagnosticsInSettings_WhenSettingValueForNonVirtualMethod() + { + var source = @"using NSubstitute; + +namespace MyNamespace +{ + public class Foo + { + public int Bar() + { + return 2; + } + } + + public class FooTests + { + public void Test() + { + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + { + substitute.Bar(); + }); + } + } +}"; + + await VerifySuppressionSettings(source, "M:MyNamespace.Foo.Bar~System.Int32", DiagnosticIdentifiers.NonVirtualReceivedInOrderSetupSpecification); + } + + [Fact] + public async Task SuppressesDiagnosticsInSettings_WhenSettingValueForStaticMethod() + { + var source = @"using NSubstitute; + +namespace MyNamespace +{ + public class Foo + { + public static int Bar() + { + return 2; + } + } + + public class FooTests + { + public void Test() + { + Received.InOrder(() => + { + Foo.Bar(); + }); + } + } +}"; + + await VerifySuppressionSettings(source, "M:MyNamespace.Foo.Bar~System.Int32", DiagnosticIdentifiers.NonVirtualReceivedInOrderSetupSpecification); + } + + [Fact] + public async Task SuppressesDiagnosticsInSettings_WhenSettingValueForExtensionMethod() + { + var source = @"using NSubstitute; + +namespace MyNamespace +{ + public class FooTests + { + public void Test() + { + MyExtensions.Bar = Substitute.For(); + var substitute = Substitute.For(); + Received.InOrder(() => + { + substitute.GetBar(); + }); + } + } + + public static class MyExtensions + { + public static IBar Bar { get; set; } + + public static int GetBar(this object @object) + { + return Bar.Foo(@object); + } + } + + public interface IBar + { + int Foo(object @obj); + } +}"; + + await VerifySuppressionSettings(source, "M:MyNamespace.MyExtensions.GetBar(System.Object)~System.Int32", DiagnosticIdentifiers.NonVirtualReceivedInOrderSetupSpecification); + } + + [Fact] + public async Task SuppressesDiagnosticsInSettings_WhenSettingValueForSealedOverrideMethod() + { + var source = @"using NSubstitute; + +namespace MyNamespace +{ + public class Foo + { + public virtual int Bar() + { + return 2; + } + } + + public class Foo2 : Foo + { + public sealed override int Bar() => 1; + } + + public class FooTests + { + public void Test() + { + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + { + substitute.Bar(); + }); + } + } +}"; + + await VerifySuppressionSettings(source, "M:MyNamespace.Foo2.Bar~System.Int32", DiagnosticIdentifiers.NonVirtualReceivedInOrderSetupSpecification); + } + + [Fact] + public async Task SuppressesDiagnosticsInSettings_WhenSettingValueForNonVirtualProperty() + { + var source = @"using NSubstitute; + +namespace MyNamespace +{ + public class Foo + { + public int Bar { get; } + } + + public class FooTests + { + public void Test() + { + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + { + _ = substitute.Bar; + }); + } + } +}"; + + await VerifySuppressionSettings(source, "P:MyNamespace.Foo.Bar", DiagnosticIdentifiers.NonVirtualReceivedInOrderSetupSpecification); + } + + [Fact] + public async Task SuppressesDiagnosticsInSettings_WhenSettingValueForNonVirtualIndexer() + { + var source = @"using NSubstitute; + +namespace MyNamespace +{ + public class Foo + { + public int this[int x] => 0; + } + + public class FooTests + { + public void Test() + { + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + { + _ = substitute[1]; + }); + } + } +}"; + + await VerifySuppressionSettings(source, "P:MyNamespace.Foo.Item(System.Int32)", DiagnosticIdentifiers.NonVirtualReceivedInOrderSetupSpecification); + } + + [Fact] + public async Task SuppressesDiagnosticsInSettingsForClass_WhenSettingsValueForNonVirtualMember_AndSelectingClassSuppression() + { + var source = @"using NSubstitute; + +namespace MyNamespace +{ + public class Foo + { + public int this[int x] => 0; + } + + public class FooTests + { + public void Test() + { + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + { + _ = substitute[1]; + }); + } + } +}"; + + await VerifySuppressionSettings(source, "T:MyNamespace.Foo", DiagnosticIdentifiers.NonVirtualReceivedInOrderSetupSpecification, 1); + } + + [Fact] + public async Task SuppressesDiagnosticsInSettingsForNamespace_WhenSettingsValueForNonVirtualMember_AndSelectingNamespaceSuppression() + { + var source = @"using NSubstitute; + +namespace MyNamespace +{ + public class Foo + { + public int this[int x] => 0; + } + + public class FooTests + { + public void Test() + { + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + { + _ = substitute[1]; + }); + } + } +}"; + + await VerifySuppressionSettings(source, "N:MyNamespace", DiagnosticIdentifiers.NonVirtualReceivedInOrderSetupSpecification, 2); + } + } +} \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/NonSubstitutableMemberReceivedInOrderAnalyzerTests/NonSubstitutableMemberReceivedInOrderDiagnosticVerifier.cs b/tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/NonSubstitutableMemberReceivedInOrderAnalyzerTests/NonSubstitutableMemberReceivedInOrderDiagnosticVerifier.cs new file mode 100644 index 00000000..584be72b --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.CSharp/DiagnosticAnalyzerTests/NonSubstitutableMemberReceivedInOrderAnalyzerTests/NonSubstitutableMemberReceivedInOrderDiagnosticVerifier.cs @@ -0,0 +1,818 @@ +using System.Linq; +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.Shared.Settings; +using NSubstitute.Analyzers.Shared.TinyJson; +using NSubstitute.Analyzers.Tests.Shared.DiagnosticAnalyzers; +using NSubstitute.Analyzers.Tests.Shared.Extensions; +using Xunit; + +namespace NSubstitute.Analyzers.Tests.CSharp.DiagnosticAnalyzerTests.NonSubstitutableMemberReceivedInOrderAnalyzerTests +{ + public class NonSubstitutableMemberReceivedInOrderDiagnosticVerifier : CSharpDiagnosticVerifier, INonSubstitutableMemberReceivedInOrderDiagnosticVerifier + { + internal AnalyzersSettings Settings { get; set; } + + protected override DiagnosticAnalyzer DiagnosticAnalyzer { get; } = new NonSubstitutableMemberReceivedInOrderAnalyzer(); + + private readonly DiagnosticDescriptor _internalSetupSpecificationDescriptor = DiagnosticDescriptors.InternalSetupSpecification; + + private readonly DiagnosticDescriptor _nonVirtualReceivedInOrderSetupSpecificationDescriptor = DiagnosticDescriptors.NonVirtualReceivedInOrderSetupSpecification; + + protected override string AnalyzerSettings => Settings != null ? Json.Encode(Settings) : null; + + [Fact] + public async Task ReportsDiagnostics_WhenInvokingNonVirtualMethodWithoutAssignment() + { + var source = @"using NSubstitute; +using System.Threading.Tasks; +namespace MyNamespace +{ + public class Foo + { + public Foo Nested { get; set; } + + public int Bar() + { + return 2; + } + } + + public class FooBar + { + public FooBar Nested { get; set; } + + public Task Bar() + { + return Task.CompletedTask; + } + } + + public class FooTests + { + public void Test() + { + var substitute = NSubstitute.Substitute.For(); + var otherSubstitute = NSubstitute.Substitute.For(); + Received.InOrder(() => [|substitute.Bar()|]); + Received.InOrder(() => [|substitute.Nested.Bar()|]); + Received.InOrder(async () => await [|otherSubstitute.Bar()|]); + Received.InOrder(async () => await [|otherSubstitute.Nested.Bar()|]); + } + } +}"; + await VerifyDiagnostic(source, _nonVirtualReceivedInOrderSetupSpecificationDescriptor, "Member Bar can not be intercepted. Only interface members and virtual, overriding, and abstract members can be intercepted."); + } + + [Fact] + public async Task ReportsDiagnostics_WhenInvokingNonVirtualMethodWithNonUsedAssignment() + { + var source = @"using NSubstitute; +using System.Threading.Tasks; +namespace MyNamespace +{ + public class Foo + { + public int Bar() + { + return 2; + } + + public Foo Bar(int x) + { + return null; + } + } + + public class FooBar + { + public Task Bar() + { + return Task.FromResult(1); + } + } + + public class FooTests + { + public void Test() + { + var substitute = NSubstitute.Substitute.For(); + var otherSubstitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + { + [|substitute.Bar()|]; + var x = (int)[|substitute.Bar()|]; + var y = [|substitute.Bar()|]; + var z = (int)[|substitute.Bar()|]; + var zz = [|substitute.Bar()|] as object; + }); + Received.InOrder(async () => + { + var x = await [|otherSubstitute.Bar()|]; + }); + } + } +}"; + await VerifyDiagnostic(source, _nonVirtualReceivedInOrderSetupSpecificationDescriptor, "Member Bar can not be intercepted. Only interface members and virtual, overriding, and abstract members can be intercepted."); + } + + [Fact] + public async Task ReportsDiagnostics_WhenInvokingNonVirtualPropertyWithoutAssignment() + { + var source = @"using NSubstitute; + +namespace MyNamespace +{ + public class Foo + { + public Foo Nested { get; set; } + + public int Bar { get; set;} + } + + public class FooTests + { + public void Test() + { + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + { + _ = [|substitute.Bar|]; + _ = [|substitute.Nested.Bar|]; + }); + } + } +}"; + await VerifyDiagnostic(source, _nonVirtualReceivedInOrderSetupSpecificationDescriptor, "Member Bar can not be intercepted. Only interface members and virtual, overriding, and abstract members can be intercepted."); + } + + [Fact] + public async Task ReportsDiagnostics_WhenInvokingNonVirtualPropertyWithNonUsedAssignment() + { + var source = @"using NSubstitute; + +namespace MyNamespace +{ + public class Foo + { + public int Bar { get; set;} + } + + public class FooTests + { + public void Test() + { + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + { + var x = [|substitute.Bar|]; + }); + } + } +}"; + await VerifyDiagnostic(source, _nonVirtualReceivedInOrderSetupSpecificationDescriptor, "Member Bar can not be intercepted. Only interface members and virtual, overriding, and abstract members can be intercepted."); + } + + [Fact] + public async Task ReportsDiagnostics_WhenInvokingNonVirtualIndexerWithoutAssignment() + { + var source = @"using NSubstitute; + +namespace MyNamespace +{ + public class Foo + { + public Foo Nested { get; set; } + + public int this[int x] { get => 1; } + } + + public class FooTests + { + public void Test() + { + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + { + _ = [|substitute[1]|]; + _ = [|substitute.Nested[1]|]; + }); + } + } +}"; + await VerifyDiagnostic(source, _nonVirtualReceivedInOrderSetupSpecificationDescriptor, "Member this[] can not be intercepted. Only interface members and virtual, overriding, and abstract members can be intercepted."); + } + + [Fact] + public async Task ReportsDiagnostics_WhenInvokingNonVirtualIndexerWithNonUsedAssignment() + { + var source = @"using NSubstitute; + +namespace MyNamespace +{ + public class Foo + { + public int this[int x] { get => 1; } + } + + public class FooTests + { + public void Test() + { + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + { + var x = [|substitute[1]|]; + }); + } + } +}"; + await VerifyDiagnostic(source, _nonVirtualReceivedInOrderSetupSpecificationDescriptor, "Member this[] can not be intercepted. Only interface members and virtual, overriding, and abstract members can be intercepted."); + } + + [Fact] + public async Task ReportsNoDiagnostics_WhenSubscribingToEvent() + { + var source = @"using NSubstitute; +using System; +namespace MyNamespace +{ + public class Foo + { + public event Action SomeEvent; + public int Bar() + { + return 2; + } + } + + public class FooTests + { + public void Test() + { + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => substitute.SomeEvent += Arg.Any()); + } + } +}"; + await VerifyNoDiagnostic(source); + } + + [Fact] + public async Task ReportsDiagnostics_WhenNonVirtualMethodUsedAsPartOfExpression_WithoutAssignment() + { + var source = @"using NSubstitute; +namespace MyNamespace +{ + public class Foo + { + public int Bar() + { + return 2; + } + } + + public class FooTests + { + public void Test() + { + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + { + var a = [|substitute.Bar()|] + [|substitute.Bar()|]; + var b = [|substitute.Bar()|] - [|substitute.Bar()|]; + }); + } + } +}"; + await VerifyDiagnostic(source, _nonVirtualReceivedInOrderSetupSpecificationDescriptor, "Member Bar can not be intercepted. Only interface members and virtual, overriding, and abstract members can be intercepted."); + } + + [Fact] + public async Task ReportsNoDiagnostics_WhenNonVirtualMethodUsedAsPartOfExpression_WithAssignment() + { + var source = @"using NSubstitute; +namespace MyNamespace +{ + public class Foo + { + public int Bar() + { + return 2; + } + } + + public class FooTests + { + public void Test() + { + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + { + var a = substitute.Bar() + substitute.Bar(); + var b = substitute.Bar() - substitute.Bar(); + var aa = a; + var bb = b; + }); + } + } +}"; + await VerifyNoDiagnostic(source); + } + + [Fact] + public async Task ReportsNoDiagnostics_WhenInvokingNonVirtualMethodWithUsedAssignment() + { + var source = @"using NSubstitute; +using System.Threading.Tasks; +namespace MyNamespace +{ + public class Foo + { + public int Bar() + { + return 2; + } + } + + public class FooBar + { + public Task Bar() + { + return Task.FromResult(1); + } + } + + public class FooTests + { + public void Test() + { + var substitute = NSubstitute.Substitute.For(); + var otherSubstitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + { + var a = substitute.Bar(); + var b = (int)substitute.Bar(); + var c = substitute.Bar() as object; + var aa = a; + var bb = b; + var cc = c; + }); + Received.InOrder(async () => + { + var a = await otherSubstitute.Bar(); + var aa = a; + }); + } + } +}"; + await VerifyNoDiagnostic(source); + } + + [Fact] + public async Task ReportsNoDiagnostics_WhenInvokingNonVirtualPropertyWithUsedAssignment() + { + var source = @"using NSubstitute; + +namespace MyNamespace +{ + public class Foo + { + public int Bar { get; set; } + } + + public class FooTests + { + public void Test() + { + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + { + var x = substitute.Bar; + var y = x; + }); + } + } +}"; + await VerifyNoDiagnostic(source); + } + + [Fact] + public async Task ReportsNoDiagnostics_WhenInvokingNonVirtualIndexerWithUsedAssignment() + { + var source = @"using NSubstitute; + +namespace MyNamespace +{ + public class Foo + { + public int this[int x] { get => 1; } + } + + public class FooTests + { + public void Test() + { + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + { + var x = substitute[1]; + var y = x; + }); + } + } +}"; + await VerifyNoDiagnostic(source); + } + + [Fact] + public async Task ReportsNoDiagnostics_WhenNonVirtualMethodIsCalledAsArgument() + { + var source = @"using NSubstitute; +using System.Threading.Tasks; +namespace MyNamespace +{ + public class Foo + { + public virtual int Bar(object x) + { + return 2; + } + + public int FooBar() + { + return 1; + } + } + + public class FooBar + { + public Task Bar() + { + return Task.FromResult(1); + } + } + + public class FooTests + { + public void Test() + { + var substitute = NSubstitute.Substitute.For(); + var otherSubstitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + { + var local = 1; + var x = substitute.Bar(substitute.FooBar()); + substitute.Bar(substitute.FooBar()); + substitute.Bar((int)substitute.FooBar()); + substitute.Bar(substitute.FooBar() as object); + substitute.Bar(local); + substitute.Bar(1); + }); + Received.InOrder(async () => + { + substitute.Bar(await otherSubstitute.Bar()); + }); + } + } +}"; + await VerifyNoDiagnostic(source); + } + + [Fact] + public async Task ReportsNoDiagnostics_WhenNonVirtualPropertyIsCalledAsArgument() + { + var source = @"using NSubstitute; + +namespace MyNamespace +{ + public class Foo + { + public virtual int Bar(int x) + { + return 2; + } + + public int FooBar { get; } + } + + public class FooTests + { + public void Test() + { + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + { + var x = substitute.Bar(substitute.FooBar); + substitute.Bar(substitute.FooBar); + }); + } + } +}"; + await VerifyNoDiagnostic(source); + } + + [Fact] + public async Task ReportsNoDiagnostics_WhenNonVirtualIndexerIsCalledAsArgument() + { + var source = @"using NSubstitute; + +namespace MyNamespace +{ + public class Foo + { + public virtual int Bar(int x) + { + return 2; + } + + public int this[int x] { get { return 1; } } + } + + public class FooTests + { + public void Test() + { + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + { + var x = substitute.Bar(substitute[1]); + substitute.Bar(substitute[1]); + }); + } + } +}"; + await VerifyNoDiagnostic(source); + } + + [Fact] + public async Task ReportsNoDiagnostics_WhenInvokingProtectedInternalVirtualMember() + { + var source = @"using NSubstitute; + +namespace MyNamespace +{ + public class Foo + { + protected internal virtual int Bar { get; } + + protected internal virtual int FooBar() + { + return 1; + } + + protected internal virtual int this[int x] + { + get { return 1; } + } + } + + public class FooTests + { + public void Test() + { + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + { + substitute.FooBar(); + var x = substitute.Bar; + var y = substitute[1]; + }); + } + } +}"; + await VerifyNoDiagnostic(source); + } + + [Fact] + public async Task ReportsNoDiagnostics_WhenInvokingVirtualMember() + { + var source = @"using NSubstitute; + +namespace MyNamespace +{ + public class Foo + { + protected internal virtual int Bar { get; } + + protected internal virtual int FooBar() + { + return 1; + } + + protected internal virtual int this[int x] + { + get { return 1; } + } + } + + public class FooTests + { + public void Test() + { + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + { + substitute.FooBar(); + var x = substitute.Bar; + var y = substitute[1]; + }); + } + } +}"; + await VerifyNoDiagnostic(source); + } + + [Fact] + public async Task ReportsDiagnostics_WhenInvokingInternalVirtualMember_AndInternalsVisibleToNotApplied() + { + var source = @"using NSubstitute; + +namespace MyNamespace +{ + public class Foo + { + internal virtual int Bar { get; } + + internal virtual int FooBar() + { + return 1; + } + + internal virtual int this[int x] + { + get { return 1; } + } + } + + public class FooTests + { + public void Test() + { + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + { + [|substitute.FooBar()|]; + var x = [|substitute.Bar|]; + var y = [|substitute[1]|]; + }); + } + } +}"; + + var textParserResult = TextParser.GetSpans(source); + + var diagnosticMessages = new[] + { + "Internal member FooBar can not be intercepted without InternalsVisibleToAttribute.", + "Internal member Bar can not be intercepted without InternalsVisibleToAttribute.", + "Internal member this[] can not be intercepted without InternalsVisibleToAttribute." + }; + + var diagnostics = textParserResult.Spans.Select((span, idx) => CreateDiagnostic(_internalSetupSpecificationDescriptor.OverrideMessage(diagnosticMessages[idx]), span)).ToArray(); + + await VerifyDiagnostic(textParserResult.Text, diagnostics); + } + + [Fact] + public async Task ReportsNoDiagnostics_WhenInvokingInternalVirtualMember_AndInternalsVisibleToApplied() + { + var source = @"using System; +using NSubstitute; +using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo(""OtherFirstAssembly"")] +[assembly: InternalsVisibleTo(""DynamicProxyGenAssembly2"")] +[assembly: InternalsVisibleTo(""OtherSecondAssembly"")] + +namespace MyNamespace +{ + public class Foo + { + internal virtual int Bar { get; } + + internal virtual int FooBar() + { + return 1; + } + + internal virtual int this[int x] + { + get { return 1; } + } + } + + public class FooTests + { + public void Test() + { + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + { + substitute.FooBar(); + var x = substitute.Bar; + var y = substitute[1]; + }); + } + } +}"; + + await VerifyNoDiagnostic(source); + } + + [Fact] + public async Task ReportsNoDiagnosticsForSuppressedMember_WhenSuppressingNonVirtualMethod() + { + Settings = AnalyzersSettings.CreateWithSuppressions("M:MyNamespace.Foo.Bar(System.Int32,System.Int32)", _nonVirtualReceivedInOrderSetupSpecificationDescriptor.Id); + + var source = @"using System; +using NSubstitute; + +namespace MyNamespace +{ + public class Foo + { + public int Bar(int x) + { + return 1; + } + + public int Bar(int x, int y) + { + return 2; + } + + public int Bar(Action x) + { + return 1; + } + + } + + public class FooTests + { + public void Test() + { + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + { + substitute.Bar(1, 1); + [|substitute.Bar(1)|]; + }); + } + } +}"; + + await VerifyDiagnostic(source, _nonVirtualReceivedInOrderSetupSpecificationDescriptor, "Member Bar can not be intercepted. Only interface members and virtual, overriding, and abstract members can be intercepted."); + } + + [Fact] + public async Task ReportsNoDiagnostics_WhenAccessingVirtualMemberViaNonVirtualAccessor() + { + var source = @"using System; +using NSubstitute; + +namespace MyNamespace +{ + public class Foo + { + public Foo NestedFoo { get; set; } + + public Foo NestedFooMethod() + { + return null; + } + + public Foo this[int x] + { + get { return null; } + } + + public virtual int Bar(int x) + { + return 1; + } + } + + public class FooTests + { + public void Test() + { + var substitute = NSubstitute.Substitute.For(); + Received.InOrder(() => + { + substitute.NestedFoo.Bar(1); + substitute.NestedFooMethod().Bar(1); + substitute[1].Bar(1); + }); + } + } +}"; + + await VerifyNoDiagnostic(source); + } + } +} \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.Shared/DiagnosticAnalyzers/INonSubstitutableMemberReceivedInOrderDiagnosticVerifier.cs b/tests/NSubstitute.Analyzers.Tests.Shared/DiagnosticAnalyzers/INonSubstitutableMemberReceivedInOrderDiagnosticVerifier.cs new file mode 100644 index 00000000..358f8c2f --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.Shared/DiagnosticAnalyzers/INonSubstitutableMemberReceivedInOrderDiagnosticVerifier.cs @@ -0,0 +1,49 @@ +using System.Threading.Tasks; + +namespace NSubstitute.Analyzers.Tests.Shared.DiagnosticAnalyzers +{ + public interface INonSubstitutableMemberReceivedInOrderDiagnosticVerifier + { + Task ReportsDiagnostics_WhenInvokingNonVirtualMethodWithoutAssignment(); + + Task ReportsDiagnostics_WhenInvokingNonVirtualMethodWithNonUsedAssignment(); + + Task ReportsDiagnostics_WhenInvokingNonVirtualPropertyWithoutAssignment(); + + Task ReportsDiagnostics_WhenInvokingNonVirtualPropertyWithNonUsedAssignment(); + + Task ReportsDiagnostics_WhenInvokingNonVirtualIndexerWithoutAssignment(); + + Task ReportsDiagnostics_WhenInvokingNonVirtualIndexerWithNonUsedAssignment(); + + Task ReportsNoDiagnostics_WhenSubscribingToEvent(); + + Task ReportsNoDiagnostics_WhenInvokingNonVirtualMethodWithUsedAssignment(); + + Task ReportsNoDiagnostics_WhenInvokingNonVirtualPropertyWithUsedAssignment(); + + Task ReportsNoDiagnostics_WhenInvokingNonVirtualIndexerWithUsedAssignment(); + + Task ReportsNoDiagnostics_WhenNonVirtualMethodIsCalledAsArgument(); + + Task ReportsNoDiagnostics_WhenNonVirtualPropertyIsCalledAsArgument(); + + Task ReportsNoDiagnostics_WhenNonVirtualIndexerIsCalledAsArgument(); + + Task ReportsNoDiagnostics_WhenInvokingProtectedInternalVirtualMember(); + + Task ReportsNoDiagnostics_WhenInvokingVirtualMember(); + + Task ReportsDiagnostics_WhenInvokingInternalVirtualMember_AndInternalsVisibleToNotApplied(); + + Task ReportsNoDiagnostics_WhenInvokingInternalVirtualMember_AndInternalsVisibleToApplied(); + + Task ReportsNoDiagnosticsForSuppressedMember_WhenSuppressingNonVirtualMethod(); + + Task ReportsDiagnostics_WhenNonVirtualMethodUsedAsPartOfExpression_WithoutAssignment(); + + Task ReportsNoDiagnostics_WhenNonVirtualMethodUsedAsPartOfExpression_WithAssignment(); + + Task ReportsNoDiagnostics_WhenAccessingVirtualMemberViaNonVirtualAccessor(); + } +} \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.VisualBasic/CodeFixProvidersTests/NonSubstitutableMemberReceivedInOrderSuppressDiagnosticsCodeFixProviderTests/NonSubstitutableMemberReceivedInOrderSuppressDiagnosticsCodeFixVerifier.cs b/tests/NSubstitute.Analyzers.Tests.VisualBasic/CodeFixProvidersTests/NonSubstitutableMemberReceivedInOrderSuppressDiagnosticsCodeFixProviderTests/NonSubstitutableMemberReceivedInOrderSuppressDiagnosticsCodeFixVerifier.cs new file mode 100644 index 00000000..f895a53f --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.VisualBasic/CodeFixProvidersTests/NonSubstitutableMemberReceivedInOrderSuppressDiagnosticsCodeFixProviderTests/NonSubstitutableMemberReceivedInOrderSuppressDiagnosticsCodeFixVerifier.cs @@ -0,0 +1,270 @@ +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Diagnostics; +using NSubstitute.Analyzers.Shared; +using NSubstitute.Analyzers.Tests.Shared.CodeFixProviders; +using NSubstitute.Analyzers.VisualBasic.CodeFixProviders; +using NSubstitute.Analyzers.VisualBasic.DiagnosticAnalyzers; +using Xunit; + +namespace NSubstitute.Analyzers.Tests.VisualBasic.CodeFixProvidersTests.NonSubstitutableMemberReceivedInOrderSuppressDiagnosticsCodeFixProviderTests +{ + public class NonSubstitutableMemberReceivedInOrderSuppressDiagnosticsCodeFixVerifier : VisualBasicSuppressDiagnosticSettingsVerifier, INonSubstitutableMemberSuppressDiagnosticsCodeFixVerifier + { + protected override CodeFixProvider CodeFixProvider { get; } = new NonSubstitutableMemberSuppressDiagnosticsCodeFixProvider(); + + protected override DiagnosticAnalyzer DiagnosticAnalyzer { get; } = new NonSubstitutableMemberReceivedInOrderAnalyzer(); + + [Fact] + public async Task SuppressesDiagnosticsInSettings_WhenSettingValueForNonVirtualMethod() + { + var source = @"Imports NSubstitute + +Namespace MyNamespace + + Public Class Foo + + Public Function Bar() As Integer + Return 2 + End Function + End Class + + Public Class FooTests + + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of Foo)() + NSubstitute.Received.InOrder(Sub() substitute.Bar()) + End Sub + End Class +End Namespace +"; + await VerifySuppressionSettings(source, "M:MyNamespace.Foo.Bar~System.Int32", DiagnosticIdentifiers.NonVirtualReceivedInOrderSetupSpecification); + } + + [Fact] + public async Task SuppressesDiagnosticsInSettings_WhenSettingValueForStaticMethod() + { + var source = @"Imports NSubstitute + +Namespace MyNamespace + + Public Class Foo + + Public Shared Function Bar() As Integer + Return 2 + End Function + End Class + + Public Class FooTests + + Public Sub Test() + NSubstitute.Received.InOrder(Sub() Foo.Bar()) + End Sub + End Class +End Namespace +"; + await VerifySuppressionSettings(source, "M:MyNamespace.Foo.Bar~System.Int32", DiagnosticIdentifiers.NonVirtualReceivedInOrderSetupSpecification); + } + + [Fact] + public async Task SuppressesDiagnosticsInSettings_WhenSettingValueForExtensionMethod() + { + var source = @"Imports NSubstitute +Imports System.Runtime.CompilerServices + +Namespace MyNamespace + Public Class FooTests + Public Sub Test() + Bar = NSubstitute.Substitute.[For](Of IBar)() + Dim substitute = NSubstitute.Substitute.[For](Of IFoo)() + NSubstitute.Received.InOrder(Sub() substitute.GetBar()) + End Sub + End Class + + Module MyExtensions + Public Property Bar As IBar + + + Function GetBar(ByVal foo As IFoo) As Integer + Return Bar.Foo() + Return 1 + End Function + + End Module + + Interface IBar + Function Foo() As Integer + End Interface + + Interface IFoo + Function Bar() As Integer + End Interface +End Namespace"; + + await VerifySuppressionSettings(source, "M:MyNamespace.MyExtensions.GetBar(MyNamespace.IFoo)~System.Int32", DiagnosticIdentifiers.NonVirtualReceivedInOrderSetupSpecification); + } + + [Fact] + public async Task SuppressesDiagnosticsInSettings_WhenSettingValueForSealedOverrideMethod() + { + var source = @"Imports NSubstitute + +Namespace MyNamespace + + Public Class Foo + + Public Overridable Function Bar() As Integer + Return 2 + End Function + End Class + + Public Class Foo2 + Inherits Foo + + Public NotOverridable Overrides Function Bar() As Integer + Return 1 + End Function + End Class + + Public Class FooTests + + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of Foo2)() + NSubstitute.Received.InOrder(Function() + Dim x = substitute.Bar() + Return 1 + End Function) + End Sub + End Class +End Namespace +"; + await VerifySuppressionSettings(source, "M:MyNamespace.Foo2.Bar~System.Int32", DiagnosticIdentifiers.NonVirtualReceivedInOrderSetupSpecification); + } + + [Fact] + public async Task SuppressesDiagnosticsInSettings_WhenSettingValueForNonVirtualProperty() + { + var source = @"Imports NSubstitute + +Namespace MyNamespace + + Public Class Foo + + Public ReadOnly Property Bar As Integer + Get + End Get + End Property + End Class + + Public Class FooTests + + Public Sub Test() + Dim substitute = NSubstitute.Substitute.For(Of Foo) + NSubstitute.Received.InOrder(Function() + Dim x = substitute.Bar + Return 1 + End Function) + End Sub + End Class +End Namespace"; + + await VerifySuppressionSettings(source, "P:MyNamespace.Foo.Bar", DiagnosticIdentifiers.NonVirtualReceivedInOrderSetupSpecification); + } + + [Fact] + public async Task SuppressesDiagnosticsInSettings_WhenSettingValueForNonVirtualIndexer() + { + var source = @"Imports System +Imports NSubstitute + +Namespace MyNamespace + + Public Class Foo + + Public Default ReadOnly Property Item(ByVal x As Integer) As Integer + Get + Throw New NotImplementedException + End Get + End Property + End Class + + Public Class FooTests + + Public Sub Test() + Dim substitute = NSubstitute.Substitute.For(Of Foo) + NSubstitute.Received.InOrder(Function() + Dim x = substitute(1) + Return 1 + End Function) + End Sub + End Class +End Namespace"; + + await VerifySuppressionSettings(source, "P:MyNamespace.Foo.Item(System.Int32)", DiagnosticIdentifiers.NonVirtualReceivedInOrderSetupSpecification); + } + + [Fact] + public async Task SuppressesDiagnosticsInSettingsForClass_WhenSettingsValueForNonVirtualMember_AndSelectingClassSuppression() + { + var source = @"Imports System +Imports NSubstitute + +Namespace MyNamespace + + Public Class Foo + + Public Default ReadOnly Property Item(ByVal x As Integer) As Integer + Get + Return 1 + End Get + End Property + End Class + + Public Class FooTests + + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of Foo)() + NSubstitute.Received.InOrder(Function() + Dim x = substitute(1) + Return 1 + End Function) + End Sub + End Class +End Namespace"; + + await VerifySuppressionSettings(source, "T:MyNamespace.Foo", DiagnosticIdentifiers.NonVirtualReceivedInOrderSetupSpecification, 1); + } + + [Fact] + public async Task SuppressesDiagnosticsInSettingsForNamespace_WhenSettingsValueForNonVirtualMember_AndSelectingNamespaceSuppression() + { + var source = @"Imports System +Imports NSubstitute + +Namespace MyNamespace + + Public Class Foo + + Public Default ReadOnly Property Item(ByVal x As Integer) As Integer + Get + Throw New NotImplementedException + End Get + End Property + End Class + + Public Class FooTests + + Public Sub Test() + Dim substitute = NSubstitute.Substitute.For(Of Foo) + NSubstitute.Received.InOrder(Function() + Dim x = substitute(1) + Return 1 + End Function) + End Sub + End Class +End Namespace"; + + await VerifySuppressionSettings(source, "N:MyNamespace", DiagnosticIdentifiers.NonVirtualReceivedInOrderSetupSpecification, 2); + } + } +} \ No newline at end of file diff --git a/tests/NSubstitute.Analyzers.Tests.VisualBasic/DiagnosticAnalyzersTests/NonSubstitutableMemberReceivedInOrderAnalyzerTests/NonSubstitutableMemberReceivedInOrderDiagnosticVerifier.cs b/tests/NSubstitute.Analyzers.Tests.VisualBasic/DiagnosticAnalyzersTests/NonSubstitutableMemberReceivedInOrderAnalyzerTests/NonSubstitutableMemberReceivedInOrderDiagnosticVerifier.cs new file mode 100644 index 00000000..5ff22498 --- /dev/null +++ b/tests/NSubstitute.Analyzers.Tests.VisualBasic/DiagnosticAnalyzersTests/NonSubstitutableMemberReceivedInOrderAnalyzerTests/NonSubstitutableMemberReceivedInOrderDiagnosticVerifier.cs @@ -0,0 +1,753 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using NSubstitute.Analyzers.Shared; +using NSubstitute.Analyzers.Shared.Settings; +using NSubstitute.Analyzers.Shared.TinyJson; +using NSubstitute.Analyzers.Tests.Shared.DiagnosticAnalyzers; +using NSubstitute.Analyzers.Tests.Shared.Extensions; +using NSubstitute.Analyzers.VisualBasic; +using NSubstitute.Analyzers.VisualBasic.DiagnosticAnalyzers; +using Xunit; + +namespace NSubstitute.Analyzers.Tests.VisualBasic.DiagnosticAnalyzersTests.NonSubstitutableMemberReceivedInOrderAnalyzerTests +{ + public class NonSubstitutableMemberReceivedInOrderDiagnosticVerifier : VisualBasicDiagnosticVerifier, INonSubstitutableMemberReceivedInOrderDiagnosticVerifier + { + internal AnalyzersSettings Settings { get; set; } + + protected override DiagnosticAnalyzer DiagnosticAnalyzer { get; } = new NonSubstitutableMemberReceivedInOrderAnalyzer(); + + private readonly DiagnosticDescriptor _internalSetupSpecificationDescriptor = DiagnosticDescriptors.InternalSetupSpecification; + + private readonly DiagnosticDescriptor _nonVirtualReceivedInOrderSetupSpecificationDescriptor = DiagnosticDescriptors.NonVirtualReceivedInOrderSetupSpecification; + + protected override string AnalyzerSettings => Settings != null ? Json.Encode(Settings) : null; + + [Fact] + public async Task ReportsDiagnostics_WhenInvokingNonVirtualMethodWithoutAssignment() + { + var source = @"Imports NSubstitute +Imports System.Threading.Tasks +Namespace MyNamespace + Public Class Foo + Public Property Nested As Foo + + Public Function Bar() As Integer + Return 2 + End Function + End Class + + Public Class FooBar + Public Property Nested As FooBar + + Public Function Bar() As Task(Of Integer) + Return Task.FromResult(1) + End Function + End Class + + Public Class FooTests + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of Foo)() + Dim otherSubstitute = NSubstitute.Substitute.[For](Of FooBar)() + + NSubstitute.Received.InOrder(Function() + [|substitute.Bar()|] + End Function) + NSubstitute.Received.InOrder(Function() [|substitute.Bar()|]) + NSubstitute.Received.InOrder(Sub() [|substitute.Bar()|]) + NSubstitute.Received.InOrder(Function() + [|substitute.Nested.Bar()|] + End Function) + NSubstitute.Received.InOrder(Function() [|substitute.Nested.Bar()|]) + NSubstitute.Received.InOrder(Sub() [|substitute.Nested.Bar()|]) + NSubstitute.Received.InOrder(Async Function() Await [|otherSubstitute.Bar()|]) + NSubstitute.Received.InOrder(Async Sub() Await [|otherSubstitute.Bar()|]) + NSubstitute.Received.InOrder(Async Function() Await [|otherSubstitute.Nested.Bar()|]) + NSubstitute.Received.InOrder(Async Sub() Await [|otherSubstitute.Nested.Bar()|]) + End Sub + End Class +End Namespace +"; + await VerifyDiagnostic(source, _nonVirtualReceivedInOrderSetupSpecificationDescriptor, "Member Bar can not be intercepted. Only interface members and overrideable, overriding, and must override members can be intercepted."); + } + + [Fact] + public async Task ReportsDiagnostics_WhenInvokingNonVirtualMethodWithNonUsedAssignment() + { + var source = @"Imports NSubstitute +Imports System.Threading.Tasks +Namespace MyNamespace + Public Class Foo + Public Function Bar() As Integer + Return 2 + End Function + + Public Function Bar(ByVal x As Integer) As Foo + Return Nothing + End Function + End Class + + Public Class FooBar + Public Function Bar() As Task(Of Integer) + Return Task.FromResult(1) + End Function + End Class + + Public Class FooTests + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of Foo)() + Dim otherSubstitute = NSubstitute.Substitute.[For](Of FooBar)() + NSubstitute.Received.InOrder(Function() + [|substitute.Bar()|] + Dim y = [|substitute.Bar()|] + Dim z = CInt([|substitute.Bar()|]) + Dim zz = TryCast([|substitute.Bar()|], Object) + End Function) + NSubstitute.Received.InOrder(Async Function() + Await [|otherSubstitute.Bar()|] + Dim y = Await [|otherSubstitute.Bar()|] + End Function) + End Sub + End Class +End Namespace"; + + await VerifyDiagnostic(source, _nonVirtualReceivedInOrderSetupSpecificationDescriptor, "Member Bar can not be intercepted. Only interface members and overrideable, overriding, and must override members can be intercepted."); + } + + [Fact] + public async Task ReportsDiagnostics_WhenInvokingNonVirtualPropertyWithoutAssignment() + { + var source = @"Imports NSubstitute + +Namespace MyNamespace + Public Class Foo + Public Property Nested As Foo + + Public Property Bar As Integer + End Class + + Public Class FooTests + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of Foo)() + NSubstitute.Received.InOrder(Function() + Dim x = [|substitute.Bar|] + End Function) + NSubstitute.Received.InOrder(Function() + Dim x = [|substitute.Nested.Bar|] + End Function) + End Sub + End Class +End Namespace +"; + await VerifyDiagnostic(source, _nonVirtualReceivedInOrderSetupSpecificationDescriptor, "Member Bar can not be intercepted. Only interface members and overrideable, overriding, and must override members can be intercepted."); + } + + [Fact] + public async Task ReportsDiagnostics_WhenInvokingNonVirtualPropertyWithNonUsedAssignment() + { + var source = @"Imports NSubstitute + +Namespace MyNamespace + Public Class Foo + Public Property Bar As Integer + End Class + + Public Class FooTests + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of Foo)() + NSubstitute.Received.InOrder(Function() + Dim x = [|substitute.Bar|] + End Function) + End Sub + End Class +End Namespace +"; + await VerifyDiagnostic(source, _nonVirtualReceivedInOrderSetupSpecificationDescriptor, "Member Bar can not be intercepted. Only interface members and overrideable, overriding, and must override members can be intercepted."); + } + + [Fact] + public async Task ReportsDiagnostics_WhenInvokingNonVirtualIndexerWithoutAssignment() + { + var source = @"Imports NSubstitute + +Namespace MyNamespace + Public Class Foo + Public Property Nested As Foo + + Default Public ReadOnly Property Item(ByVal x As Integer) As Integer + Get + Return 1 + End Get + End Property + End Class + + Public Class FooTests + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of Foo)() + NSubstitute.Received.InOrder(Function() + Dim x = [|substitute(1)|] + End Function) + NSubstitute.Received.InOrder(Function() + Dim x = [|substitute.Nested(1)|] + End Function) + End Sub + End Class +End Namespace +"; + await VerifyDiagnostic(source, _nonVirtualReceivedInOrderSetupSpecificationDescriptor, "Member Item can not be intercepted. Only interface members and overrideable, overriding, and must override members can be intercepted."); + } + + [Fact] + public async Task ReportsDiagnostics_WhenInvokingNonVirtualIndexerWithNonUsedAssignment() + { + var source = @"Imports NSubstitute + +Namespace MyNamespace + Public Class Foo + Default Public ReadOnly Property Item(ByVal x As Integer) As Integer + Get + Return 1 + End Get + End Property + End Class + + Public Class FooTests + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of Foo)() + NSubstitute.Received.InOrder(Function() + Dim x = [|substitute(1)|] + End Function) + End Sub + End Class +End Namespace +"; + await VerifyDiagnostic(source, _nonVirtualReceivedInOrderSetupSpecificationDescriptor, "Member Item can not be intercepted. Only interface members and overrideable, overriding, and must override members can be intercepted."); + } + + [Fact] + public async Task ReportsNoDiagnostics_WhenSubscribingToEvent() + { + var source = @"Imports NSubstitute +Imports System + +Namespace MyNamespace + Public Class Foo + Public Event SomeEvent As Action + + Public Function Bar() As Integer + Return 2 + End Function + End Class + + Public Class FooTests + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of Foo)() + NSubstitute.Received.InOrder(Function() + AddHandler substitute.SomeEvent, Arg.Any(Of Action)() + End Function) + End Sub + + End Class +End Namespace +"; + await VerifyNoDiagnostic(source); + } + + [Fact] + public async Task ReportsNoDiagnostics_WhenInvokingNonVirtualMethodWithUsedAssignment() + { + var source = @"Imports NSubstitute +Imports System.Threading.Tasks +Namespace MyNamespace + Public Class Foo + Public Function Bar() As Integer + Return 2 + End Function + End Class + + Public Class FooBar + Public Function Bar() As Task(Of Integer) + Return Task.FromResult(1) + End Function + End Class + + Public Class FooTests + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of Foo)() + Dim otherSubstitute = NSubstitute.Substitute.[For](Of FooBar)() + NSubstitute.Received.InOrder(Function() + Dim x = substitute.Bar() + Dim y = x + End Function) + NSubstitute.Received.InOrder(Async Function() + Dim x = Await otherSubstitute.Bar() + Dim y = x + End Function) + End Sub + End Class +End Namespace +"; + await VerifyNoDiagnostic(source); + } + + [Fact] + public async Task ReportsNoDiagnostics_WhenInvokingNonVirtualPropertyWithUsedAssignment() + { + var source = @"Imports NSubstitute + +Namespace MyNamespace + Public Class Foo + Public Property Bar As Integer + End Class + + Public Class FooTests + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of Foo)() + NSubstitute.Received.InOrder(Function() + Dim x = substitute.Bar + Dim y = x + End Function) + End Sub + End Class +End Namespace +"; + await VerifyNoDiagnostic(source); + } + + [Fact] + public async Task ReportsNoDiagnostics_WhenInvokingNonVirtualIndexerWithUsedAssignment() + { + var source = @"Imports NSubstitute + +Namespace MyNamespace + Public Class Foo + Default Public ReadOnly Property Item(ByVal x As Integer) As Integer + Get + Return 1 + End Get + End Property + End Class + + Public Class FooTests + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of Foo)() + NSubstitute.Received.InOrder(Function() + Dim a = substitute(1) + Dim b = TryCast(substitute(1), Object) + Dim c = CInt(substitute(1)) + Dim d = CType(substitute(1), Integer) + Dim e = DirectCast(substitute(1), Integer) + Dim aa = a + Dim bb = b + Dim cc = c + Dim dd = d + Dim ee = e + End Function) + End Sub + End Class +End Namespace +"; + await VerifyNoDiagnostic(source); + } + + [Fact] + public async Task ReportsNoDiagnostics_WhenNonVirtualMethodIsCalledAsArgument() + { + var source = @"Imports NSubstitute +Imports System.Threading.Tasks +Namespace MyNamespace + Public Class Foo + Public Overridable Function Bar(ByVal x As Object) As Integer + Return 2 + End Function + + Public Function FooBar() As Object + Return 1 + End Function + End Class + + Public Class FooBar + Public Function Bar() As Task(Of Integer) + Return Task.FromResult(1) + End Function + End Class + + Public Class FooTests + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of Foo)() + Dim otherSubstitute = NSubstitute.Substitute.[For](Of FooBar)() + NSubstitute.Received.InOrder(Function() + Dim local = 1 + Dim x = substitute.Bar(substitute.FooBar()) + substitute.Bar(substitute.FooBar()) + substitute.Bar(CInt(substitute.FooBar())) + substitute.Bar(TryCast(substitute.FooBar(), Object)) + substitute.Bar(DirectCast(substitute.FooBar(), Integer)) + substitute.Bar(CType(substitute.FooBar(), Integer)) + substitute.Bar(local) + substitute.Bar(1) + End Function) + NSubstitute.Received.InOrder(Async Function() + Dim x = substitute.Bar(Await otherSubstitute.Bar()) + End Function) + End Sub + End Class +End Namespace +"; + await VerifyNoDiagnostic(source); + } + + [Fact] + public async Task ReportsNoDiagnostics_WhenNonVirtualPropertyIsCalledAsArgument() + { + var source = @"Imports NSubstitute + +Namespace MyNamespace + Public Class Foo + Public Overridable Function Bar(ByVal x As Integer) As Integer + Return 2 + End Function + + Public ReadOnly Property FooBar As Integer + End Class + + Public Class FooTests + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of Foo)() + NSubstitute.Received.InOrder(Function() + Dim x = substitute.Bar(substitute.FooBar) + substitute.Bar(substitute.FooBar) + End Function) + End Sub + End Class +End Namespace +"; + await VerifyNoDiagnostic(source); + } + + [Fact] + public async Task ReportsNoDiagnostics_WhenNonVirtualIndexerIsCalledAsArgument() + { + var source = @"Imports NSubstitute + +Namespace MyNamespace + Public Class Foo + Public Overridable Function Bar(ByVal x As Integer) As Integer + Return 2 + End Function + + Default Public ReadOnly Property Item(ByVal x As Integer) As Integer + Get + Return 1 + End Get + End Property + End Class + + Public Class FooTests + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of Foo)() + NSubstitute.Received.InOrder(Function() + Dim x = substitute.Bar(substitute(1)) + substitute.Bar(substitute(1)) + End Function) + End Sub + End Class +End Namespace +"; + await VerifyNoDiagnostic(source); + } + + [Fact] + public async Task ReportsNoDiagnostics_WhenInvokingProtectedInternalVirtualMember() + { + var source = @"Imports NSubstitute + +Namespace MyNamespace + Public Class Foo + Protected Friend Overridable ReadOnly Property Bar As Integer + + Protected Friend Overridable Function FooBar() As Integer + Return 1 + End Function + + Default Protected Friend Overridable ReadOnly Property Item(ByVal x As Integer) As Integer + Get + Return 1 + End Get + End Property + End Class + + Public Class FooTests + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of Foo)() + NSubstitute.Received.InOrder(Function() + substitute.FooBar() + Dim x = substitute.Bar + Dim y = substitute(1) + End Function) + End Sub + End Class +End Namespace +"; + await VerifyNoDiagnostic(source); + } + + [Fact] + public async Task ReportsNoDiagnostics_WhenInvokingVirtualMember() + { + var source = @"Imports NSubstitute + +Namespace MyNamespace + Public Class Foo + Overridable ReadOnly Property Bar As Integer + + Overridable Function FooBar() As Integer + Return 1 + End Function + + Default Protected Friend Overridable ReadOnly Property Item(ByVal x As Integer) As Integer + Get + Return 1 + End Get + End Property + End Class + + Public Class FooTests + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of Foo)() + NSubstitute.Received.InOrder(Function() + substitute.FooBar() + Dim x = substitute.Bar + Dim y = substitute(1) + End Function) + NSubstitute.Received.InOrder(Function() substitute.FooBar()) + NSubstitute.Received.InOrder(Sub() substitute.FooBar()) + End Sub + End Class +End Namespace +"; + await VerifyNoDiagnostic(source); + } + + [Fact] + public async Task ReportsDiagnostics_WhenInvokingInternalVirtualMember_AndInternalsVisibleToNotApplied() + { + var source = @"Imports NSubstitute + +Namespace MyNamespace + Public Class Foo + Friend Overridable ReadOnly Property Bar As Integer + + Friend Overridable Function FooBar() As Integer + Return 1 + End Function + + Default Friend Overridable ReadOnly Property Item(ByVal x As Integer) As Integer + Get + Return 1 + End Get + End Property + End Class + + Public Class FooTests + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of Foo)() + NSubstitute.Received.InOrder(Function() + [|substitute.FooBar()|] + Dim x = [|substitute.Bar|] + Dim y = [|substitute(1)|] + End Function) + End Sub + End Class +End Namespace +"; + + var textParserResult = TextParser.GetSpans(source); + + var diagnosticMessages = new[] + { + "Friend member FooBar can not be intercepted without InternalsVisibleToAttribute.", + "Friend member Bar can not be intercepted without InternalsVisibleToAttribute.", + "Friend member Item can not be intercepted without InternalsVisibleToAttribute." + }; + + var diagnostics = textParserResult.Spans.Select((span, idx) => CreateDiagnostic(_internalSetupSpecificationDescriptor.OverrideMessage(diagnosticMessages[idx]), span)).ToArray(); + + await VerifyDiagnostic(textParserResult.Text, diagnostics); + } + + [Fact] + public async Task ReportsNoDiagnostics_WhenInvokingInternalVirtualMember_AndInternalsVisibleToApplied() + { + var source = @"Imports System +Imports NSubstitute +Imports System.Runtime.CompilerServices + + + + +Namespace MyNamespace + Public Class Foo + Friend Overridable ReadOnly Property Bar As Integer + + Friend Overridable Function FooBar() As Integer + Return 1 + End Function + + Default Friend Overridable ReadOnly Property Item(ByVal x As Integer) As Integer + Get + Return 1 + End Get + End Property + End Class + + Public Class FooTests + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of Foo)() + NSubstitute.Received.InOrder(Function() + substitute.FooBar() + Dim x = substitute.Bar + Dim y = substitute(1) + End Function) + End Sub + End Class +End Namespace"; + + await VerifyNoDiagnostic(source); + } + + [Fact] + public async Task ReportsNoDiagnosticsForSuppressedMember_WhenSuppressingNonVirtualMethod() + { + Settings = AnalyzersSettings.CreateWithSuppressions("M:MyNamespace.Foo.Bar(System.Int32,System.Int32)", _nonVirtualReceivedInOrderSetupSpecificationDescriptor.Id); + + var source = @"Imports System +Imports NSubstitute + +Namespace MyNamespace + Public Class Foo + Public Function Bar(ByVal x As Integer) As Integer + Return 1 + End Function + + Public Function Bar(ByVal x As Integer, ByVal y As Integer) As Integer + Return 2 + End Function + + Public Function Bar(ByVal x As Action) As Integer + Return 1 + End Function + End Class + + Public Class FooTests + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of Foo)() + NSubstitute.Received.InOrder(Function() + substitute.Bar(1, 1) + [|substitute.Bar(1)|] + End Function) + End Sub + End Class +End Namespace"; + + await VerifyDiagnostic(source, _nonVirtualReceivedInOrderSetupSpecificationDescriptor, "Member Bar can not be intercepted. Only interface members and overrideable, overriding, and must override members can be intercepted."); + } + + [Fact] + public async Task ReportsDiagnostics_WhenNonVirtualMethodUsedAsPartOfExpression_WithoutAssignment() + { + var source = @"Imports NSubstitute + +Namespace MyNamespace + Public Class Foo + Public Function Bar() As Integer + Return 2 + End Function + End Class + + Public Class FooTests + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of Foo)() + NSubstitute.Received.InOrder(Function() + Dim a = [|substitute.Bar()|] + [|substitute.Bar()|] + Dim b = [|substitute.Bar()|] - [|substitute.Bar()|] + End Function) + End Sub + End Class +End Namespace +"; + + await VerifyDiagnostic(source, _nonVirtualReceivedInOrderSetupSpecificationDescriptor, "Member Bar can not be intercepted. Only interface members and overrideable, overriding, and must override members can be intercepted."); + } + + [Fact] + public async Task ReportsNoDiagnostics_WhenNonVirtualMethodUsedAsPartOfExpression_WithAssignment() + { + var source = @"Imports NSubstitute + +Namespace MyNamespace + Public Class Foo + Public Function Bar() As Integer + Return 2 + End Function + End Class + + Public Class FooTests + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of Foo)() + NSubstitute.Received.InOrder(Function() + Dim a = substitute.Bar() + substitute.Bar() + Dim b = substitute.Bar() - substitute.Bar() + Dim aa = a + Dim bb = b + End Function) + End Sub + End Class +End Namespace"; + + await VerifyNoDiagnostic(source); + } + + [Fact] + public async Task ReportsNoDiagnostics_WhenAccessingVirtualMemberViaNonVirtualAccessor() + { + var source = @"Imports System +Imports NSubstitute + +Namespace MyNamespace + Public Class Foo + Public Property NestedFoo As Foo + + Public Function NestedFooMethod() As Foo + Return Nothing + End Function + + Default Public ReadOnly Property Item(ByVal x As Integer) As Foo + Get + Return Nothing + End Get + End Property + + Public Overridable Function Bar(ByVal x As Integer) As Integer + Return 1 + End Function + End Class + + Public Class FooTests + Public Sub Test() + Dim substitute = NSubstitute.Substitute.[For](Of Foo)() + NSubstitute.Received.InOrder(Function() + substitute.NestedFoo.Bar(1) + substitute.NestedFooMethod().Bar(1) + substitute(1).Bar(1) + End Function) + End Sub + End Class +End Namespace"; + + await VerifyNoDiagnostic(source); + } + } +} \ No newline at end of file