diff --git a/src/Analyzers/NoMethodsInPropertySetupAnalyzer.cs b/src/Analyzers/NoMethodsInPropertySetupAnalyzer.cs index 17f02afa9..8226da758 100644 --- a/src/Analyzers/NoMethodsInPropertySetupAnalyzer.cs +++ b/src/Analyzers/NoMethodsInPropertySetupAnalyzer.cs @@ -1,3 +1,6 @@ +using System.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + namespace Moq.Analyzers; /// @@ -28,23 +31,70 @@ public override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression); + + context.RegisterCompilationStartAction(RegisterCompilationStartAction); + } + + private static void RegisterCompilationStartAction(CompilationStartAnalysisContext context) + { + MoqKnownSymbols knownSymbols = new(context.Compilation); + + if (!knownSymbols.IsMockReferenced()) + { + return; + } + + ImmutableArray propertySetupMethods = ImmutableArray.CreateRange([ + ..knownSymbols.Mock1SetupGet, + ..knownSymbols.Mock1SetupSet, + ..knownSymbols.Mock1SetupProperty]); + + if (propertySetupMethods.IsEmpty) + { + return; + } + + context.RegisterOperationAction( + operationAnalysisContext => Analyze(operationAnalysisContext, propertySetupMethods), + OperationKind.Invocation); } - private static void Analyze(SyntaxNodeAnalysisContext context) + private static void Analyze(OperationAnalysisContext context, ImmutableArray propertySetupMethods) { - InvocationExpressionSyntax setupGetOrSetInvocation = (InvocationExpressionSyntax)context.Node; + Debug.Assert(context.Operation is IInvocationOperation, "Expected IInvocationOperation"); + + if (context.Operation is not IInvocationOperation invocationOperation) + { + return; + } + + IMethodSymbol targetMethod = invocationOperation.TargetMethod; + if (!targetMethod.IsInstanceOf(propertySetupMethods)) + { + return; + } + + // The lambda argument to SetupGet/SetupSet/SetupProperty contains the mocked member access. + // If the lambda body is an invocation (method call), that is invalid for property setup. + InvocationExpressionSyntax? mockedMethodCall = + (invocationOperation.Syntax as InvocationExpressionSyntax).FindMockedMethodInvocationFromSetupMethod(); - if (setupGetOrSetInvocation.Expression is not MemberAccessExpressionSyntax setupGetOrSetMethod) return; - if (!string.Equals(setupGetOrSetMethod.Name.ToFullString(), "SetupGet", StringComparison.Ordinal) - && !string.Equals(setupGetOrSetMethod.Name.ToFullString(), "SetupSet", StringComparison.Ordinal) - && !string.Equals(setupGetOrSetMethod.Name.ToFullString(), "SetupProperty", StringComparison.Ordinal)) return; + if (mockedMethodCall == null) + { + return; + } - InvocationExpressionSyntax? mockedMethodCall = setupGetOrSetInvocation.FindMockedMethodInvocationFromSetupMethod(); - if (mockedMethodCall == null) return; + SemanticModel? semanticModel = invocationOperation.SemanticModel; + if (semanticModel == null) + { + return; + } - ISymbol? mockedMethodSymbol = context.SemanticModel.GetSymbolInfo(mockedMethodCall, context.CancellationToken).Symbol; - if (mockedMethodSymbol == null) return; + ISymbol? mockedMethodSymbol = semanticModel.GetSymbolInfo(mockedMethodCall, context.CancellationToken).Symbol; + if (mockedMethodSymbol == null) + { + return; + } Diagnostic diagnostic = mockedMethodCall.CreateDiagnostic(Rule, mockedMethodSymbol.Name); context.ReportDiagnostic(diagnostic); diff --git a/src/Common/WellKnown/MoqKnownSymbols.cs b/src/Common/WellKnown/MoqKnownSymbols.cs index c5d24e8da..edfb71f79 100644 --- a/src/Common/WellKnown/MoqKnownSymbols.cs +++ b/src/Common/WellKnown/MoqKnownSymbols.cs @@ -50,6 +50,21 @@ internal MoqKnownSymbols(Compilation compilation) /// internal ImmutableArray Mock1Setup => Mock1?.GetMembers("Setup").OfType().ToImmutableArray() ?? ImmutableArray.Empty; + /// + /// Gets the methods for Moq.Mock{T}.SetupGet. + /// + internal ImmutableArray Mock1SetupGet => Mock1?.GetMembers("SetupGet").OfType().ToImmutableArray() ?? ImmutableArray.Empty; + + /// + /// Gets the methods for Moq.Mock{T}.SetupSet. + /// + internal ImmutableArray Mock1SetupSet => Mock1?.GetMembers("SetupSet").OfType().ToImmutableArray() ?? ImmutableArray.Empty; + + /// + /// Gets the methods for Moq.Mock{T}.SetupProperty. + /// + internal ImmutableArray Mock1SetupProperty => Mock1?.GetMembers("SetupProperty").OfType().ToImmutableArray() ?? ImmutableArray.Empty; + /// /// Gets the methods for Moq.Mock{T}.SetupAdd. /// diff --git a/tests/Moq.Analyzers.Test/Helpers/ReferenceAssemblyCatalog.cs b/tests/Moq.Analyzers.Test/Helpers/ReferenceAssemblyCatalog.cs index 3d6af60ca..c7f74a2d8 100644 --- a/tests/Moq.Analyzers.Test/Helpers/ReferenceAssemblyCatalog.cs +++ b/tests/Moq.Analyzers.Test/Helpers/ReferenceAssemblyCatalog.cs @@ -14,6 +14,12 @@ namespace Moq.Analyzers.Test.Helpers; /// public static class ReferenceAssemblyCatalog { + /// + /// Gets the name of the reference assembly group for .NET 8.0 without Moq. + /// Used to test analyzer behavior when Moq is not referenced. + /// + public static string Net80 => nameof(Net80); + /// /// Gets the name of the reference assembly group for .NET 8.0 with an older version of Moq (4.8.2). /// @@ -32,6 +38,9 @@ public static class ReferenceAssemblyCatalog /// public static IReadOnlyDictionary Catalog { get; } = new Dictionary(StringComparer.Ordinal) { + // .NET 8.0 without Moq, used to verify analyzers short-circuit when Moq is not referenced. + { nameof(Net80), ReferenceAssemblies.Net.Net80 }, + // 4.8.2 was one of the first popular versions of Moq. Ensure this version is prior to 4.13.1, as it changed the internal // implementation of `.As()` (see https://github.com/devlooped/moq/commit/b552aeddd82090ee0f4743a1ab70a16f3e6d2d11). { nameof(Net80WithOldMoq), ReferenceAssemblies.Net.Net80.AddPackages([new PackageIdentity("Moq", "4.8.2")]) }, diff --git a/tests/Moq.Analyzers.Test/NoMethodsInPropertySetupAnalyzerTests.cs b/tests/Moq.Analyzers.Test/NoMethodsInPropertySetupAnalyzerTests.cs index ada8e1967..1bee778db 100644 --- a/tests/Moq.Analyzers.Test/NoMethodsInPropertySetupAnalyzerTests.cs +++ b/tests/Moq.Analyzers.Test/NoMethodsInPropertySetupAnalyzerTests.cs @@ -1,3 +1,5 @@ +using Microsoft.CodeAnalysis.Testing; + using Verifier = Moq.Analyzers.Test.Helpers.AnalyzerVerifier; namespace Moq.Analyzers.Test; @@ -61,6 +63,32 @@ await Verifier.VerifyAnalyzerAsync( ReferenceAssemblyCatalog.Net80WithNewMoq); } + [Fact] + public async Task ShouldNotAnalyzeWhenMoqNotReferenced() + { + await Verifier.VerifyAnalyzerAsync( + """ + namespace Test + { + public interface IFoo + { + string Prop1 { get; set; } + string Method(); + } + + public class UnitTest + { + private void Test() + { + var x = new object(); + } + } + } + """, + ReferenceAssemblyCatalog.Net80, + CompilerDiagnostics.None); + } + [Fact] public async Task ShouldIncludeMethodNameInDiagnosticMessage() {