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()
{