Skip to content

Commit 4838e22

Browse files
committed
Remove false positive for Moq1200 when using parameterized lambda
Fixes #234
1 parent 20c0af0 commit 4838e22

File tree

1 file changed

+44
-6
lines changed

1 file changed

+44
-6
lines changed

src/Analyzers/ConstructorArgumentsShouldMatchAnalyzer.cs

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,13 @@ private static bool IsFirstArgumentMockBehavior(SyntaxNodeAnalysisContext contex
106106
return IsExpressionMockBehavior(context, knownSymbols, expression);
107107
}
108108

109+
private static bool IsSecondArgumentMockBehavior(SyntaxNodeAnalysisContext context, MoqKnownSymbols knownSymbols, ArgumentListSyntax? argumentList)
110+
{
111+
ExpressionSyntax? expression = argumentList?.Arguments[1].Expression;
112+
113+
return IsExpressionMockBehavior(context, knownSymbols, expression);
114+
}
115+
109116
private static void VerifyDelegateMockAttempt(
110117
SyntaxNodeAnalysisContext context,
111118
ArgumentListSyntax? argumentList,
@@ -267,10 +274,13 @@ private static void AnalyzeNewObject(SyntaxNodeAnalysisContext context, MoqKnown
267274
/// <param name="constructors">The constructors.</param>
268275
/// <param name="arguments">The arguments.</param>
269276
/// <param name="context">The context.</param>
270-
/// <returns><c>true</c> if a suitable constructor was found; otherwise <c>false</c>. </returns>
277+
/// <returns>
278+
/// <see langword="true" /> if a suitable constructor was found; otherwise <see langword="false" />.
279+
/// If the construction method is a parenthesized lambda expression, <see langword="null" /> is returned.
280+
/// </returns>
271281
/// <remarks>Handles <see langword="params" /> and optional parameters.</remarks>
272282
[SuppressMessage("Design", "MA0051:Method is too long", Justification = "This should be refactored; suppressing for now to enable TreatWarningsAsErrors in CI.")]
273-
private static bool AnyConstructorsFound(
283+
private static bool? AnyConstructorsFound(
274284
IMethodSymbol[] constructors,
275285
ArgumentSyntax[] arguments,
276286
SyntaxNodeAnalysisContext context)
@@ -348,6 +358,24 @@ private static bool AnyConstructorsFound(
348358
}
349359
}
350360

361+
// Special case for Lambda expression syntax
362+
// In Moq you can specify a Lambda expression that creates an instance
363+
// of the specified type
364+
// See https://github.com/devlooped/moq/blob/18dc7410ad4f993ce0edd809c5dfcaa3199f13ff/src/Moq/Mock%601.cs#L200
365+
//
366+
// The parenthesized lambda takes arguments as the first child node
367+
// which may be empty or have args defined as part of a closure.
368+
// Either way, we don't care about that, we only care that the
369+
// constructor is valid.
370+
//
371+
// Since this does not use reflection through Castle, an invalid
372+
// lambda here would cause the compiler to break, so no need to
373+
// do additional checks.
374+
if (arguments.Length == 1 && arguments[0].Expression.IsKind(SyntaxKind.ParenthesizedLambdaExpression))
375+
{
376+
return null;
377+
}
378+
351379
return false;
352380
}
353381

@@ -386,10 +414,18 @@ private static void VerifyMockAttempt(
386414
ArgumentSyntax[] arguments = argumentList?.Arguments.ToArray() ?? [];
387415
#pragma warning restore ECS0900 // Consider using an alternative implementation to avoid boxing and unboxing
388416

389-
if (hasMockBehavior && arguments.Length > 0 && IsFirstArgumentMockBehavior(context, knownSymbols, argumentList))
417+
if (hasMockBehavior && arguments.Length > 0)
390418
{
391-
// They passed a mock behavior as the first argument; ignore as Moq swallows it
392-
arguments = arguments.RemoveAt(0);
419+
if (arguments.Length >= 1 && IsFirstArgumentMockBehavior(context, knownSymbols, argumentList))
420+
{
421+
// They passed a mock behavior as the first argument; ignore as Moq swallows it
422+
arguments = arguments.RemoveAt(0);
423+
}
424+
else if (arguments.Length >= 2 && IsSecondArgumentMockBehavior(context, knownSymbols, argumentList))
425+
{
426+
// They passed a mock behavior as the second argument; ignore as Moq swallows it
427+
arguments = arguments.RemoveAt(1);
428+
}
393429
}
394430

395431
switch (mockedClass.TypeKind)
@@ -433,7 +469,9 @@ private static void VerifyClassMockAttempt(
433469
}
434470

435471
// We have constructors, now we need to check if the arguments match any of them
436-
if (!AnyConstructorsFound(constructors, arguments, context))
472+
// If the value is null it means we want to ignore and not create a diagnostic
473+
bool? matchingCtorFound = AnyConstructorsFound(constructors, arguments, context);
474+
if (matchingCtorFound.HasValue && !matchingCtorFound.Value)
437475
{
438476
Diagnostic diagnostic = constructorIsEmpty.Location.CreateDiagnostic(ClassMustHaveMatchingConstructor, argumentList);
439477
context.ReportDiagnostic(diagnostic);

0 commit comments

Comments
 (0)