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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ something is wrong with your Moq configuration.
| [Moq1205](docs/rules/Moq1205.md) | Correctness | Event setup handler type should match event delegate type |
| [Moq1206](docs/rules/Moq1206.md) | Correctness | Async method setups should use ReturnsAsync instead of Returns with async lambda |
| [Moq1207](docs/rules/Moq1207.md) | Correctness | SetupSequence should be used only for overridable members |
| [Moq1208](docs/rules/Moq1208.md) | Correctness | Returns() delegate type mismatch on async method setup |
| [Moq1210](docs/rules/Moq1210.md) | Correctness | Verify should be used only for overridable members |
| [Moq1300](docs/rules/Moq1300.md) | Usage | `Mock.As()` should take interfaces only |
| [Moq1301](docs/rules/Moq1301.md) | Usage | Mock.Get() should not take literals |
Expand Down
133 changes: 133 additions & 0 deletions docs/rules/Moq1208.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Moq1208: Returns() delegate type mismatch on async method setup

| Item | Value |
| -------- | ------- |
| Enabled | True |
| Severity | Warning |
| CodeFix | True |

---

## What this rule checks

In Moq, `.Setup()` defines what a mocked method should do when called.
`.Returns()` specifies the value the method gives back. For example:

```csharp
mock.Setup(x => x.GetName()).Returns(() => "Alice");
// ^^^^^ "when GetName is called" ^^^^^^^^^ "return Alice"
```

This rule fires when a delegate passed to `.Returns()` gives back a plain
value like `int` or `string`, but the mocked method is async and returns
`Task<int>` or `ValueTask<string>`. Moq requires the types to match exactly.

The rule detects three forms of delegates:

- **Lambdas**: `Returns(() => 42)`
- **Anonymous methods**: `Returns(delegate { return 42; })`
- **Method groups**: `Returns(GetInt)` where `GetInt` returns `int`

### Why this matters

The code compiles without errors, but the test fails at runtime with this
exception:

```text
MockException: Invalid callback. Setup on method with return type 'Task<int>'
cannot invoke callback with return type 'int'.
```

This analyzer catches the mismatch at compile time so you don't have to debug
a failing test to find it.

### How this differs from Moq1206

[Moq1206](./Moq1206.md) flags `async` delegates in `.Returns()`, such as
`Returns(async () => 42)`. Moq1208 flags regular (non-async) delegates that
return the wrong type, such as `Returns(() => 42)` on a `Task<int>` method.

## Examples of patterns that are flagged by this analyzer

```csharp
public interface IService
{
Task<int> GetValueAsync(); // Returns Task<int>
Task<string> GetNameAsync(); // Returns Task<string>
ValueTask<int> GetValueTaskAsync(); // Returns ValueTask<int>
}

var mock = new Mock<IService>();

// Lambda returning int on a Task<int> method.
mock.Setup(x => x.GetValueAsync()).Returns(() => 42); // Moq1208

// Lambda returning string on a Task<string> method.
mock.Setup(x => x.GetNameAsync()).Returns(() => "hello"); // Moq1208

// Anonymous method returning int on a Task<int> method.
mock.Setup(x => x.GetValueAsync()).Returns(delegate { return 42; }); // Moq1208

// Method group returning int on a Task<int> method.
mock.Setup(x => x.GetValueAsync()).Returns(GetInt); // Moq1208
// where: static int GetInt() => 42;

// ValueTask<int> with wrong return type.
mock.Setup(x => x.GetValueTaskAsync()).Returns(() => 42); // Moq1208
```

## Solution

### Option 1: Use ReturnsAsync (recommended)

`.ReturnsAsync()` wraps the value in `Task.FromResult()` for you. This is the
simplest fix and what the built-in code fix applies automatically.

```csharp
var mock = new Mock<IService>();

// Pass a plain value. Moq wraps it in Task.FromResult() internally.
mock.Setup(x => x.GetValueAsync()).ReturnsAsync(42);

// Or pass a lambda. Moq wraps the lambda's return value the same way.
mock.Setup(x => x.GetValueAsync()).ReturnsAsync(() => 42);

// Anonymous methods and method groups work the same way.
mock.Setup(x => x.GetValueAsync()).ReturnsAsync(delegate { return 42; });
mock.Setup(x => x.GetValueAsync()).ReturnsAsync(GetInt);
```

### Option 2: Wrap the value yourself

If you need more control, keep `.Returns()` and wrap the value explicitly.

```csharp
var mock = new Mock<IService>();

mock.Setup(x => x.GetValueAsync()).Returns(() => Task.FromResult(42));
mock.Setup(x => x.GetNameAsync()).Returns(() => Task.FromResult("hello"));
mock.Setup(x => x.GetValueTaskAsync()).Returns(() => new ValueTask<int>(42));
```

## Suppress a warning

If you just want to suppress a single violation, add preprocessor directives to
your source file to disable and then re-enable the rule.

```csharp
#pragma warning disable Moq1208
mock.Setup(x => x.GetValueAsync()).Returns(() => 42);
#pragma warning restore Moq1208
```

To disable the rule for a file, folder, or project, set its severity to `none`
in the
[configuration file](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/configuration-files).

```ini
[*.{cs,vb}]
dotnet_diagnostic.Moq1208.severity = none
```

For more information, see
[How to suppress code analysis warnings](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/suppress-warnings).
1 change: 1 addition & 0 deletions docs/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
| [Moq1205](./Moq1205.md) | Correctness | Event setup handler type should match event delegate type | [EventSetupHandlerShouldMatchEventTypeAnalyzer.cs](../../src/Analyzers/EventSetupHandlerShouldMatchEventTypeAnalyzer.cs) |
| [Moq1206](./Moq1206.md) | Correctness | Async method setups should use ReturnsAsync instead of Returns with async lambda | [ReturnsAsyncShouldBeUsedForAsyncMethodsAnalyzer.cs](../../src/Analyzers/ReturnsAsyncShouldBeUsedForAsyncMethodsAnalyzer.cs) |
| [Moq1207](./Moq1207.md) | Correctness | SetupSequence should be used only for overridable members | [SetupSequenceShouldBeUsedOnlyForOverridableMembersAnalyzer.cs](../../src/Analyzers/SetupSequenceShouldBeUsedOnlyForOverridableMembersAnalyzer.cs) |
| [Moq1208](./Moq1208.md) | Correctness | Returns() delegate type mismatch on async method setup | [ReturnsDelegateShouldReturnTaskAnalyzer.cs](../../src/Analyzers/ReturnsDelegateShouldReturnTaskAnalyzer.cs) |
| [Moq1210](./Moq1210.md) | Correctness | Verify should be used only for overridable members | [VerifyShouldBeUsedOnlyForOverridableMembersAnalyzer.cs](../../src/Analyzers/VerifyShouldBeUsedOnlyForOverridableMembersAnalyzer.cs) |
| [Moq1300](./Moq1300.md) | Usage | `Mock.As()` should take interfaces only | [AsShouldBeUsedOnlyForInterfaceAnalyzer.cs](../../src/Analyzers/AsShouldBeUsedOnlyForInterfaceAnalyzer.cs) |
| [Moq1301](./Moq1301.md) | Usage | Mock.Get() should not take literals | [MockGetShouldNotTakeLiteralsAnalyzer.cs](../../src/Analyzers/MockGetShouldNotTakeLiteralsAnalyzer.cs) |
Expand Down
1 change: 1 addition & 0 deletions src/Analyzers/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Moq1204 | Usage | Warning | RaisesEventArgumentsShouldMatchEventSignatureAnalyze
Moq1205 | Usage | Warning | EventSetupHandlerShouldMatchEventTypeAnalyzer (updated category from Moq to Usage)
Moq1206 | Usage | Warning | ReturnsAsyncShouldBeUsedForAsyncMethodsAnalyzer (updated category from Moq to Usage)
Moq1207 | Usage | Error | SetupSequenceShouldBeUsedOnlyForOverridableMembersAnalyzer (updated category from Moq to Usage)
Moq1208 | Usage | Warning | ReturnsDelegateShouldReturnTaskAnalyzer
Moq1210 | Usage | Error | VerifyShouldBeUsedOnlyForOverridableMembersAnalyzer (updated category from Moq to Usage)
Moq1300 | Usage | Error | AsShouldBeUsedOnlyForInterfaceAnalyzer (updated category from Moq to Usage)
Moq1301 | Usage | Warning | Mock.Get() should not take literals (updated category from Moq to Usage)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
}

// Find the Setup call that this Returns is chained from
InvocationExpressionSyntax? setupInvocation = FindSetupInvocation(invocation);
MemberAccessExpressionSyntax memberAccess = (MemberAccessExpressionSyntax)invocation.Expression;
InvocationExpressionSyntax? setupInvocation = memberAccess.Expression.FindSetupInvocation(context.SemanticModel, knownSymbols);
if (setupInvocation == null)
{
return;
Expand All @@ -58,9 +59,6 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
}

// Report diagnostic on just the Returns(...) method call
// We can safely cast here because IsReturnsMethodCallWithAsyncLambda already verified this is a MemberAccessExpressionSyntax
MemberAccessExpressionSyntax memberAccess = (MemberAccessExpressionSyntax)invocation.Expression;

// Create a span from the Returns identifier through the end of the invocation
int startPos = memberAccess.Name.SpanStart;
int endPos = invocation.Span.End;
Expand Down Expand Up @@ -96,21 +94,6 @@ private static bool IsReturnsMethodCallWithAsyncLambda(InvocationExpressionSynta
return HasAsyncLambdaArgument(invocation);
}

private static InvocationExpressionSyntax? FindSetupInvocation(InvocationExpressionSyntax returnsInvocation)
{
// The pattern is: mock.Setup(...).Returns(...)
// The returnsInvocation is the entire chain, so we need to examine its structure
if (returnsInvocation.Expression is MemberAccessExpressionSyntax memberAccess &&
memberAccess.Expression.WalkDownParentheses() is InvocationExpressionSyntax setupInvocation &&
setupInvocation.Expression is MemberAccessExpressionSyntax setupMemberAccess &&
string.Equals(setupMemberAccess.Name.Identifier.ValueText, "Setup", StringComparison.Ordinal))
{
return setupInvocation;
}

return null;
}

private static bool HasAsyncLambdaArgument(InvocationExpressionSyntax invocation)
{
if (invocation.ArgumentList.Arguments.Count == 0)
Expand Down
Loading
Loading