Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ private static bool HasReturnValueSpecification(
}

SyntaxNode? current = setupSyntax;
while (current?.Parent is MemberAccessExpressionSyntax memberAccess)
while (current?.GetParentSkippingParentheses() is MemberAccessExpressionSyntax memberAccess)
{
cancellationToken.ThrowIfCancellationRequested();

Expand Down
17 changes: 17 additions & 0 deletions src/Common/SyntaxNodeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,21 @@ internal static class SyntaxNodeExtensions
? Location.Create(syntax.SyntaxTree, node.Span)
: null;
}

/// <summary>
/// Returns the parent node, skipping any intermediate <see cref="ParenthesizedExpressionSyntax"/> wrappers.
/// This handles cases like <c>(mock.Setup(...)).Returns()</c> where parentheses wrap an invocation.
/// </summary>
/// <param name="node">The node whose logical parent to find.</param>
/// <returns>The first non-parenthesized ancestor, or <see langword="null"/> if none exists.</returns>
internal static SyntaxNode? GetParentSkippingParentheses(this SyntaxNode node)
{
SyntaxNode? parent = node.Parent;
while (parent is ParenthesizedExpressionSyntax)
{
parent = parent.Parent;
}

return parent;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,55 @@ public static IEnumerable<object[]> OverloadResolutionFailureWithDiagnosticTestD
return data.WithNamespaces().WithMoqReferenceAssemblyGroups();
}

// Regression test data for https://github.com/rjmurillo/moq.analyzers/issues/887
// Tests parenthesized Setup expressions that should correctly detect return value specs.
public static IEnumerable<object[]> Issue887_ParenthesizedSetupTestData()
{
IEnumerable<object[]> data =
[

// Parenthesized Setup with Returns should not trigger diagnostic
["""(new Mock<IFoo>().Setup(x => x.GetValue())).Returns(42);"""],

// Nested parentheses with Returns should not trigger diagnostic
["""((new Mock<IFoo>().Setup(x => x.GetValue()))).Returns(42);"""],

// Parenthesized Setup with Throws should not trigger diagnostic
["""(new Mock<IFoo>().Setup(x => x.DoSomething("test"))).Throws<InvalidOperationException>();"""],

// Parenthesized Setup with Callback chaining should not trigger diagnostic
["""(new Mock<IFoo>().Setup(x => x.GetValue())).Callback(() => { }).Returns(42);"""],

// Parentheses around intermediate chain node should not trigger diagnostic
["""(new Mock<IFoo>().Setup(x => x.GetValue()).Callback(() => { })).Returns(42);"""],

// Parenthesized Setup with ReturnsAsync should not trigger diagnostic
["""(new Mock<IFoo>().Setup(x => x.BarAsync())).ReturnsAsync(1);"""],
];

return data.WithNamespaces().WithMoqReferenceAssemblyGroups();
}

// Parenthesized Setup without return value spec should still trigger Moq1203.
// Uses discard assignment to keep expressions valid C# while preserving parentheses.
public static IEnumerable<object[]> Issue887_ParenthesizedSetupWithDiagnosticTestData()
{
IEnumerable<object[]> data =
[

// Parenthesized Setup WITHOUT return value spec should still trigger diagnostic
["""_ = ({|Moq1203:new Mock<IFoo>().Setup(x => x.GetValue())|});"""],

// Nested parentheses WITHOUT return value spec should still trigger diagnostic
["""_ = (({|Moq1203:new Mock<IFoo>().Setup(x => x.GetValue())|}));"""],

Comment thread
rjmurillo marked this conversation as resolved.
// Parenthesized async Setup WITHOUT return value spec should still trigger diagnostic
["""_ = ({|Moq1203:new Mock<IFoo>().Setup(x => x.BarAsync())|});"""],
];

return data.WithNamespaces().WithMoqReferenceAssemblyGroups();
}

[Theory]
[MemberData(nameof(TestData))]
public async Task ShouldAnalyzeMethodSetupReturnValue(string referenceAssemblyGroup, string @namespace, string mock)
Expand All @@ -156,6 +205,20 @@ public async Task ShouldFlagCallbackOnlySetupOnNewMoq(string referenceAssemblyGr
await VerifyMockAsync(referenceAssemblyGroup, @namespace, mock);
}

[Theory]
[MemberData(nameof(Issue887_ParenthesizedSetupTestData))]
public async Task ShouldHandleParenthesizedSetupExpressions(string referenceAssemblyGroup, string @namespace, string mock)
{
await VerifyMockAsync(referenceAssemblyGroup, @namespace, mock);
}

[Theory]
[MemberData(nameof(Issue887_ParenthesizedSetupWithDiagnosticTestData))]
public async Task ShouldFlagParenthesizedSetupWithoutReturnValue(string referenceAssemblyGroup, string @namespace, string mock)
{
await VerifyMockAsync(referenceAssemblyGroup, @namespace, mock);
}

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