Skip to content

Commit 1a0f8c5

Browse files
v1.3.0 (#7)
* Fix the PosInfoMoq2000 rule to check the Returns() / ReturnsAsync() method has been called for the mocked properties. (fixes #6). * Add the PosInfoMoq2003 rule to check the Callback() signature method (fixes #3).
1 parent 2b74a61 commit 1a0f8c5

15 files changed

Lines changed: 564 additions & 179 deletions

PosInformatique.Moq.Analyzers.sln

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{3C20D95F-A
3333
EndProject
3434
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Design", "Design", "{815BE8D0-C7D5-4B4E-82E0-DE29C11F258E}"
3535
ProjectSection(SolutionItems) = preProject
36-
docs\design\PosInfoMoq1001.md = docs\design\PosInfoMoq1001.md
3736
docs\design\PosInfoMoq1000.md = docs\design\PosInfoMoq1000.md
37+
docs\design\PosInfoMoq1001.md = docs\design\PosInfoMoq1001.md
3838
EndProjectSection
3939
EndProject
4040
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Compilation", "Compilation", "{D9C84D36-7F9C-4EFB-BE6F-9F7A05FE957D}"
4141
ProjectSection(SolutionItems) = preProject
4242
docs\Compilation\PosInfoMoq2000.md = docs\Compilation\PosInfoMoq2000.md
4343
docs\Compilation\PosInfoMoq2001.md = docs\Compilation\PosInfoMoq2001.md
4444
docs\Compilation\PosInfoMoq2002.md = docs\Compilation\PosInfoMoq2002.md
45+
docs\Compilation\PosInfoMoq2003.md = docs\Compilation\PosInfoMoq2003.md
4546
EndProjectSection
4647
EndProject
4748
Global

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,5 @@ All the rules of this category should not be disabled (or changed their severity
4040
| [PosInfoMoq2000: The `Returns()` or `ReturnsAsync()` methods must be call for Strict mocks](docs/Compilation/PosInfoMoq2000.md) | When a `Mock<T>` has been defined with the `Strict` behavior, the `Returns()` or `ReturnsAsync()` method must be called when setup a method to mock which returns a value. |
4141
| [PosInfoMoq2001: The `Setup()` method must be used only on overridable members](docs/Compilation/PosInfoMoq2001.md)) | The `Setup()` method must be applied only for overridable members. |
4242
| [PosInfoMoq2002: `Mock<T>` class can be used only to mock non-sealed class](docs/Compilation/PosInfoMoq2002.md) | The `Mock<T>` can mock only interfaces or non-`sealed` classes. |
43+
| [PosInfoMoq2003: The `Callback()` delegate expression must match the signature of the mocked method](docs/Compilation/PosInfoMoq2003.md) | The delegate in the argument of the `Callback()` method must match the signature of the mocked method. |
4344

build/azure-pipelines-release.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ parameters:
22
- name: VersionPrefix
33
displayName: The version of the library
44
type: string
5-
default: 1.2.0
5+
default: 1.3.0
66
- name: VersionSuffix
77
displayName: The version suffix of the library (rc.1). Use a space ' ' if no suffix.
88
type: string

docs/Compilation/PosInfoMoq2003.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# PosInfoMoq2003: The `Callback()` delegate expression must match the signature of the mocked method
2+
3+
| Property | Value |
4+
|-------------------------------------|-----------------------------------------------------------------------------------------|
5+
| **Rule ID** | PosInfoMoq2003 |
6+
| **Title** | The `Callback()` delegate expression must match the signature of the mocked method |
7+
| **Category** | Compilation |
8+
| **Default severity** | Error |
9+
10+
## Cause
11+
12+
The delegate in the argument of the `Callback()` method must match the signature of the mocked method.
13+
14+
## Rule description
15+
16+
The lambda expression in the argument of the `Callback()` method must match the signature of the mocked method.
17+
18+
For example, the `Callback()` have a lambda expression with the `(string, double)` signature
19+
which does not match the `GetData()` mocked method which have the `(string, int)` signature.
20+
21+
```csharp
22+
[Fact]
23+
public void Test()
24+
{
25+
var service = new Mock<Service>();
26+
service.Setup(s => s.GetData("TOURREAU", 1234))
27+
.Callback((string n, double age) => // Different signature of the GetData() method.
28+
{
29+
// ...
30+
})
31+
.Returns(10);
32+
}
33+
34+
public interface IService
35+
{
36+
public int GetData(string name, int age) { }
37+
}
38+
```
39+
40+
## How to fix violations
41+
42+
To fix a violation of this rule, be sure to use the mocked method signature in the `Callback()` method.
43+
44+
## When to suppress warnings
45+
46+
Do not suppress an error from this rule. If bypassed, the execution of the unit test will be failed with a `MoqException`
47+
thrown with the *"Invalid callback. Setup on method with parameters (xxx) cannot invoke callback with parameters (yyy)."* message.

src/Moq.Analyzers/AnalyzerReleases.Shipped.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
## Release 1.2.0
1+
## Release 1.3.0
2+
3+
### New Rules
4+
5+
Rule ID | Category | Severity | Notes
6+
--------|----------|----------|--------------------
7+
PosInfoMoq2003 | Compilation | Error | The `Callback()` delegate expression must match the signature of the mocked method.
8+
9+
## Release 1.2.0
210

311
### New Rules
412

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
//-----------------------------------------------------------------------
2+
// <copyright file="CallBackDelegateMustMatchMockedMethodAnalyzer.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 CallBackDelegateMustMatchMockedMethodAnalyzer : DiagnosticAnalyzer
17+
{
18+
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
19+
"PosInfoMoq2003",
20+
"The Callback() delegate expression must match the signature of the mocked method",
21+
"The Callback() delegate expression must match the signature of the mocked method",
22+
"Compilation",
23+
DiagnosticSeverity.Error,
24+
isEnabledByDefault: true,
25+
description: "The Callback() delegate expression must match the signature of the mocked method.");
26+
27+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
28+
29+
public override void Initialize(AnalysisContext context)
30+
{
31+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
32+
context.EnableConcurrentExecution();
33+
34+
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression);
35+
}
36+
37+
private static void Analyze(SyntaxNodeAnalysisContext context)
38+
{
39+
var invocationExpression = (InvocationExpressionSyntax)context.Node;
40+
41+
var moqSymbols = MoqSymbols.FromCompilation(context.Compilation);
42+
43+
if (moqSymbols is null)
44+
{
45+
return;
46+
}
47+
48+
var moqExpressionAnalyzer = new MoqExpressionAnalyzer(context.SemanticModel);
49+
50+
// Try to determine if the invocation expression is a Callback() expression.
51+
var methodSymbol = context.SemanticModel.GetSymbolInfo(invocationExpression);
52+
53+
if (!moqSymbols.IsCallback(methodSymbol.Symbol))
54+
{
55+
return;
56+
}
57+
58+
// If yes, we extract the lambda expression of it.
59+
var callBackLambdaExpressionSymbol = moqExpressionAnalyzer.ExtractCallBackLambdaExpressionMethod(invocationExpression, out var lambdaExpression);
60+
61+
if (callBackLambdaExpressionSymbol is null)
62+
{
63+
return;
64+
}
65+
66+
// Check each CallBack() method for the following calls.
67+
var followingMethods = invocationExpression.DescendantNodes().OfType<InvocationExpressionSyntax>();
68+
69+
foreach (var followingMethod in followingMethods)
70+
{
71+
// Find the symbol of the mocked method (if not symbol found, it is mean we Setup() method that not currently compile)
72+
// so we skip the analysis.
73+
var mockedMethod = moqExpressionAnalyzer.ExtractSetupMethod(followingMethod, out var _);
74+
75+
if (mockedMethod is null)
76+
{
77+
continue;
78+
}
79+
80+
// Compare the parameters between the mocked method and lambda expression in the CallBack() method.
81+
// 1- Compare the number of the parameters
82+
if (callBackLambdaExpressionSymbol.Parameters.Length != mockedMethod.Parameters.Length)
83+
{
84+
var diagnostic = Diagnostic.Create(Rule, lambdaExpression!.ParameterList.GetLocation());
85+
context.ReportDiagnostic(diagnostic);
86+
87+
continue;
88+
}
89+
90+
// 2- Iterate for each parameter
91+
for (var i = 0; i < callBackLambdaExpressionSymbol.Parameters.Length; i++)
92+
{
93+
// Special case, if the argument is IsAnyType
94+
if (moqSymbols.IsAnyType(mockedMethod.Parameters[i].Type))
95+
{
96+
// The callback parameter associated must be an object.
97+
if (callBackLambdaExpressionSymbol.Parameters[i].Type.SpecialType != SpecialType.System_Object)
98+
{
99+
var diagnostic = Diagnostic.Create(Rule, lambdaExpression!.ParameterList.Parameters[i].GetLocation());
100+
context.ReportDiagnostic(diagnostic);
101+
102+
continue;
103+
}
104+
}
105+
else if (!SymbolEqualityComparer.Default.Equals(callBackLambdaExpressionSymbol.Parameters[i].Type, mockedMethod.Parameters[i].Type))
106+
{
107+
var diagnostic = Diagnostic.Create(Rule, lambdaExpression!.ParameterList.Parameters[i].GetLocation());
108+
context.ReportDiagnostic(diagnostic);
109+
110+
continue;
111+
}
112+
}
113+
}
114+
}
115+
}
116+
}

src/Moq.Analyzers/Moq.Analyzers.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,16 @@
3333
</PropertyGroup>
3434

3535
<ItemGroup>
36-
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
37-
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.7.0" />
36+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
37+
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.8.0" />
3838
</ItemGroup>
3939

4040
<ItemGroup>
4141
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
4242
</ItemGroup>
4343

4444
<ItemGroup>
45-
<None Include="Icon.png" Pack="true" PackagePath=""/>
45+
<None Include="Icon.png" Pack="true" PackagePath="" />
4646
</ItemGroup>
4747

4848
<Target Name="_AddAnalyzersToOutput">

src/Moq.Analyzers/MoqExpressionAnalyzer.cs

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -172,19 +172,31 @@ public bool IsStrictBehavior(MoqSymbols moqSymbols, IdentifierNameSyntax localVa
172172
return null;
173173
}
174174

175-
if (lambdaExpression.Body is not InvocationExpressionSyntax methodExpression)
175+
var methodSymbolInfo = this.semanticModel.GetSymbolInfo(lambdaExpression.Body);
176+
177+
if (methodSymbolInfo.Symbol is IMethodSymbol methodSymbol)
176178
{
177-
return null;
179+
return methodSymbol.ReturnType;
178180
}
179181

180-
var methodSymbolInfo = this.semanticModel.GetSymbolInfo(methodExpression);
182+
if (methodSymbolInfo.Symbol is IPropertySymbol propertySymbol)
183+
{
184+
return propertySymbol.Type;
185+
}
186+
187+
return null;
188+
}
189+
190+
public IMethodSymbol? ExtractSetupMethod(InvocationExpressionSyntax invocationExpression, out NameSyntax? memberIdentifierName)
191+
{
192+
var symbol = this.ExtractSetupMember(invocationExpression, out memberIdentifierName);
181193

182-
if (methodSymbolInfo.Symbol is not IMethodSymbol methodSymbol)
194+
if (symbol is not IMethodSymbol methodSymbol)
183195
{
184196
return null;
185197
}
186198

187-
return methodSymbol.ReturnType;
199+
return methodSymbol;
188200
}
189201

190202
public ISymbol? ExtractSetupMember(InvocationExpressionSyntax invocationExpression, out NameSyntax? memberIdentifierName)
@@ -231,6 +243,32 @@ public bool IsStrictBehavior(MoqSymbols moqSymbols, IdentifierNameSyntax localVa
231243
return symbol.Symbol;
232244
}
233245

246+
public IMethodSymbol? ExtractCallBackLambdaExpressionMethod(InvocationExpressionSyntax invocationExpression, out ParenthesizedLambdaExpressionSyntax? lambdaExpression)
247+
{
248+
lambdaExpression = null;
249+
250+
if (invocationExpression.ArgumentList.Arguments.Count != 1)
251+
{
252+
return null;
253+
}
254+
255+
if (invocationExpression.ArgumentList.Arguments[0].Expression is not ParenthesizedLambdaExpressionSyntax lambdaExpressionFound)
256+
{
257+
return null;
258+
}
259+
260+
var symbol = this.semanticModel.GetSymbolInfo(lambdaExpressionFound);
261+
262+
if (symbol.Symbol is not IMethodSymbol methodSymbol)
263+
{
264+
return null;
265+
}
266+
267+
lambdaExpression = lambdaExpressionFound;
268+
269+
return methodSymbol;
270+
}
271+
234272
private static ObjectCreationExpressionSyntax? FindMockCreation(BlockSyntax block, string variableName)
235273
{
236274
foreach (var statement in block.Statements.OfType<LocalDeclarationStatementSyntax>())

src/Moq.Analyzers/MoqSymbols.cs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@ internal sealed class MoqSymbols
1818

1919
private readonly ISymbol mockBehaviorStrictField;
2020

21-
private MoqSymbols(INamedTypeSymbol mockClass, INamedTypeSymbol mockBehaviorEnum)
21+
private readonly ISymbol isAnyTypeClass;
22+
23+
private MoqSymbols(INamedTypeSymbol mockClass, INamedTypeSymbol mockBehaviorEnum, ISymbol isAnyTypeClass)
2224
{
2325
this.mockClass = mockClass;
2426
this.mockBehaviorEnum = mockBehaviorEnum;
27+
this.isAnyTypeClass = isAnyTypeClass;
2528

2629
this.setupMethods = mockClass.GetMembers("Setup").OfType<IMethodSymbol>().ToArray();
2730
this.mockBehaviorStrictField = mockBehaviorEnum.GetMembers("Strict").First();
@@ -43,7 +46,24 @@ private MoqSymbols(INamedTypeSymbol mockClass, INamedTypeSymbol mockBehaviorEnum
4346
return null;
4447
}
4548

46-
return new MoqSymbols(mockClass, mockBehaviorEnum);
49+
var isAnyTypeClass = compilation.GetTypeByMetadataName("Moq.It+IsAnyType");
50+
51+
if (isAnyTypeClass is null)
52+
{
53+
return null;
54+
}
55+
56+
return new MoqSymbols(mockClass, mockBehaviorEnum, isAnyTypeClass);
57+
}
58+
59+
public bool IsAnyType(ITypeSymbol symbol)
60+
{
61+
if (!SymbolEqualityComparer.Default.Equals(symbol, this.isAnyTypeClass))
62+
{
63+
return false;
64+
}
65+
66+
return true;
4767
}
4868

4969
public bool IsMock(ISymbol? symbol)
@@ -81,6 +101,21 @@ public bool IsSetupMethod(ISymbol? symbol)
81101
return false;
82102
}
83103

104+
public bool IsCallback(ISymbol? symbol)
105+
{
106+
if (symbol is null)
107+
{
108+
return false;
109+
}
110+
111+
if (symbol.Name != "Callback")
112+
{
113+
return false;
114+
}
115+
116+
return true;
117+
}
118+
84119
public bool IsReturnsMethod(ISymbol? symbol)
85120
{
86121
if (symbol is null)

0 commit comments

Comments
 (0)