diff --git a/src/Analyzers/LinqToMocksExpressionShouldBeValidAnalyzer.cs b/src/Analyzers/LinqToMocksExpressionShouldBeValidAnalyzer.cs index fd69302d9..f3eb5a255 100644 --- a/src/Analyzers/LinqToMocksExpressionShouldBeValidAnalyzer.cs +++ b/src/Analyzers/LinqToMocksExpressionShouldBeValidAnalyzer.cs @@ -55,7 +55,7 @@ private static void AnalyzeInvocation(OperationAnalysisContext context) } // Analyze lambda expressions in the arguments - AnalyzeMockOfArguments(context, invocationOperation); + AnalyzeMockOfArguments(context, invocationOperation, knownSymbols); } /// @@ -75,7 +75,7 @@ private static bool IsValidMockOfInvocation(IInvocationOperation invocation, Moq targetMethod.ContainingType.Equals(knownSymbols.Mock, SymbolEqualityComparer.Default); } - private static void AnalyzeMockOfArguments(OperationAnalysisContext context, IInvocationOperation invocationOperation) + private static void AnalyzeMockOfArguments(OperationAnalysisContext context, IInvocationOperation invocationOperation, MoqKnownSymbols knownSymbols) { // Look for lambda expressions in the arguments (LINQ to Mocks expressions) foreach (IArgumentOperation argument in invocationOperation.Arguments) @@ -84,19 +84,19 @@ private static void AnalyzeMockOfArguments(OperationAnalysisContext context, IIn if (argumentValue is IAnonymousFunctionOperation lambdaOperation) { - AnalyzeLambdaExpression(context, lambdaOperation); + AnalyzeLambdaExpression(context, lambdaOperation, knownSymbols); } } } - private static void AnalyzeLambdaExpression(OperationAnalysisContext context, IAnonymousFunctionOperation lambdaOperation) + private static void AnalyzeLambdaExpression(OperationAnalysisContext context, IAnonymousFunctionOperation lambdaOperation, MoqKnownSymbols knownSymbols) { // For LINQ to Mocks, we need to handle more complex expressions like: x => x.Property == "value" // The lambda body is often a binary expression where the left operand is the member we want to check - AnalyzeLambdaBody(context, lambdaOperation, lambdaOperation.Body); + AnalyzeLambdaBody(context, lambdaOperation, lambdaOperation.Body, knownSymbols); } - private static void AnalyzeLambdaBody(OperationAnalysisContext context, IAnonymousFunctionOperation lambdaOperation, IOperation? body) + private static void AnalyzeLambdaBody(OperationAnalysisContext context, IAnonymousFunctionOperation lambdaOperation, IOperation? body, MoqKnownSymbols knownSymbols) { if (body == null) { @@ -107,18 +107,20 @@ private static void AnalyzeLambdaBody(OperationAnalysisContext context, IAnonymo { case IBlockOperation blockOp when blockOp.Operations.Length == 1: // Handle block lambdas with return statements - AnalyzeLambdaBody(context, lambdaOperation, blockOp.Operations[0]); + AnalyzeLambdaBody(context, lambdaOperation, blockOp.Operations[0], knownSymbols); break; case IReturnOperation returnOp: // Handle return statements - AnalyzeLambdaBody(context, lambdaOperation, returnOp.ReturnedValue); + AnalyzeLambdaBody(context, lambdaOperation, returnOp.ReturnedValue, knownSymbols); break; case IBinaryOperation binaryOp: - // Handle binary expressions like equality comparisons - AnalyzeMemberOperations(context, lambdaOperation, binaryOp.LeftOperand); - AnalyzeMemberOperations(context, lambdaOperation, binaryOp.RightOperand); + // Analyze each operand independently. The IsRootedInLambdaParameter guard + // in AnalyzeMemberOperations filters out operands not rooted in the lambda + // parameter (e.g., static constants, enum values). + AnalyzeMemberOperations(context, lambdaOperation, binaryOp.LeftOperand, knownSymbols); + AnalyzeMemberOperations(context, lambdaOperation, binaryOp.RightOperand, knownSymbols); break; case IPropertyReferenceOperation propertyRef: @@ -137,35 +139,60 @@ private static void AnalyzeLambdaBody(OperationAnalysisContext context, IAnonymo break; default: - // For other complex expressions, try to recursively find member references + // Route children through AnalyzeMemberOperations so they pass through the + // IsRootedInLambdaParameter guard. Calling AnalyzeLambdaBody directly would + // bypass the guard for operation kinds not enumerated above (e.g., + // IConditionalOperation, ICoalesceOperation). foreach (IOperation childOperation in body.ChildOperations) { - AnalyzeLambdaBody(context, lambdaOperation, childOperation); + AnalyzeMemberOperations(context, lambdaOperation, childOperation, knownSymbols); } break; } } - private static void AnalyzeMemberOperations(OperationAnalysisContext context, IAnonymousFunctionOperation lambdaOperation, IOperation? operation) + /// + /// Guards member analysis by filtering out operations not rooted in the lambda parameter, + /// then delegates to for recursive analysis. + /// + /// + /// + /// This method is the single entry point for all recursive member analysis. Every code path + /// in that descends into child operations must route through + /// this method. The guard is + /// applied only to leaf member operations ( and + /// ). Composite operations (e.g., IBinaryOperation + /// for chained &&/||/==) pass through to + /// for decomposition. Blocking composite operations would + /// cause false negatives for chained comparisons (see GitHub issue #1010). + /// + /// + /// Nested Mock.Of calls are excluded to prevent false positives from inner mock + /// expressions that have their own lambda parameters. + /// + /// + private static void AnalyzeMemberOperations(OperationAnalysisContext context, IAnonymousFunctionOperation lambdaOperation, IOperation operation, MoqKnownSymbols knownSymbols) { - if (operation == null) + // Don't recursively analyze nested Mock.Of calls to avoid false positives + if (operation is IInvocationOperation invocation && IsValidMockOfInvocation(invocation, knownSymbols)) { return; } - // Don't recursively analyze nested Mock.Of calls to avoid false positives - if (operation is IInvocationOperation invocation) + // Only apply the lambda-parameter guard to leaf member operations (property, + // field, event, method). Composite operations (IBinaryOperation for &&/||/==, + // IConditionalOperation, etc.) must pass through to AnalyzeLambdaBody for + // decomposition; blocking them here causes false negatives for chained + // comparisons like `c.Prop == "a" && c.Other == "b"`. + if (operation is (IMemberReferenceOperation or IInvocationOperation) + && !operation.IsRootedInLambdaParameter(lambdaOperation)) { - MoqKnownSymbols knownSymbols = new(context.Operation.SemanticModel!.Compilation); - if (IsValidMockOfInvocation(invocation, knownSymbols)) - { - return; // Skip analyzing nested Mock.Of calls - } + return; } // Recursively analyze the operation to find member references - AnalyzeLambdaBody(context, lambdaOperation, operation); + AnalyzeLambdaBody(context, lambdaOperation, operation, knownSymbols); } private static void AnalyzeMemberSymbol(OperationAnalysisContext context, ISymbol memberSymbol, IAnonymousFunctionOperation lambdaOperation) diff --git a/src/Common/IOperationExtensions.cs b/src/Common/IOperationExtensions.cs index ef0e323dc..67faa66cb 100644 --- a/src/Common/IOperationExtensions.cs +++ b/src/Common/IOperationExtensions.cs @@ -60,6 +60,76 @@ public static IOperation WalkDownImplicitConversion(this IOperation operation) internal static SyntaxNode? GetReferencedMemberSyntaxFromLambda(this IOperation? bodyOperation) => TraverseLambdaBody(bodyOperation, static op => op.GetSyntaxFromOperation()); + /// + /// Determines whether an operation's receiver chain terminates in a parameter of the + /// given lambda. Walks instance receivers (property, method, field, event) and transparent + /// wrappers (conversion, parenthesized) until it reaches a + /// or a terminal node. + /// + /// + /// + /// This method exists because IAnonymousFunctionOperation.GetCaptures() is an + /// internal Roslyn API and cannot be used by analyzers. Even if it were public, it + /// solves a different problem: it reports closed-over variables, not whether a member + /// access chain originates from the lambda parameter. + /// + /// + /// Use this method before flagging member accesses inside lambda expression analysis + /// to distinguish mock setup members (rooted in the lambda parameter) from value + /// expressions (static members, external locals, constants). + /// + /// + /// The operation whose receiver chain to walk. + /// The lambda whose parameter to match against. + /// + /// if the receiver chain terminates in the lambda parameter; + /// otherwise. + /// + internal static bool IsRootedInLambdaParameter( + this IOperation operation, + IAnonymousFunctionOperation lambdaOperation) + { + IParameterSymbol? lambdaParameter = lambdaOperation.Symbol.Parameters.FirstOrDefault(); + IOperation? current = operation; + while (true) + { + switch (current) + { + case IParameterReferenceOperation paramRef: + return lambdaParameter is not null && + SymbolEqualityComparer.Default.Equals(paramRef.Parameter, lambdaParameter); + + case IMemberReferenceOperation memberRef: + if (memberRef.Instance == null) + { + return false; // Static member access + } + + current = memberRef.Instance; + break; + + case IInvocationOperation invocationOp: + if (invocationOp.Instance == null) + { + return false; // Static method call + } + + current = invocationOp.Instance; + break; + + case IConversionOperation conversionOp: + current = conversionOp.Operand; + break; + + default: + // IParenthesizedOperation is intentionally omitted. The C# compiler + // never emits it in IOperation trees (VB.NET only), and this analyzer + // targets C# exclusively via [DiagnosticAnalyzer(LanguageNames.CSharp)]. + return false; + } + } + } + /// /// Traverses a lambda body operation to extract a value. For block lambdas, iterates all /// operations and returns the first non-null result (handling lambdas with multiple diff --git a/tests/Moq.Analyzers.Test/LinqToMocksExpressionShouldBeValidAnalyzerTests.cs b/tests/Moq.Analyzers.Test/LinqToMocksExpressionShouldBeValidAnalyzerTests.cs index d5f3cd6e5..e1c3174e7 100644 --- a/tests/Moq.Analyzers.Test/LinqToMocksExpressionShouldBeValidAnalyzerTests.cs +++ b/tests/Moq.Analyzers.Test/LinqToMocksExpressionShouldBeValidAnalyzerTests.cs @@ -6,6 +6,17 @@ public class LinqToMocksExpressionShouldBeValidAnalyzerTests(ITestOutputHelper o { private readonly ITestOutputHelper output = output; + /// + /// Provides both Moq reference assembly versions (4.8.2 and 4.18.4) for [Theory] tests. + /// All analyzer tests must run against both versions to catch version-specific regressions. + /// + /// One element per Moq reference assembly group. + public static IEnumerable MoqReferenceAssemblyGroups() + { + yield return [ReferenceAssemblyCatalog.Net80WithOldMoq]; + yield return [ReferenceAssemblyCatalog.Net80WithNewMoq]; + } + // Only one version of each static data source method public static IEnumerable EdgeCaseExpressionTestData() { @@ -215,8 +226,640 @@ private void Test() await Verifier.VerifyAnalyzerAsync(o, referenceAssemblyGroup); } - [Fact] - public async Task ShouldNotAnalyzeNonMockOfInvocations() + [Theory] + [MemberData(nameof(MoqReferenceAssemblyGroups))] + public async Task ShouldNotFlagStaticConstOnRightSideOfComparison(string referenceAssemblyGroup) + { + // Repro for https://github.com/rjmurillo/moq.analyzers/issues/1010 + await Verifier.VerifyAnalyzerAsync( + """ + using Moq; + + public interface IResponse + { + int Status { get; } + } + + public static class StatusCodes + { + public const int Status200OK = 200; + } + + internal class UnitTest + { + private void Test() + { + var response = Mock.Of(r => r.Status == StatusCodes.Status200OK); + } + } + """, + referenceAssemblyGroup); + } + + [Theory] + [MemberData(nameof(MoqReferenceAssemblyGroups))] + public async Task ShouldNotFlagStaticPropertyOnRightSideOfComparison(string referenceAssemblyGroup) + { + await Verifier.VerifyAnalyzerAsync( + """ + using Moq; + + public interface IResponse + { + int Status { get; } + } + + public static class StatusCodes + { + public static int Status202Accepted => 202; + } + + internal class UnitTest + { + private void Test() + { + var response = Mock.Of(r => r.Status == StatusCodes.Status202Accepted); + } + } + """, + referenceAssemblyGroup); + } + + [Theory] + [MemberData(nameof(MoqReferenceAssemblyGroups))] + public async Task ShouldNotFlagEnumValueOnRightSideOfComparison(string referenceAssemblyGroup) + { + await Verifier.VerifyAnalyzerAsync( + """ + using Moq; + + public enum UserStatus { Active, Inactive } + + public interface IUser + { + UserStatus Status { get; } + } + + internal class UnitTest + { + private void Test() + { + var user = Mock.Of(u => u.Status == UserStatus.Active); + } + } + """, + referenceAssemblyGroup); + } + + [Theory] + [MemberData(nameof(MoqReferenceAssemblyGroups))] + public async Task ShouldNotFlagStaticFieldOnLeftSideOfComparison(string referenceAssemblyGroup) + { + // Value expression on the left, mocked member on the right + await Verifier.VerifyAnalyzerAsync( + """ + using Moq; + + public interface IResponse + { + int Status { get; } + } + + public static class StatusCodes + { + public const int Status200OK = 200; + } + + internal class UnitTest + { + private void Test() + { + var response = Mock.Of(r => StatusCodes.Status200OK == r.Status); + } + } + """, + referenceAssemblyGroup); + } + + [Theory] + [MemberData(nameof(MoqReferenceAssemblyGroups))] + public async Task ShouldNotFlagExternalMembersInChainedComparisons(string referenceAssemblyGroup) + { + // Multiple comparisons joined with && using external constants + await Verifier.VerifyAnalyzerAsync( + """ + using Moq; + + public interface IResponse + { + int Status { get; } + string ReasonPhrase { get; } + } + + public static class StatusCodes + { + public const int Status200OK = 200; + } + + public static class Reasons + { + public const string Ok = "OK"; + } + + internal class UnitTest + { + private void Test() + { + var response = Mock.Of(r => + r.Status == StatusCodes.Status200OK && + r.ReasonPhrase == Reasons.Ok); + } + } + """, + referenceAssemblyGroup); + } + + [Theory] + [MemberData(nameof(MoqReferenceAssemblyGroups))] + public async Task ShouldNotFlagStaticMethodCallOnRightSideOfComparison(string referenceAssemblyGroup) + { + await Verifier.VerifyAnalyzerAsync( + """ + using Moq; + + public interface ITimer + { + int Timeout { get; } + } + + public class Defaults + { + public static int GetTimeout() => 30; + } + + internal class UnitTest + { + private void Test() + { + var timer = Mock.Of(t => t.Timeout == Defaults.GetTimeout()); + } + } + """, + referenceAssemblyGroup); + } + + [Theory] + [MemberData(nameof(MoqReferenceAssemblyGroups))] + public async Task ShouldStillFlagNonVirtualMemberOnLambdaParameter(string referenceAssemblyGroup) + { + // The fix must not suppress true positives: non-virtual members accessed + // through the lambda parameter should still be flagged. + await Verifier.VerifyAnalyzerAsync( + """ + using Moq; + + public class ConcreteClass + { + public string NonVirtualProperty { get; set; } + } + + public static class Constants + { + public const string DefaultValue = "default"; + } + + internal class UnitTest + { + private void Test() + { + var mock = Mock.Of(c => {|Moq1302:c.NonVirtualProperty|} == Constants.DefaultValue); + } + } + """, + referenceAssemblyGroup); + } + + [Theory] + [MemberData(nameof(MoqReferenceAssemblyGroups))] + public async Task ShouldStillFlagFieldOnLambdaParameterWithExternalConstant(string referenceAssemblyGroup) + { + // Fields on the lambda parameter are always invalid, even when the + // other side of the comparison is an external constant. + await Verifier.VerifyAnalyzerAsync( + """ + using Moq; + + public class ConcreteClass + { + public int Field; + } + + public static class Constants + { + public const int Value = 42; + } + + internal class UnitTest + { + private void Test() + { + var mock = Mock.Of(c => {|Moq1302:c.Field|} == Constants.Value); + } + } + """, + referenceAssemblyGroup); + } + + [Theory] + [MemberData(nameof(MoqReferenceAssemblyGroups))] + public async Task ShouldNotFlagInstancePropertyOnExternalObjectInComparison(string referenceAssemblyGroup) + { + // Instance property on a local variable (not the lambda parameter) should not be flagged + await Verifier.VerifyAnalyzerAsync( + """ + using Moq; + + public interface IService + { + string Name { get; } + } + + public class Config + { + public string ServiceName { get; set; } + } + + internal class UnitTest + { + private void Test() + { + var config = new Config { ServiceName = "test" }; + var svc = Mock.Of(s => s.Name == config.ServiceName); + } + } + """, + referenceAssemblyGroup); + } + + [Theory] + [MemberData(nameof(MoqReferenceAssemblyGroups))] + public async Task ShouldNotFlagTernaryWithStaticMember(string referenceAssemblyGroup) + { + // Ternary (conditional) expression containing a static member reference + // exercises the default branch in AnalyzeLambdaBody via IConditionalOperation. + await Verifier.VerifyAnalyzerAsync( + """ + using Moq; + + public interface IService + { + string Name { get; } + } + + public static class Defaults + { + public static string FallbackName => "fallback"; + } + + internal class UnitTest + { + private void Test() + { + var svc = Mock.Of(s => s.Name == (true ? "active" : Defaults.FallbackName)); + } + } + """, + referenceAssemblyGroup); + } + + [Theory] + [MemberData(nameof(MoqReferenceAssemblyGroups))] + public async Task ShouldNotFlagNullCoalescingWithExternalDefault(string referenceAssemblyGroup) + { + // Null-coalescing expression exercises the default branch in AnalyzeLambdaBody + // via ICoalesceOperation. The external constant should not be flagged. + await Verifier.VerifyAnalyzerAsync( + """ + using Moq; + + public interface IRepository + { + string Name { get; } + } + + public static class Constants + { + public const string DefaultName = "default"; + } + + internal class UnitTest + { + private void Test() + { + var repo = Mock.Of(r => (r.Name ?? Constants.DefaultName) == "test"); + } + } + """, + referenceAssemblyGroup); + } + + [Theory] + [MemberData(nameof(MoqReferenceAssemblyGroups))] + public async Task ShouldFlagNonVirtualMemberInsideConditionalExpression(string referenceAssemblyGroup) + { + // Non-virtual member inside a ternary expression should be flagged. + // Virtual members and string literals in the same ternary should not be flagged. + await Verifier.VerifyAnalyzerAsync( + """ + using Moq; + + public class ConcreteClass + { + public string NonVirtualProperty { get; set; } + public virtual bool IsEnabled { get; set; } + } + + internal class UnitTest + { + private void Test() + { + var mock = Mock.Of(c => (c.IsEnabled ? {|Moq1302:c.NonVirtualProperty|} : "none") == "test"); + } + } + """, + referenceAssemblyGroup); + } + + [Theory] + [MemberData(nameof(MoqReferenceAssemblyGroups))] + public async Task ShouldHandleChainedPropertyAccessOnLambdaParameter(string referenceAssemblyGroup) + { + // Exercises multiple hops in the IsRootedInLambdaParameter receiver chain walk. + await Verifier.VerifyAnalyzerAsync( + """ + using Moq; + + public interface IInner + { + string Value { get; } + } + + public interface IOuter + { + IInner Inner { get; } + } + + internal class UnitTest + { + private void Test() + { + var mock = Mock.Of(o => o.Inner.Value == "test"); + } + } + """, + referenceAssemblyGroup); + } + + [Theory] + [MemberData(nameof(MoqReferenceAssemblyGroups))] + public async Task ShouldFlagNonVirtualMemberInChainedAndComparison(string referenceAssemblyGroup) + { + // Regression test: chained && must not suppress non-virtual member diagnostics. + // The inner == comparisons are IBinaryOperation nodes that must pass through + // AnalyzeLambdaBody for decomposition, not be blocked by the guard. + await Verifier.VerifyAnalyzerAsync( + """ + using Moq; + + public class ConcreteClass + { + public string NonVirtualProperty { get; set; } + public virtual int VirtualProperty { get; set; } + } + + internal class UnitTest + { + private void Test() + { + var mock = Mock.Of(c => + {|Moq1302:c.NonVirtualProperty|} == "a" && + c.VirtualProperty == 1); + } + } + """, + referenceAssemblyGroup); + } + + [Theory] + [MemberData(nameof(MoqReferenceAssemblyGroups))] + public async Task ShouldFlagMultipleNonVirtualMembersInChainedComparison(string referenceAssemblyGroup) + { + // Both non-virtual members in a chained && should be flagged. + await Verifier.VerifyAnalyzerAsync( + """ + using Moq; + + public class ConcreteClass + { + public string Name { get; set; } + public int Age { get; set; } + } + + internal class UnitTest + { + private void Test() + { + var mock = Mock.Of(c => + {|Moq1302:c.Name|} == "test" && + {|Moq1302:c.Age|} == 42); + } + } + """, + referenceAssemblyGroup); + } + + [Theory] + [MemberData(nameof(MoqReferenceAssemblyGroups))] + public async Task ShouldNotFlagVirtualMembersInChainedComparisonWithStaticConstants(string referenceAssemblyGroup) + { + // No false positives: virtual members with static constants in chained &&. + await Verifier.VerifyAnalyzerAsync( + """ + using Moq; + + public interface IService + { + string Name { get; } + int Priority { get; } + bool IsEnabled { get; } + } + + public static class Defaults + { + public const string ServiceName = "default"; + public const int DefaultPriority = 5; + } + + internal class UnitTest + { + private void Test() + { + var svc = Mock.Of(s => + s.Name == Defaults.ServiceName && + s.Priority == Defaults.DefaultPriority && + s.IsEnabled == true); + } + } + """, + referenceAssemblyGroup); + } + + [Theory] + [MemberData(nameof(MoqReferenceAssemblyGroups))] + public async Task ShouldFlagNonVirtualMemberInOrComparison(string referenceAssemblyGroup) + { + // || is also an IBinaryOperation; non-virtual members must still be flagged. + await Verifier.VerifyAnalyzerAsync( + """ + using Moq; + + public class ConcreteClass + { + public string NonVirtualProperty { get; set; } + public virtual bool IsActive { get; set; } + } + + internal class UnitTest + { + private void Test() + { + var mock = Mock.Of(c => + {|Moq1302:c.NonVirtualProperty|} == "x" || + c.IsActive == true); + } + } + """, + referenceAssemblyGroup); + } + + [Theory] + [MemberData(nameof(MoqReferenceAssemblyGroups))] + public async Task ShouldFlagNonVirtualPropertyInNullCoalescing(string referenceAssemblyGroup) + { + // Non-virtual member rooted in lambda parameter inside null-coalescing + // should still be flagged. + await Verifier.VerifyAnalyzerAsync( + """ + using Moq; + + public class ConcreteClass + { + public string NonVirtualProperty { get; set; } + } + + internal class UnitTest + { + private void Test() + { + var mock = Mock.Of(c => + ({|Moq1302:c.NonVirtualProperty|} ?? "fallback") == "test"); + } + } + """, + referenceAssemblyGroup); + } + + [Theory] + [MemberData(nameof(MoqReferenceAssemblyGroups))] + public async Task ShouldNotFlagNestedMockOfExpression(string referenceAssemblyGroup) + { + // Nested Mock.Of calls have their own lambda parameters and should not + // be recursively analyzed by the outer lambda's analysis. + await Verifier.VerifyAnalyzerAsync( + """ + using Moq; + + public interface IInner + { + string Value { get; } + } + + public interface IOuter + { + IInner Inner { get; } + } + + internal class UnitTest + { + private void Test() + { + var mock = Mock.Of(o => o.Inner == Mock.Of(i => i.Value == "nested")); + } + } + """, + referenceAssemblyGroup); + } + + [Theory] + [MemberData(nameof(MoqReferenceAssemblyGroups))] + public async Task ShouldNotFlagMemberAccessThroughExplicitCast(string referenceAssemblyGroup) + { + // Explicit cast on the lambda parameter inserts an IConversionOperation + // in the receiver chain. IsRootedInLambdaParameter must walk through it. + await Verifier.VerifyAnalyzerAsync( + """ + using Moq; + + public interface IBase + { + string Name { get; } + } + + public interface IDerived : IBase + { + string Extra { get; } + } + + internal class UnitTest + { + private void Test() + { + var mock = Mock.Of(d => ((IBase)d).Name == "test"); + } + } + """, + referenceAssemblyGroup); + } + + [Theory] + [MemberData(nameof(MoqReferenceAssemblyGroups))] + public async Task ShouldNotFlagVirtualInstanceMethodOnLambdaParameter(string referenceAssemblyGroup) + { + // Instance method call on the lambda parameter exercises the + // IInvocationOperation branch with non-null Instance in IsRootedInLambdaParameter. + await Verifier.VerifyAnalyzerAsync( + """ + using Moq; + + public interface IFormatter + { + string Format(string input); + } + + internal class UnitTest + { + private void Test() + { + var mock = Mock.Of(f => f.Format("x") == "formatted"); + } + } + """, + referenceAssemblyGroup); + } + + [Theory] + [MemberData(nameof(MoqReferenceAssemblyGroups))] + public async Task ShouldNotAnalyzeNonMockOfInvocations(string referenceAssemblyGroup) { await Verifier.VerifyAnalyzerAsync( """ @@ -239,6 +882,6 @@ private void Test() private string SomeMethod(System.Func predicate) => "test"; } """, - referenceAssemblyGroup: ReferenceAssemblyCatalog.Net80WithOldMoq); + referenceAssemblyGroup); } }