Skip to content

Commit dd853ba

Browse files
v1.10.0 (#34)
* 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 parent ba32035 commit dd853ba

10 files changed

Lines changed: 744 additions & 4 deletions

.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.9.3
10+
default: 1.10.0
1111
VersionSuffix:
1212
type: string
1313
description: The version suffix of the library (for example rc.1)

PosInformatique.Moq.Analyzers.sln

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Compilation", "Compilation"
4949
docs\Compilation\PosInfoMoq2009.md = docs\Compilation\PosInfoMoq2009.md
5050
docs\Compilation\PosInfoMoq2010.md = docs\Compilation\PosInfoMoq2010.md
5151
docs\Compilation\PosInfoMoq2011.md = docs\Compilation\PosInfoMoq2011.md
52+
docs\Compilation\PosInfoMoq2012.md = docs\Compilation\PosInfoMoq2012.md
53+
docs\Compilation\PosInfoMoq2013.md = docs\Compilation\PosInfoMoq2013.md
5254
EndProjectSection
5355
EndProject
5456
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Analyzers.Sandbox", "tests\Moq.Analyzers.Sandbox\Moq.Analyzers.Sandbox.csproj", "{07F970A1-1477-4D4C-B233-C9B4DA6E3AD6}"

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ All the rules of this category should not be disabled (or changed their severity
5151
| [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 |
5252
| [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 |
5353
| [PosInfoMoq2011: Constructor of the mocked class must be accessible.](docs/Compilation/PosInfoMoq2011.md) | The constructor of the instantiate mocked class must non-private. |
54+
| [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. |
55+
| [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. |
5456

5557

5658

docs/Compilation/PosInfoMoq2012.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# PosInfoMoq2012: The delegate in the argument of the `Returns()` method must return a value with same type of the mocked method.
2+
3+
| Property | Value |
4+
|-------------------------------------|-------------------------------------------------------------------------|
5+
| **Rule ID** | PosInfoMoq2012 |
6+
| **Title** | The delegate in the argument of the `Returns()` method must return a value with same type of the mocked method. |
7+
| **Category** | Compilation |
8+
| **Default severity** | Error |
9+
10+
## Cause
11+
12+
The delegate in the argument of the `Returns()` must return return a value of the same type as the mocked method or property.
13+
14+
## Rule description
15+
16+
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.
17+
18+
```csharp
19+
[Fact]
20+
public void Test()
21+
{
22+
var validMock = new Mock<IService>();
23+
validMock.Setup(s => s.GetData())
24+
.Returns(() =>
25+
{
26+
return 1234; // OK, the mocked GetData() method return an int.
27+
});
28+
validMock.Setup(s => s.IsAvailable)
29+
.Returns(() =>
30+
{
31+
return true; // OK, the mocked IsAvailable property return a bool.
32+
});
33+
34+
var invalidMock = new Mock<IService>();
35+
invalidMock.Setup(s => s.GetData())
36+
.Returns(() =>
37+
{
38+
return "Foobar"; // Error, the mocked GetData() method must return an int.
39+
});
40+
invalidMock.Setup(s => s.IsAvailable)
41+
.Returns(() =>
42+
{
43+
return "Foobar"; // Error, the mocked IsAvailable property must return a bool.
44+
});
45+
}
46+
47+
public interface IService
48+
{
49+
int GetData();
50+
51+
bool IsAvailable { get; }
52+
}
53+
```
54+
55+
## How to fix violations
56+
57+
To fix a violation of this rule, be sure the lambda expression, anonymous method or method as parameter of the `Returns()`
58+
method returns values with the same type of mocked method/property.
59+
60+
## When to suppress warnings
61+
62+
Do not suppress an error from this rule. If bypassed, the execution of the unit test will be failed with a `ArgumentException`
63+
thrown with the *"Invalid callback. Setup on method with return type 'xxx' cannot invoke callback with return type 'yyy'."* message.

docs/Compilation/PosInfoMoq2013.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# PosInfoMoq2013: The delegate in the argument of the `Returns()`/`ReturnsAsync()` method must have the same parameter types of the mocked method/property.
2+
3+
| Property | Value |
4+
|-------------------------------------|-------------------------------------------------------------------------|
5+
| **Rule ID** | PosInfoMoq2013 |
6+
| **Title** | The delegate in the argument of the `Returns()`/`ReturnsAsync()` method must have the same parameter types of the mocked method/property. |
7+
| **Category** | Compilation |
8+
| **Default severity** | Error |
9+
10+
## Cause
11+
12+
The delegate in the argument of the `Returns()`/`ReturnsAsync()` method must have the same parameter types of the mocked method/property.
13+
14+
## Rule description
15+
16+
The lambda expression, anonymous method or method in the argument of the `Returns()` must have the same parameter types of the mocked method/property.
17+
> NB: Moq allows to pass a delegate with no argument in the `Returns()`/`ReturnsAsync()` method even the setup method contains arguments.
18+
19+
```csharp
20+
[Fact]
21+
public void Test()
22+
{
23+
var validMock = new Mock<IService>();
24+
validMock.Setup(s => s.GetData(1234))
25+
.Returns((int a) => // OK, the mocked GetData() take a int value as argument.
26+
{
27+
return 1234;
28+
});
29+
validMock.Setup(s => s.GetData(1234))
30+
.Returns(() => // OK, Moq allows no arguments.
31+
{
32+
return 1234;
33+
});
34+
validMock.Setup(s => s.IsAvailable) // OK, property don't have arguments.
35+
.Returns(() =>
36+
{
37+
return true;
38+
});
39+
40+
var invalidMock = new Mock<IService>();
41+
invalidMock.Setup(s => s.GetData(1234))
42+
.Returns((string s) => // Error, the mocked GetData() take a int value as argument.
43+
{
44+
return "Foobar";
45+
});
46+
invalidMock.Setup(s => s.IsAvailable)
47+
.Returns((string s) => // Error, mocked property have no arguments.
48+
{
49+
return "Foobar";
50+
});
51+
}
52+
53+
public interface IService
54+
{
55+
int GetData(int id);
56+
57+
bool IsAvailable { get; }
58+
}
59+
```
60+
61+
## How to fix violations
62+
63+
To fix a violation of this rule, be sure the lambda expression, anonymous method or method as parameter of the `Returns()`/`ReturnsAsync()`
64+
method must have the same arguments type of the setup property/method.
65+
66+
## When to suppress warnings
67+
68+
Do not suppress an error from this rule. If bypassed, the execution of the unit test will be failed with a `ArgumentException`
69+
thrown with the *"Object of type 'xxx' cannot be converted to type '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.9.1
1+
## Release 1.10.0
2+
3+
### New Rules
4+
Rule ID | Category | Severity | Notes
5+
--------|----------|----------|-------
6+
PosInfoMoq2012 | Compilation | Error | The delegate in the argument of the `Returns()` method must return a value with same type of the mocked method.
7+
PosInfoMoq2013 | Compilation | Error | The delegate in the argument of the `Returns()`/`ReturnsAsync()` method must have the same parameter types of the mocked method/property.
8+
9+
## Release 1.9.1
210

311
### New Rules
412
Rule ID | Category | Severity | Notes
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
//-----------------------------------------------------------------------
2+
// <copyright file="ReturnsMethodDelegateMustMatchMockedMethodAnalyzer.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 ReturnsMethodDelegateMustMatchMockedMethodAnalyzer : DiagnosticAnalyzer
17+
{
18+
internal static readonly DiagnosticDescriptor ReturnValueMustMatchRule = new DiagnosticDescriptor(
19+
"PosInfoMoq2012",
20+
"The delegate in the argument of the Returns() method must return a value with same type of the mocked method/property",
21+
"The delegate in the argument of the Returns() method must return a '{0}' type value",
22+
"Compilation",
23+
DiagnosticSeverity.Error,
24+
isEnabledByDefault: true,
25+
description: "The delegate in the argument of the Returns() method must return a value with same type of the mocked method/property.",
26+
helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2012.html");
27+
28+
private static readonly DiagnosticDescriptor ArgumentMustMatchRule = new DiagnosticDescriptor(
29+
"PosInfoMoq2013",
30+
"The delegate in the argument of the Returns()/ReturnsAsync() method must have the same parameter types of the mocked method/property",
31+
"The delegate in the argument of the Returns()/ReturnsAsync() method must have the same parameter types of the mocked method/property",
32+
"Compilation",
33+
DiagnosticSeverity.Error,
34+
isEnabledByDefault: true,
35+
description: "The delegate in the argument of the Returns()/ReturnsAsync() method must have the same parameter types of the mocked method/property.",
36+
helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2012.html");
37+
38+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(ReturnValueMustMatchRule, ArgumentMustMatchRule);
39+
40+
public override void Initialize(AnalysisContext context)
41+
{
42+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
43+
context.EnableConcurrentExecution();
44+
45+
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression);
46+
}
47+
48+
private static void Analyze(SyntaxNodeAnalysisContext context)
49+
{
50+
var invocationExpression = (InvocationExpressionSyntax)context.Node;
51+
52+
var moqSymbols = MoqSymbols.FromCompilation(context.Compilation);
53+
54+
if (moqSymbols is null)
55+
{
56+
return;
57+
}
58+
59+
var invocationExpressionSymbol = context.SemanticModel.GetSymbolInfo(invocationExpression, context.CancellationToken);
60+
61+
if (invocationExpressionSymbol.Symbol is not IMethodSymbol methodSymbol)
62+
{
63+
return;
64+
}
65+
66+
if (!moqSymbols.IsReturnsMethod(methodSymbol) && !moqSymbols.IsReturnsAsyncMethod(methodSymbol))
67+
{
68+
return;
69+
}
70+
71+
// Gets the first argument of the Returns() method.
72+
if (invocationExpression.ArgumentList.Arguments.Count != 1)
73+
{
74+
return;
75+
}
76+
77+
var firstArgumentExpression = invocationExpression.ArgumentList.Arguments[0].Expression;
78+
79+
if (firstArgumentExpression is not ParenthesizedLambdaExpressionSyntax delegateMethodSyntax)
80+
{
81+
return;
82+
}
83+
84+
var firstArgumentSymbol = context.SemanticModel.GetSymbolInfo(firstArgumentExpression, context.CancellationToken);
85+
86+
if (firstArgumentSymbol.Symbol is not IMethodSymbol delegateMethodSymbol)
87+
{
88+
return;
89+
}
90+
91+
var moqExpressionAnalyzer = new MoqExpressionAnalyzer(moqSymbols, context.SemanticModel);
92+
93+
// Extract the Setup() method.
94+
var setupMethod = moqExpressionAnalyzer.ExtractSetupMethod(invocationExpression, context.CancellationToken);
95+
96+
if (setupMethod is null)
97+
{
98+
return;
99+
}
100+
101+
// Check the return type
102+
if (!moqSymbols.IsReturnsAsyncMethod(methodSymbol))
103+
{
104+
var expectedReturnType = setupMethod.ReturnType;
105+
106+
if (!moqSymbols.IsAnyType(expectedReturnType) && !SymbolEqualityComparer.Default.Equals(delegateMethodSymbol.ReturnType, expectedReturnType))
107+
{
108+
context.ReportDiagnostic(ReturnValueMustMatchRule, firstArgumentExpression.GetLocation(), expectedReturnType.Name);
109+
}
110+
}
111+
112+
// Check the argument types.
113+
if (setupMethod.IsProperty)
114+
{
115+
if (delegateMethodSymbol.Parameters.Length > 0)
116+
{
117+
// With property, the Returns() method must have no arguments.
118+
context.ReportDiagnostic(ArgumentMustMatchRule, delegateMethodSyntax.ParameterList.GetLocation());
119+
}
120+
121+
return;
122+
}
123+
124+
if (delegateMethodSymbol.Parameters.Length == 0)
125+
{
126+
// No argument in the delegate method, Moq accept it.
127+
return;
128+
}
129+
130+
if (delegateMethodSymbol.Parameters.Length != setupMethod.InvocationArguments.Count)
131+
{
132+
context.ReportDiagnostic(ArgumentMustMatchRule, delegateMethodSyntax.ParameterList.GetLocation());
133+
return;
134+
}
135+
136+
for (var i = 0; i < delegateMethodSymbol.Parameters.Length; i++)
137+
{
138+
if (!SymbolEqualityComparer.Default.Equals(delegateMethodSymbol.Parameters[i].Type, setupMethod.InvocationArguments[i].ParameterSymbol.Type))
139+
{
140+
context.ReportDiagnostic(ArgumentMustMatchRule, delegateMethodSyntax.ParameterList.Parameters[i].GetLocation());
141+
}
142+
}
143+
}
144+
}
145+
}

src/Moq.Analyzers/ChainMembersInvocation.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,32 @@ public ChainMembersInvocation(IReadOnlyList<ChainMember> members, IReadOnlyList<
2020

2121
public IReadOnlyList<ChainInvocationArgument> InvocationArguments { get; }
2222

23+
public ITypeSymbol ReturnType
24+
{
25+
get
26+
{
27+
if (this.Members[0].Symbol is IMethodSymbol methodSymbol)
28+
{
29+
return methodSymbol.ReturnType;
30+
}
31+
32+
return ((IPropertySymbol)this.Members[0].Symbol).Type;
33+
}
34+
}
35+
36+
public bool IsProperty
37+
{
38+
get
39+
{
40+
if (this.Members[0].Symbol is IMethodSymbol methodSymbol)
41+
{
42+
return false;
43+
}
44+
45+
return true;
46+
}
47+
}
48+
2349
public bool HasSameMembers(ChainMembersInvocation otherChainInvocation)
2450
{
2551
if (this.Members.Count != otherChainInvocation.Members.Count)

src/Moq.Analyzers/Moq.Analyzers.csproj

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,21 @@
1717
<PackageProjectUrl>https://github.com/PosInformatique/PosInformatique.Moq.Analyzers</PackageProjectUrl>
1818
<PackageReadmeFile>README.md</PackageReadmeFile>
1919
<PackageReleaseNotes>
20+
1.10.0
21+
- Add new rules:
22+
- PosInfoMoq2012: The delegate in the argument of the Returns() method must return a value with same type of the mocked method.
23+
- PosInfoMoq2013: The delegate in the argument of the Returns()/ReturnsAsync() method must have the same parameter types of the mocked method/property.
24+
2025
1.9.3
2126
- Fix the PosInfoMoq2006 when Setup() a method/property in inherited class.
22-
27+
2328
1.9.2
2429
- Fix the PosInfoMoq1003 to raise warnings when using InSequence() method.
2530
- Fix the PosInfoMoq2003 to raise errors when using InSequence() method.
2631

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

0 commit comments

Comments
 (0)