diff --git a/src/Analyzers/ConstructorArgumentsShouldMatchAnalyzer.cs b/src/Analyzers/ConstructorArgumentsShouldMatchAnalyzer.cs
index ae93b7e..8af2adb 100644
--- a/src/Analyzers/ConstructorArgumentsShouldMatchAnalyzer.cs
+++ b/src/Analyzers/ConstructorArgumentsShouldMatchAnalyzer.cs
@@ -99,9 +99,9 @@ private static bool IsExpressionMockBehavior(SyntaxNodeAnalysisContext context,
return targetSymbol.IsInstanceOf(knownSymbols.MockBehavior);
}
- private static bool IsFirstArgumentMockBehavior(SyntaxNodeAnalysisContext context, MoqKnownSymbols knownSymbols, ArgumentListSyntax? argumentList)
+ private static bool IsArgumentMockBehavior(SyntaxNodeAnalysisContext context, MoqKnownSymbols knownSymbols, ArgumentListSyntax? argumentList, uint argumentOrdinal)
{
- ExpressionSyntax? expression = argumentList?.Arguments[0].Expression;
+ ExpressionSyntax? expression = argumentList?.Arguments.Count > argumentOrdinal ? argumentList.Arguments[(int)argumentOrdinal].Expression : null;
return IsExpressionMockBehavior(context, knownSymbols, expression);
}
@@ -267,10 +267,13 @@ private static void AnalyzeNewObject(SyntaxNodeAnalysisContext context, MoqKnown
/// The constructors.
/// The arguments.
/// The context.
- /// true if a suitable constructor was found; otherwise false.
+ ///
+ /// if a suitable constructor was found; otherwise .
+ /// If the construction method is a parenthesized lambda expression, is returned.
+ ///
/// Handles and optional parameters.
[SuppressMessage("Design", "MA0051:Method is too long", Justification = "This should be refactored; suppressing for now to enable TreatWarningsAsErrors in CI.")]
- private static bool AnyConstructorsFound(
+ private static bool? AnyConstructorsFound(
IMethodSymbol[] constructors,
ArgumentSyntax[] arguments,
SyntaxNodeAnalysisContext context)
@@ -348,6 +351,24 @@ private static bool AnyConstructorsFound(
}
}
+ // Special case for Lambda expression syntax
+ // In Moq you can specify a Lambda expression that creates an instance
+ // of the specified type
+ // See https://github.com/devlooped/moq/blob/18dc7410ad4f993ce0edd809c5dfcaa3199f13ff/src/Moq/Mock%601.cs#L200
+ //
+ // The parenthesized lambda takes arguments as the first child node
+ // which may be empty or have args defined as part of a closure.
+ // Either way, we don't care about that, we only care that the
+ // constructor is valid.
+ //
+ // Since this does not use reflection through Castle, an invalid
+ // lambda here would cause the compiler to break, so no need to
+ // do additional checks.
+ if (arguments.Length == 1 && arguments[0].Expression.IsKind(SyntaxKind.ParenthesizedLambdaExpression))
+ {
+ return null;
+ }
+
return false;
}
@@ -386,10 +407,18 @@ private static void VerifyMockAttempt(
ArgumentSyntax[] arguments = argumentList?.Arguments.ToArray() ?? [];
#pragma warning restore ECS0900 // Consider using an alternative implementation to avoid boxing and unboxing
- if (hasMockBehavior && arguments.Length > 0 && IsFirstArgumentMockBehavior(context, knownSymbols, argumentList))
+ if (hasMockBehavior && arguments.Length > 0)
{
- // They passed a mock behavior as the first argument; ignore as Moq swallows it
- arguments = arguments.RemoveAt(0);
+ if (arguments.Length >= 1 && IsArgumentMockBehavior(context, knownSymbols, argumentList, 0))
+ {
+ // They passed a mock behavior as the first argument; ignore as Moq swallows it
+ arguments = arguments.RemoveAt(0);
+ }
+ else if (arguments.Length >= 2 && IsArgumentMockBehavior(context, knownSymbols, argumentList, 1))
+ {
+ // They passed a mock behavior as the second argument; ignore as Moq swallows it
+ arguments = arguments.RemoveAt(1);
+ }
}
switch (mockedClass.TypeKind)
@@ -433,7 +462,9 @@ private static void VerifyClassMockAttempt(
}
// We have constructors, now we need to check if the arguments match any of them
- if (!AnyConstructorsFound(constructors, arguments, context))
+ // If the value is null it means we want to ignore and not create a diagnostic
+ bool? matchingCtorFound = AnyConstructorsFound(constructors, arguments, context);
+ if (matchingCtorFound.HasValue && !matchingCtorFound.Value)
{
Diagnostic diagnostic = constructorIsEmpty.Location.CreateDiagnostic(ClassMustHaveMatchingConstructor, argumentList);
context.ReportDiagnostic(diagnostic);
diff --git a/tests/Moq.Analyzers.Test/ConstructorArgumentsShouldMatchAnalyzerTests.Expressions.cs b/tests/Moq.Analyzers.Test/ConstructorArgumentsShouldMatchAnalyzerTests.Expressions.cs
new file mode 100644
index 0000000..d6d3b94
--- /dev/null
+++ b/tests/Moq.Analyzers.Test/ConstructorArgumentsShouldMatchAnalyzerTests.Expressions.cs
@@ -0,0 +1,39 @@
+using Verifier = Moq.Analyzers.Test.Helpers.AnalyzerVerifier;
+
+namespace Moq.Analyzers.Test;
+
+public partial class ConstructorArgumentsShouldMatchAnalyzerTests
+{
+ public static IEnumerable