Skip to content

Commit

Permalink
v1.10.0 (#34)
Browse files Browse the repository at this point in the history
* PosInfoMoq2012: The delegate in the argument of the Returns() method must return a value with same type of the mocked method.
* PosInfoMoq2013: The delegate in the argument of the Returns()/ReturnsAsync() method must have the same parameter types of the mocked method/property.
  • Loading branch information
GillesTourreau authored Sep 2, 2024
1 parent ba32035 commit dd853ba
Show file tree
Hide file tree
Showing 10 changed files with 744 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/github-actions-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
type: string
description: The version of the library
required: true
default: 1.9.3
default: 1.10.0
VersionSuffix:
type: string
description: The version suffix of the library (for example rc.1)
Expand Down
2 changes: 2 additions & 0 deletions PosInformatique.Moq.Analyzers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Compilation", "Compilation"
docs\Compilation\PosInfoMoq2009.md = docs\Compilation\PosInfoMoq2009.md
docs\Compilation\PosInfoMoq2010.md = docs\Compilation\PosInfoMoq2010.md
docs\Compilation\PosInfoMoq2011.md = docs\Compilation\PosInfoMoq2011.md
docs\Compilation\PosInfoMoq2012.md = docs\Compilation\PosInfoMoq2012.md
docs\Compilation\PosInfoMoq2013.md = docs\Compilation\PosInfoMoq2013.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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ All the rules of this category should not be disabled (or changed their severity
| [PosInfoMoq2009: `Mock.Of<T>` method must be used only to mock non-sealed class](docs/Compilation/PosInfoMoq2009.md) | The `Mock.Of<T>` method can mock only interfaces or non-`sealed` classes |
| [PosInfoMoq2010: `Mock.Of<T>` method must be used only with types that contains parameterless contructor](docs/Compilation/PosInfoMoq2010.md) | The `Mock.Of<T>` method requires a non-private parameterless contructor |
| [PosInfoMoq2011: Constructor of the mocked class must be accessible.](docs/Compilation/PosInfoMoq2011.md) | The constructor of the instantiate mocked class must non-private. |
| [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. |



63 changes: 63 additions & 0 deletions docs/Compilation/PosInfoMoq2012.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# PosInfoMoq2012: The delegate in the argument of the `Returns()` method must return a value with same type of the mocked method.

| Property | Value |
|-------------------------------------|-------------------------------------------------------------------------|
| **Rule ID** | PosInfoMoq2012 |
| **Title** | The delegate in the argument of the `Returns()` method must return a value with same type of the mocked method. |
| **Category** | Compilation |
| **Default severity** | Error |

## Cause

The delegate in the argument of the `Returns()` must return return a value of the same type as the mocked method or property.

## Rule description

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.

```csharp
[Fact]
public void Test()
{
var validMock = new Mock<IService>();
validMock.Setup(s => s.GetData())
.Returns(() =>
{
return 1234; // OK, the mocked GetData() method return an int.
});
validMock.Setup(s => s.IsAvailable)
.Returns(() =>
{
return true; // OK, the mocked IsAvailable property return a bool.
});

var invalidMock = new Mock<IService>();
invalidMock.Setup(s => s.GetData())
.Returns(() =>
{
return "Foobar"; // Error, the mocked GetData() method must return an int.
});
invalidMock.Setup(s => s.IsAvailable)
.Returns(() =>
{
return "Foobar"; // Error, the mocked IsAvailable property must return a bool.
});
}

public interface IService
{
int GetData();

bool IsAvailable { get; }
}
```

## How to fix violations

To fix a violation of this rule, be sure the lambda expression, anonymous method or method as parameter of the `Returns()`
method returns values with the same type of mocked method/property.

## 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 *"Invalid callback. Setup on method with return type 'xxx' cannot invoke callback with return type 'yyy'."* message.
69 changes: 69 additions & 0 deletions docs/Compilation/PosInfoMoq2013.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# PosInfoMoq2013: The delegate in the argument of the `Returns()`/`ReturnsAsync()` method must have the same parameter types of the mocked method/property.

| Property | Value |
|-------------------------------------|-------------------------------------------------------------------------|
| **Rule ID** | PosInfoMoq2013 |
| **Title** | The delegate in the argument of the `Returns()`/`ReturnsAsync()` method must have the same parameter types of the mocked method/property. |
| **Category** | Compilation |
| **Default severity** | Error |

## Cause

The delegate in the argument of the `Returns()`/`ReturnsAsync()` method must have the same parameter types of the mocked method/property.

## Rule description

The lambda expression, anonymous method or method in the argument of the `Returns()` must have the same parameter types of the mocked method/property.
> NB: Moq allows to pass a delegate with no argument in the `Returns()`/`ReturnsAsync()` method even the setup method contains arguments.
```csharp
[Fact]
public void Test()
{
var validMock = new Mock<IService>();
validMock.Setup(s => s.GetData(1234))
.Returns((int a) => // OK, the mocked GetData() take a int value as argument.
{
return 1234;
});
validMock.Setup(s => s.GetData(1234))
.Returns(() => // OK, Moq allows no arguments.
{
return 1234;
});
validMock.Setup(s => s.IsAvailable) // OK, property don't have arguments.
.Returns(() =>
{
return true;
});

var invalidMock = new Mock<IService>();
invalidMock.Setup(s => s.GetData(1234))
.Returns((string s) => // Error, the mocked GetData() take a int value as argument.
{
return "Foobar";
});
invalidMock.Setup(s => s.IsAvailable)
.Returns((string s) => // Error, mocked property have no arguments.
{
return "Foobar";
});
}

public interface IService
{
int GetData(int id);

bool IsAvailable { get; }
}
```

## How to fix violations

To fix a violation of this rule, be sure the lambda expression, anonymous method or method as parameter of the `Returns()`/`ReturnsAsync()`
method must have the same arguments type of the setup property/method.

## 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 *"Object of type 'xxx' cannot be converted to type 'yyy'."* message.
10 changes: 9 additions & 1 deletion src/Moq.Analyzers/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
## Release 1.9.1
## Release 1.10.0

### New Rules
Rule ID | Category | Severity | Notes
--------|----------|----------|-------
PosInfoMoq2012 | Compilation | Error | The delegate in the argument of the `Returns()` method must return a value with same type of the mocked method.
PosInfoMoq2013 | Compilation | Error | The delegate in the argument of the `Returns()`/`ReturnsAsync()` method must have the same parameter types of the mocked method/property.

## Release 1.9.1

### New Rules
Rule ID | Category | Severity | Notes
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
//-----------------------------------------------------------------------
// <copyright file="ReturnsMethodDelegateMustMatchMockedMethodAnalyzer.cs" company="P.O.S Informatique">
// Copyright (c) P.O.S Informatique. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

namespace PosInformatique.Moq.Analyzers
{
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ReturnsMethodDelegateMustMatchMockedMethodAnalyzer : DiagnosticAnalyzer
{
internal static readonly DiagnosticDescriptor ReturnValueMustMatchRule = new DiagnosticDescriptor(
"PosInfoMoq2012",
"The delegate in the argument of the Returns() method must return a value with same type of the mocked method/property",
"The delegate in the argument of the Returns() method must return a '{0}' type value",
"Compilation",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "The delegate in the argument of the Returns() method must return a value with same type of the mocked method/property.",
helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2012.html");

private static readonly DiagnosticDescriptor ArgumentMustMatchRule = new DiagnosticDescriptor(
"PosInfoMoq2013",
"The delegate in the argument of the Returns()/ReturnsAsync() method must have the same parameter types of the mocked method/property",
"The delegate in the argument of the Returns()/ReturnsAsync() method must have the same parameter types of the mocked method/property",
"Compilation",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "The delegate in the argument of the Returns()/ReturnsAsync() method must have the same parameter types of the mocked method/property.",
helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2012.html");

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(ReturnValueMustMatchRule, ArgumentMustMatchRule);

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();

context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression);
}

private static void Analyze(SyntaxNodeAnalysisContext context)
{
var invocationExpression = (InvocationExpressionSyntax)context.Node;

var moqSymbols = MoqSymbols.FromCompilation(context.Compilation);

if (moqSymbols is null)
{
return;
}

var invocationExpressionSymbol = context.SemanticModel.GetSymbolInfo(invocationExpression, context.CancellationToken);

if (invocationExpressionSymbol.Symbol is not IMethodSymbol methodSymbol)
{
return;
}

if (!moqSymbols.IsReturnsMethod(methodSymbol) && !moqSymbols.IsReturnsAsyncMethod(methodSymbol))
{
return;
}

// Gets the first argument of the Returns() method.
if (invocationExpression.ArgumentList.Arguments.Count != 1)
{
return;
}

var firstArgumentExpression = invocationExpression.ArgumentList.Arguments[0].Expression;

if (firstArgumentExpression is not ParenthesizedLambdaExpressionSyntax delegateMethodSyntax)
{
return;
}

var firstArgumentSymbol = context.SemanticModel.GetSymbolInfo(firstArgumentExpression, context.CancellationToken);

if (firstArgumentSymbol.Symbol is not IMethodSymbol delegateMethodSymbol)
{
return;
}

var moqExpressionAnalyzer = new MoqExpressionAnalyzer(moqSymbols, context.SemanticModel);

// Extract the Setup() method.
var setupMethod = moqExpressionAnalyzer.ExtractSetupMethod(invocationExpression, context.CancellationToken);

if (setupMethod is null)
{
return;
}

// Check the return type
if (!moqSymbols.IsReturnsAsyncMethod(methodSymbol))
{
var expectedReturnType = setupMethod.ReturnType;

if (!moqSymbols.IsAnyType(expectedReturnType) && !SymbolEqualityComparer.Default.Equals(delegateMethodSymbol.ReturnType, expectedReturnType))
{
context.ReportDiagnostic(ReturnValueMustMatchRule, firstArgumentExpression.GetLocation(), expectedReturnType.Name);
}
}

// Check the argument types.
if (setupMethod.IsProperty)
{
if (delegateMethodSymbol.Parameters.Length > 0)
{
// With property, the Returns() method must have no arguments.
context.ReportDiagnostic(ArgumentMustMatchRule, delegateMethodSyntax.ParameterList.GetLocation());
}

return;
}

if (delegateMethodSymbol.Parameters.Length == 0)
{
// No argument in the delegate method, Moq accept it.
return;
}

if (delegateMethodSymbol.Parameters.Length != setupMethod.InvocationArguments.Count)
{
context.ReportDiagnostic(ArgumentMustMatchRule, delegateMethodSyntax.ParameterList.GetLocation());
return;
}

for (var i = 0; i < delegateMethodSymbol.Parameters.Length; i++)
{
if (!SymbolEqualityComparer.Default.Equals(delegateMethodSymbol.Parameters[i].Type, setupMethod.InvocationArguments[i].ParameterSymbol.Type))
{
context.ReportDiagnostic(ArgumentMustMatchRule, delegateMethodSyntax.ParameterList.Parameters[i].GetLocation());
}
}
}
}
}
26 changes: 26 additions & 0 deletions src/Moq.Analyzers/ChainMembersInvocation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,32 @@ public ChainMembersInvocation(IReadOnlyList<ChainMember> members, IReadOnlyList<

public IReadOnlyList<ChainInvocationArgument> InvocationArguments { get; }

public ITypeSymbol ReturnType
{
get
{
if (this.Members[0].Symbol is IMethodSymbol methodSymbol)
{
return methodSymbol.ReturnType;
}

return ((IPropertySymbol)this.Members[0].Symbol).Type;
}
}

public bool IsProperty
{
get
{
if (this.Members[0].Symbol is IMethodSymbol methodSymbol)
{
return false;
}

return true;
}
}

public bool HasSameMembers(ChainMembersInvocation otherChainInvocation)
{
if (this.Members.Count != otherChainInvocation.Members.Count)
Expand Down
9 changes: 7 additions & 2 deletions src/Moq.Analyzers/Moq.Analyzers.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,21 @@
<PackageProjectUrl>https://github.com/PosInformatique/PosInformatique.Moq.Analyzers</PackageProjectUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageReleaseNotes>
1.10.0
- Add new rules:
- PosInfoMoq2012: The delegate in the argument of the Returns() method must return a value with same type of the mocked method.
- PosInfoMoq2013: The delegate in the argument of the Returns()/ReturnsAsync() method must have the same parameter types of the mocked method/property.

1.9.3
- Fix the PosInfoMoq2006 when Setup() a method/property in inherited class.

1.9.2
- Fix the PosInfoMoq1003 to raise warnings when using InSequence() method.
- Fix the PosInfoMoq2003 to raise errors when using InSequence() method.

1.9.1
- Add new rules:
- PosInfoMoq2009: Mock.Of&lt;T&gt; method must be used only to mock non-sealed class
- PosInfoMoq2009: Mock.Of&lt;T&gt; method must be used only to mock non-sealed class
- PosInfoMoq2010: Mock.Of&lt;T&gt; method must be used only with types that contains parameterless contructor
- PosInfoMoq2011: Constructor of the mocked class must be accessible.
- Fix the PosInfoMoq1001 to check the MockBehavior usage to strict.
Expand Down
Loading

0 comments on commit dd853ba

Please sign in to comment.