Skip to content

Commit

Permalink
Add the PosInfoMoq2015 rule to check the return type of the Protected…
Browse files Browse the repository at this point in the history
…() setup methods (fixes #38).
  • Loading branch information
GillesTourreau committed Oct 23, 2024
1 parent e5bda95 commit 0a57f99
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 31 deletions.
1 change: 1 addition & 0 deletions PosInformatique.Moq.Analyzers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Compilation", "Compilation"
docs\Compilation\PosInfoMoq2012.md = docs\Compilation\PosInfoMoq2012.md
docs\Compilation\PosInfoMoq2013.md = docs\Compilation\PosInfoMoq2013.md
docs\Compilation\PosInfoMoq2014.md = docs\Compilation\PosInfoMoq2014.md
docs\Compilation\PosInfoMoq2015.md = docs\Compilation\PosInfoMoq2015.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Analyzers.Sandbox", "tests\Moq.Analyzers.Sandbox\Moq.Analyzers.Sandbox.csproj", "{07F970A1-1477-4D4C-B233-C9B4DA6E3AD6}"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ All the rules of this category should not be disabled (or changed their severity
| [PosInfoMoq2012: The delegate in the argument of the `Returns()` method must return a value with same type of the mocked method.](docs/Compilation/PosInfoMoq2012.md) | The lambda expression, anonymous method or method in the argument of the `Returns()` must return return a value of the same type as the mocked method or property. |
| [PosInfoMoq2013: The delegate in the argument of the `Returns()`/`ReturnsAsync()` method must have the same parameter types of the mocked method/property.](docs/Compilation/PosInfoMoq2013.md) | The lambda expression, anonymous method or method in the argument of the `Returns()`/`ReturnsAsync()` must have the same arguments type of the mocked method or property. |
| [PosInfoMoq2014: The `Callback()` delegate expression must not return a value.](docs/Compilation/PosInfoMoq2014.md) | The `Callback()` delegate expression must not return a value. |
| [PosInfoMoq2014: PosInfoMoq2015: The `Protected().Setup()` method must match the return type of the mocked method](docs/Compilation/PosInfoMoq2015.md) | The method setup with `Protected().Setup()` must match the return type of the mocked method. |



2 changes: 1 addition & 1 deletion docs/Compilation/PosInfoMoq2006.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ and must be overridable (`virtual`, `abstract` and `override`, but not `sealed`)
public void Test()
{
var service = new Mock<Service>();
service.Protected().Setup("GetData") // The GetData() is public and can be mocked with Protected() feature.
service.Protected().Setup("GetData") // The GetData() is public and can't be mocked with Protected() feature.
.Returns(10);
service.Protected().Setup("NotExists") // The NotExists() method does not exist.
.Returns(10);
Expand Down
50 changes: 50 additions & 0 deletions docs/Compilation/PosInfoMoq2015.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# PosInfoMoq2015: The `Protected().Setup()` method must match the return type of the mocked method

| Property | Value |
|-------------------------------------|----------------------------------------------------------------------------------------------|
| **Rule ID** | PosInfoMoq2015 |
| **Title** | The `Protected().Setup()` method must match the return type of the mocked method. |
| **Category** | Compilation |
| **Default severity** | Error |

## Cause

The method setup with `Protected().Setup()` must match the return type of the mocked method.

## Rule description

When using the `Protected().Setup()`, the return type of mocked method must match of the generic
argument specified in the `Setup<T>()` method.

```csharp
[Fact]
public void Test()
{
var service = new Mock<Service>();
service.Protected().Setup<int>("GetData") // OK.
.Returns(10);
service.Protected().Setup("GetData") // Error: The GetData() return an int, Setup<int>() must be use.
service.Protected().Setup<int>("SendEmail") // Error: The SendEmail() method does not return a value, the `int` generic argument must be remove.0
.Returns(10);
service.Protected().Setup<string>("GetData") // Error: The GetData() return an int, Setup<int>() must be use.
.Returns("The data");
}

public abstract class Service
{
protected abstract int GetData();

protected abstract void SendEmail();
}
```

## How to fix violations

To fix a violation of this rule, use the generic parameter of the `Setup<T>()` method if the protected mocked
method return a value. Else do not specify a generic parameter for the `Setup<T>()` method of the protected mocked
method does not return a value (`void`).

## When to suppress warnings

Do not suppress an error from this rule. If bypassed, the execution of the unit test will be failed with a `ArgumentException`
thrown with the *"Can't set return value for void method xxx."* message.
3 changes: 2 additions & 1 deletion src/Moq.Analyzers/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
### New Rules
Rule ID | Category | Severity | Notes
--------|----------|----------|-------
PosInfoMoq2014 | Compilation | Error | CallBackDelegateMustMatchMockedMethodAnalyzer, [Documentation](https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2014.html)PosInfoMoq2013 | Compilation | Error | The delegate in the argument of the `Returns()`/`ReturnsAsync()` method must have the same parameter types of the mocked method/property.
PosInfoMoq2014 | Compilation | Error | The `Callback()` delegate expression must not return a value.
PosInfoMoq2015 | Compilation | Error | The `Protected().Setup()` method must match the return type of the mocked method.

## Release 1.10.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace PosInformatique.Moq.Analyzers
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class SetupProtectedMustBeUsedWithProtectedOrInternalMembersAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
private static readonly DiagnosticDescriptor SetupMustBeOnOverridableMethods = new DiagnosticDescriptor(
"PosInfoMoq2006",
"The Protected().Setup() method must be use with overridable protected or internal methods",
"The Protected().Setup() method must be use with overridable protected or internal methods",
Expand All @@ -25,7 +25,17 @@ public class SetupProtectedMustBeUsedWithProtectedOrInternalMembersAnalyzer : Di
description: "The Protected().Setup() method must be use with overridable protected or internal methods.",
helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2006.html");

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
private static readonly DiagnosticDescriptor SetupReturnTypeMustMatch = new DiagnosticDescriptor(
"PosInfoMoq2015",
"The Protected().Setup() method must match the return type of the mocked method",
"The Protected().Setup() method must match the return type of the mocked method",
"Compilation",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "The Protected().Setup() method must match the return type of the mocked method.",
helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2015.html");

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(SetupMustBeOnOverridableMethods, SetupReturnTypeMustMatch);

public override void Initialize(AnalysisContext context)
{
Expand Down Expand Up @@ -81,6 +91,8 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
}

// Check if a method exists with the specified name
IMethodSymbol? methodMatch = null;

foreach (var method in mockedType.GetAllMembers(methodName).OfType<IMethodSymbol>())
{
if (!method.IsAbstract && !method.IsVirtual && !method.IsOverride)
Expand All @@ -95,23 +107,78 @@ private static void Analyze(SyntaxNodeAnalysisContext context)

if (method.DeclaredAccessibility == Accessibility.Protected)
{
return;
methodMatch = method;
break;
}

if (method.DeclaredAccessibility == Accessibility.Internal)
{
return;
methodMatch = method;
break;
}

if (method.DeclaredAccessibility == Accessibility.ProtectedOrInternal)
{
return;
methodMatch = method;
break;
}
}

if (methodMatch is null)
{
// No method match, raise an error.
context.ReportDiagnostic(SetupMustBeOnOverridableMethods, literalExpression.GetLocation());
return;
}

// Else check the argument type of the Setup<T>() method and the method found.
var setupMethodSymbol = context.SemanticModel.GetSymbolInfo(invocationExpression, context.CancellationToken);

if (setupMethodSymbol.Symbol is not IMethodSymbol setupMethod)
{
return;
}

if (invocationExpression.Expression is not MemberAccessExpressionSyntax memberAccessExpressionSyntax)
{
return;
}

if (methodMatch.ReturnsVoid)
{
if (setupMethod.TypeArguments.Length > 0)
{
if (memberAccessExpressionSyntax.Name is not GenericNameSyntax genericNameSyntax)
{
return;
}

// The method mocked is void and no generic arguments has been specified in the Setup<T>() method.
context.ReportDiagnostic(SetupReturnTypeMustMatch, genericNameSyntax.TypeArgumentList.Arguments[0].GetLocation());
}

return;
}

if (setupMethod.TypeArguments.Length != 1)
{
// No generic type has been specified in the Setup<T>().
context.ReportDiagnostic(SetupReturnTypeMustMatch, memberAccessExpressionSyntax.Name.GetLocation());
return;
}

// No returns method has been specified with Strict mode. Report the diagnostic issue.
var diagnostic = Diagnostic.Create(Rule, literalExpression.GetLocation());
context.ReportDiagnostic(diagnostic);
if (!SymbolEqualityComparer.Default.Equals(setupMethod.TypeArguments[0], methodMatch.ReturnType))
{
if (memberAccessExpressionSyntax.Name is not GenericNameSyntax genericNameSyntax)
{
return;
}

// The method mocked return a type which does not match the argument type of the Setup<T>() method.
context.ReportDiagnostic(SetupReturnTypeMustMatch, genericNameSyntax.TypeArgumentList.Arguments[0].GetLocation());

return;
}
}
}
}
1 change: 1 addition & 0 deletions src/Moq.Analyzers/Moq.Analyzers.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
1.10.0
- Add new rules:
- PosInfoMoq2014: The delegate in the argument of the Returns() method must return a value with same type of the mocked method.
- PosInfoMoq2015: The Protected().Setup() method must match the return type of the mocked method.

1.10.0
- Add new rules:
Expand Down
Loading

0 comments on commit 0a57f99

Please sign in to comment.