Skip to content

Commit 74d3525

Browse files
v1.12.0 (#45)
* Fix warnings. * Add the rule PosInfo1006 to check the It.IsAny<T>() match the arguments of mocked method. * Add the support of the It.Is<T>() arguments. * Fix warnings. * Update the version.
1 parent 2a5b492 commit 74d3525

10 files changed

Lines changed: 340 additions & 7 deletions

.editorconfig

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[*.cs]
22

3-
# StyleCop
3+
## StyleCop
44

55
# SA1600: Elements should be documented
66
dotnet_diagnostic.SA1600.severity = none
@@ -10,3 +10,7 @@ dotnet_diagnostic.SA1601.severity = none
1010

1111
# SA1602: Enumeration items should be documented
1212
dotnet_diagnostic.SA1602.severity = none
13+
14+
## IDE
15+
# IDE0130: Namespace does not match folder structure
16+
dotnet_diagnostic.IDE0130.severity = none

.github/workflows/github-actions-release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77
type: string
88
description: The version of the library
99
required: true
10-
default: 1.11.0
10+
default: 1.12.0
1111
VersionSuffix:
1212
type: string
1313
description: The version suffix of the library (for example rc.1)

PosInformatique.Moq.Analyzers.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Design", "Design", "{815BE8
3434
docs\Design\PosInfoMoq1003.md = docs\Design\PosInfoMoq1003.md
3535
docs\Design\PosInfoMoq1004.md = docs\Design\PosInfoMoq1004.md
3636
docs\Design\PosInfoMoq1005.md = docs\Design\PosInfoMoq1005.md
37+
docs\Design\PosInfoMoq1006.md = docs\Design\PosInfoMoq1006.md
3738
EndProjectSection
3839
EndProject
3940
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Compilation", "Compilation", "{D9C84D36-7F9C-4EFB-BE6F-9F7A05FE957D}"

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Design rules used to make your unit tests more strongly strict.
3232
| [PosInfoMoq1003: The `Callback()` method should be used to check the parameters when mocking a method with `It.IsAny<T>()` arguments](docs/Design/PosInfoMoq1003.md) | When a mocked method contains a `It.IsAny<T>()` argument, the related parameter should be checked in the `Callback()` method. |
3333
| [PosInfoMoq1004: The `Callback()` parameter should not be ignored if it has been setup as an `It.IsAny<T>()` argument](docs/Design/PosInfoMoq1004.md) | When a mocked method contains a `It.IsAny<T>()` argument, the related parameter should not be ignored in the `Callback()` method. |
3434
| [PosInfoMoq1005: Defines the generic argument of the `SetupSet()` method with the type of the mocked property](docs/Design/PosInfoMoq1005.md) | When mocking the setter of a property, use the `SetupSet<TProperty>()` method version. |
35+
| [PosInfoMoq1006: The `It.IsAny<T>()` or `It.Is<T>()` arguments must match the parameters of the mocked method.](docs/Design/PosInfoMoq1006.md) | When setting up a method using `It.IsAny<T>()` or `It.Is<T>()` as arguments, the type `T` must exactly match the parameters of the configured method. |
3536

3637
### Compilation
3738

docs/Design/PosInfoMoq1006.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# PosInfoMoq1006: The `It.IsAny<T>()` or `It.Is<T>()` arguments must match the parameters of the mocked method.
2+
3+
| Property | Value |
4+
|-------------------------------------|------------------------------------------------------------------------------------------------------|
5+
| **Rule ID** | PosInfoMoq1006 |
6+
| **Title** | The `It.IsAny<T>()` or `It.Is<T>()` arguments must match the parameters of the mocked method. |
7+
| **Category** | Design |
8+
| **Default severity** | Warning |
9+
10+
## Cause
11+
12+
When setting up a method using `It.IsAny<T>()` or `It.Is<T>()` as arguments,
13+
the type `T` must exactly match the parameters of the configured method.
14+
15+
## Rule description
16+
17+
Although the compiler checks the validity between the parameters and arguments of the configured method,
18+
the type specified in `It.IsAny<T>()` or `It.Is<T>()` must still match the method parameters.
19+
This is especially important when using nullable `struct` parameters, which can easily cause confusion.
20+
21+
For example, consider the following test code:
22+
23+
```csharp
24+
[Fact]
25+
public interface IRepository
26+
{
27+
void DeleteUser(int? id)
28+
}
29+
```
30+
31+
If you mock the method from the `IRepository` type and use `It.IsAny<int>()` as the argument,
32+
the following code will throw an exception because `It.IsAny<int>()` is treated as the integer value `0`:
33+
34+
```csharp
35+
[Fact]
36+
public void DeleteUser()
37+
{
38+
var repository = new Mock<IRepository>(MockBehavior.Strict);
39+
repository.Setup(It.IsAny<int>());
40+
41+
repository.Object.DeleteUser(null); // Will raise an exception, because It.IsAny<int>() ('0') is different of `null`.
42+
}
43+
```
44+
45+
Instead, you should use `It.IsAny<int?>()` with a nullable `int?`,
46+
which makes the method configuration explicit and avoids mismatches:
47+
48+
```csharp
49+
[Fact]
50+
public void DeleteUser()
51+
{
52+
var repository = new Mock<IRepository>(MockBehavior.Strict);
53+
repository.Setup(It.IsAny<int?>());
54+
55+
repository.Object.DeleteUser(null);
56+
}
57+
```
58+
59+
## How to fix violations
60+
61+
To fix a violation of this rule, ensure the type arguments used with `It.IsAny<T>()` or `It.Is<T>()` exactly match the parameters of the configured method.
62+
63+
## When to suppress warnings
64+
65+
You may suppress warnings from this rule, but for better readability and maintainability,
66+
it is strongly recommended to always use matching type arguments in `It.IsAny<T>()` or `It.Is<T>()`.

src/Moq.Analyzers/AnalyzerReleases.Shipped.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
## Release 1.11.0
1+
## Release 1.12.0
2+
3+
### New Rules
4+
Rule ID | Category | Severity | Notes
5+
--------|----------|----------|-------
6+
PosInfoMoq1006 | Design | Warning | ItArgumentsMustMatchMockedMethodArgumentsAnalyzer, [Documentation](https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Design/PosInfoMoq1006.html)
7+
8+
## Release 1.11.0
29

310
### New Rules
411
Rule ID | Category | Severity | Notes
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
//-----------------------------------------------------------------------
2+
// <copyright file="ItArgumentsMustMatchMockedMethodArgumentsAnalyzer.cs" company="P.O.S Informatique">
3+
// Copyright (c) P.O.S Informatique. All rights reserved.
4+
// </copyright>
5+
//-----------------------------------------------------------------------
6+
7+
namespace PosInformatique.Moq.Analyzers
8+
{
9+
using System.Collections.Immutable;
10+
using Microsoft.CodeAnalysis;
11+
using Microsoft.CodeAnalysis.CSharp;
12+
using Microsoft.CodeAnalysis.CSharp.Syntax;
13+
using Microsoft.CodeAnalysis.Diagnostics;
14+
15+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
16+
public class ItArgumentsMustMatchMockedMethodArgumentsAnalyzer : DiagnosticAnalyzer
17+
{
18+
internal static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
19+
"PosInfoMoq1006",
20+
"The It.IsAny<T>() or It.Is<T>() arguments must match the parameters of the mocked method",
21+
"The It.IsAny<T>() or It.Is<T>() arguments must match the parameters of the mocked method",
22+
"Design",
23+
DiagnosticSeverity.Warning,
24+
isEnabledByDefault: true,
25+
description: "The It.IsAny<T>() or It.Is<T>() arguments must match the parameters of the mocked method.",
26+
helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Design/PosInfoMoq1006.html");
27+
28+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
29+
30+
public override void Initialize(AnalysisContext context)
31+
{
32+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
33+
context.EnableConcurrentExecution();
34+
35+
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression);
36+
}
37+
38+
private static void Analyze(SyntaxNodeAnalysisContext context)
39+
{
40+
var invocationExpression = (InvocationExpressionSyntax)context.Node;
41+
42+
var moqSymbols = MoqSymbols.FromCompilation(context.Compilation);
43+
44+
if (moqSymbols is null)
45+
{
46+
return;
47+
}
48+
49+
// Extracts the setup method from the Callback() method call.
50+
var moqExpressionAnalyzer = new MoqExpressionAnalyzer(moqSymbols, context.SemanticModel);
51+
var setupMethod = moqExpressionAnalyzer.ExtractSetupMethod(invocationExpression, context.CancellationToken);
52+
53+
if (setupMethod is null)
54+
{
55+
return;
56+
}
57+
58+
// Iterate for each parameter
59+
for (var i = 0; i < setupMethod.InvocationArguments.Count; i++)
60+
{
61+
var invocationArgument = setupMethod.InvocationArguments[i];
62+
63+
// Check if the parameter is "It.IsAny<xxx>()"
64+
var itIsAnyType = moqSymbols.GetItIsAnyType(invocationArgument.Symbol);
65+
if (itIsAnyType is not null)
66+
{
67+
if (!SymbolEqualityComparer.Default.Equals(itIsAnyType, invocationArgument.ParameterSymbol.Type))
68+
{
69+
context.ReportDiagnostic(Rule, invocationArgument.Syntax.GetLocation());
70+
continue;
71+
}
72+
}
73+
74+
// Check if the parameter is "It.Is<xxx>()"
75+
var itIsType = moqSymbols.GetItIsType(invocationArgument.Symbol);
76+
if (itIsType is not null)
77+
{
78+
if (!SymbolEqualityComparer.Default.Equals(itIsType, invocationArgument.ParameterSymbol.Type))
79+
{
80+
context.ReportDiagnostic(Rule, invocationArgument.Syntax.GetLocation());
81+
continue;
82+
}
83+
}
84+
}
85+
}
86+
}
87+
}

src/Moq.Analyzers/MoqSymbols.cs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ internal sealed class MoqSymbols
3535

3636
private readonly Lazy<INamedTypeSymbol> isAnyTypeClass;
3737

38-
private readonly Lazy<ISymbol> isAnyMethod;
38+
private readonly Lazy<IReadOnlyList<ISymbol>> itIsMethods;
39+
40+
private readonly Lazy<ISymbol> itIsAnyMethod;
3941

4042
private readonly Lazy<ISymbol> asMethod;
4143

@@ -55,7 +57,8 @@ private MoqSymbols(INamedTypeSymbol mockGenericClass, Compilation compilation)
5557

5658
this.mockBehaviorEnum = new Lazy<INamedTypeSymbol>(() => compilation.GetTypeByMetadataName("Moq.MockBehavior")!);
5759
this.isAnyTypeClass = new Lazy<INamedTypeSymbol>(() => compilation.GetTypeByMetadataName("Moq.It+IsAnyType")!);
58-
this.isAnyMethod = new Lazy<ISymbol>(() => compilation.GetTypeByMetadataName("Moq.It")!.GetMembers("IsAny").Single());
60+
this.itIsMethods = new Lazy<IReadOnlyList<ISymbol>>(() => compilation.GetTypeByMetadataName("Moq.It")!.GetMembers("Is").ToArray());
61+
this.itIsAnyMethod = new Lazy<ISymbol>(() => compilation.GetTypeByMetadataName("Moq.It")!.GetMembers("IsAny").Single());
5962
this.verifiesInterface = new Lazy<INamedTypeSymbol>(() => compilation.GetTypeByMetadataName("Moq.Language.IVerifies")!);
6063

6164
this.setupMethods = new Lazy<IReadOnlyList<IMethodSymbol>>(() => mockGenericClass.GetMembers("Setup").Concat(setupConditionResultInterface.Value.GetMembers("Setup")).OfType<IMethodSymbol>().ToArray());
@@ -99,21 +102,54 @@ public bool IsAnyType(ITypeSymbol symbol)
99102
return true;
100103
}
101104

105+
public ISymbol? GetItIsType(ISymbol? symbol)
106+
{
107+
if (symbol is not IMethodSymbol methodSymbol)
108+
{
109+
return null;
110+
}
111+
112+
foreach (var itIsMethod in this.itIsMethods.Value)
113+
{
114+
if (SymbolEqualityComparer.Default.Equals(symbol.OriginalDefinition, itIsMethod))
115+
{
116+
return methodSymbol.TypeArguments[0];
117+
}
118+
}
119+
120+
return null;
121+
}
122+
102123
public bool IsItIsAny(ISymbol? symbol)
103124
{
104125
if (symbol is null)
105126
{
106127
return false;
107128
}
108129

109-
if (!SymbolEqualityComparer.Default.Equals(symbol.OriginalDefinition, this.isAnyMethod.Value))
130+
if (!SymbolEqualityComparer.Default.Equals(symbol.OriginalDefinition, this.itIsAnyMethod.Value))
110131
{
111132
return false;
112133
}
113134

114135
return true;
115136
}
116137

138+
public ISymbol? GetItIsAnyType(ISymbol? symbol)
139+
{
140+
if (symbol is not IMethodSymbol methodSymbol)
141+
{
142+
return null;
143+
}
144+
145+
if (!SymbolEqualityComparer.Default.Equals(symbol.OriginalDefinition, this.itIsAnyMethod.Value))
146+
{
147+
return null;
148+
}
149+
150+
return methodSymbol.TypeArguments[0];
151+
}
152+
117153
public bool IsMock(ISymbol? symbol)
118154
{
119155
if (symbol is null)

0 commit comments

Comments
 (0)