Skip to content
29 changes: 21 additions & 8 deletions src/Analyzers/MethodSetupShouldSpecifyReturnValueAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,8 @@ private static bool TryGetMockedMethodWithReturnValue(
}

/// <summary>
/// Checks if the setup invocation is followed by a Returns() or Throws() call.
/// This uses semantic analysis to verify the method being called.
/// Checks if the setup invocation is followed by a return value specification
/// anywhere in the method chain (e.g., .Setup().Callback().Returns()).
/// </summary>
private static bool HasReturnValueSpecification(IInvocationOperation setupInvocation)
{
Expand All @@ -146,17 +146,30 @@ private static bool HasReturnValueSpecification(IInvocationOperation setupInvoca
return false;
}

// Check if the setup call is the target of a member access (method chaining)
if (setupSyntax.Parent is Microsoft.CodeAnalysis.CSharp.Syntax.MemberAccessExpressionSyntax memberAccess)
SyntaxNode? current = setupSyntax;
while (current?.Parent is Microsoft.CodeAnalysis.CSharp.Syntax.MemberAccessExpressionSyntax memberAccess)
{
// Get the symbol information for the method being accessed
SymbolInfo symbolInfo = semanticModel.GetSymbolInfo(memberAccess.Name);

// Check if it's a method call and if the method name is Returns or Throws
return symbolInfo.Symbol is IMethodSymbol method &&
(string.Equals(method.Name, "Returns", StringComparison.Ordinal) || string.Equals(method.Name, "Throws", StringComparison.Ordinal));
if (symbolInfo.Symbol is IMethodSymbol method && IsReturnValueMethod(method.Name))
{
return true;
}

// Walk up to the containing invocation to check the next chained call
current = memberAccess.Parent is Microsoft.CodeAnalysis.CSharp.Syntax.InvocationExpressionSyntax parentInvocation
? parentInvocation
: null;
}
Comment thread
rjmurillo marked this conversation as resolved.

return false;
}

private static bool IsReturnValueMethod(string methodName)
{
return string.Equals(methodName, "Returns", StringComparison.Ordinal)
|| string.Equals(methodName, "ReturnsAsync", StringComparison.Ordinal)
|| string.Equals(methodName, "Throws", StringComparison.Ordinal)
|| string.Equals(methodName, "ThrowsAsync", StringComparison.Ordinal);
}
Comment thread
rjmurillo marked this conversation as resolved.
Outdated
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,31 @@ public static IEnumerable<object[]> TestData()
return both.Union(edge).WithNamespaces().WithMoqReferenceAssemblyGroups();
}

// Regression test data for https://github.com/rjmurillo/moq.analyzers/issues/849
public static IEnumerable<object[]> Issue849_FalsePositiveTestData()
{
IEnumerable<object[]> data =
[

// ReturnsAsync should be recognized as a return value specification
["""new Mock<IFoo>().Setup(x => x.BarAsync()).ReturnsAsync(1);"""],

// Callback before Returns should not prevent detection
["""new Mock<IFoo>().Setup(x => x.BarAsync()).Callback(() => { }).Returns(Task.FromResult(1));"""],

// Callback before ReturnsAsync should not prevent detection
["""new Mock<IFoo>().Setup(x => x.BarAsync()).Callback(() => { }).ReturnsAsync(1);"""],

// Callback before Returns on sync method should not prevent detection
["""new Mock<IFoo>().Setup(x => x.DoSomething("test")).Callback(() => { }).Returns(true);"""],

// Callback before Throws should not prevent detection
["""new Mock<IFoo>().Setup(x => x.DoSomething("test")).Callback(() => { }).Throws<InvalidOperationException>();"""],
];

return data.WithNamespaces().WithMoqReferenceAssemblyGroups();
}
Comment thread
rjmurillo marked this conversation as resolved.

[Theory]
[MemberData(nameof(TestData))]
public async Task ShouldAnalyzeMethodSetupReturnValue(string referenceAssemblyGroup, string @namespace, string mock)
Expand Down Expand Up @@ -83,6 +108,40 @@ await Verifier.VerifyAnalyzerAsync(
referenceAssemblyGroup);
}

[Theory]
[MemberData(nameof(Issue849_FalsePositiveTestData))]
public async Task ShouldNotFlagSetupWithReturnsAsyncOrCallbackChaining(string referenceAssemblyGroup, string @namespace, string mock)
{
string source = $$"""
{{@namespace}}

public interface IFoo
{
bool DoSomething(string value);
int GetValue();
int Calculate(int a, int b);
Task<int> BarAsync();
void DoVoidMethod();
void ProcessData(string data);
string Name { get; set; }
}

internal class UnitTest
{
private void Test()
{
{{mock}}
}
}
""";

output.WriteLine(source);

await Verifier.VerifyAnalyzerAsync(
source,
referenceAssemblyGroup);
Comment thread
qltysh[bot] marked this conversation as resolved.
Outdated
}
Comment thread
rjmurillo marked this conversation as resolved.
Outdated

[Theory]
[MemberData(nameof(DoppelgangerTestHelper.GetAllCustomMockData), MemberType = typeof(DoppelgangerTestHelper))]
public async Task ShouldPassIfCustomMockClassIsUsed(string mockCode)
Expand Down
Loading