Skip to content

Commit 6b78a9a

Browse files
Initial version
1 parent a62f9a7 commit 6b78a9a

12 files changed

Lines changed: 1254 additions & 0 deletions

PosInformatique.Moq.Analyzers.sln

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.7.34221.43
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Analyzers.Tests", "tests\Moq.Analyzers.Tests\Moq.Analyzers.Tests.csproj", "{DEE86A7E-8338-4C3D-822B-E8FB976D9905}"
7+
EndProject
8+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Analyzers", "src\Moq.Analyzers\Moq.Analyzers.csproj", "{1962BEF9-E6DF-4485-A113-E255C84177D4}"
9+
EndProject
10+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1D59B801-B4D3-44FC-A2BE-F2F53AC54061}"
11+
ProjectSection(SolutionItems) = preProject
12+
.gitignore = .gitignore
13+
LICENSE = LICENSE
14+
README.md = README.md
15+
EndProjectSection
16+
EndProject
17+
Global
18+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
19+
Debug|Any CPU = Debug|Any CPU
20+
Release|Any CPU = Release|Any CPU
21+
EndGlobalSection
22+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
23+
{DEE86A7E-8338-4C3D-822B-E8FB976D9905}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
24+
{DEE86A7E-8338-4C3D-822B-E8FB976D9905}.Debug|Any CPU.Build.0 = Debug|Any CPU
25+
{DEE86A7E-8338-4C3D-822B-E8FB976D9905}.Release|Any CPU.ActiveCfg = Release|Any CPU
26+
{DEE86A7E-8338-4C3D-822B-E8FB976D9905}.Release|Any CPU.Build.0 = Release|Any CPU
27+
{1962BEF9-E6DF-4485-A113-E255C84177D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
28+
{1962BEF9-E6DF-4485-A113-E255C84177D4}.Debug|Any CPU.Build.0 = Debug|Any CPU
29+
{1962BEF9-E6DF-4485-A113-E255C84177D4}.Release|Any CPU.ActiveCfg = Release|Any CPU
30+
{1962BEF9-E6DF-4485-A113-E255C84177D4}.Release|Any CPU.Build.0 = Release|Any CPU
31+
EndGlobalSection
32+
GlobalSection(SolutionProperties) = preSolution
33+
HideSolutionNode = FALSE
34+
EndGlobalSection
35+
GlobalSection(ExtensibilityGlobals) = postSolution
36+
SolutionGuid = {3307E7F7-9CD7-4C12-B34A-943F5A8B62A4}
37+
EndGlobalSection
38+
EndGlobal
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
## Release 1.0
2+
3+
### New Rules
4+
5+
Rule ID | Category | Severity | Notes
6+
--------|----------|----------|--------------------
7+
MQ2000 | Design | Warning |
8+
MQ2001 | Design | Warning | MockInstanceShouldBeStrictBehavior
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
namespace PosInformatique.Moq.Analyzers
2+
{
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CSharp;
5+
using Microsoft.CodeAnalysis.CSharp.Syntax;
6+
using Microsoft.CodeAnalysis.Diagnostics;
7+
using System;
8+
using System.Collections.Immutable;
9+
using System.Linq;
10+
11+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
12+
public class MockInstanceShouldBeStrictBehaviorAnalyzer : DiagnosticAnalyzer
13+
{
14+
public const string DiagnosticId = "MQ2001";
15+
16+
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
17+
DiagnosticId,
18+
"Check that Mock<T> instances are instantiate with the Strict behavior",
19+
"The Mock<T> instance behavior should be defined to Strict mode",
20+
"Design",
21+
DiagnosticSeverity.Warning,
22+
isEnabledByDefault: true,
23+
description: "The Mock<T> instance behavior should be defined to Strict mode.");
24+
25+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
26+
27+
public override void Initialize(AnalysisContext context)
28+
{
29+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
30+
context.EnableConcurrentExecution();
31+
32+
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.ObjectCreationExpression);
33+
}
34+
35+
private static void Analyze(SyntaxNodeAnalysisContext context)
36+
{
37+
var objectCreation = (ObjectCreationExpressionSyntax)context.Node;
38+
39+
// Check there is "new Mock<I>()" statement.
40+
if (!MockExpressionHelper.IsMockCreation(objectCreation))
41+
{
42+
return;
43+
}
44+
45+
// Check that the "new Mock<I>()" statement have at least one argument (else Strict is missing...).
46+
if (objectCreation.ArgumentList is null)
47+
{
48+
var diagnostic = Diagnostic.Create(Rule, objectCreation.GetLocation());
49+
context.ReportDiagnostic(diagnostic);
50+
51+
return;
52+
}
53+
54+
var firstArgument = objectCreation.ArgumentList.Arguments.FirstOrDefault();
55+
56+
if (firstArgument is null)
57+
{
58+
var diagnostic = Diagnostic.Create(Rule, objectCreation.GetLocation());
59+
context.ReportDiagnostic(diagnostic);
60+
61+
return;
62+
}
63+
64+
// Gets the first argument of "new Mock<I>(...)" and ensures it is a MemberAccessExpressionSyntax
65+
// (because we searching for MockBehavior.Strict).
66+
if (firstArgument.Expression is not MemberAccessExpressionSyntax memberAccessExpression)
67+
{
68+
var diagnostic = Diagnostic.Create(Rule, objectCreation.GetLocation());
69+
context.ReportDiagnostic(diagnostic);
70+
71+
return;
72+
}
73+
74+
// Find the "MockBehavior" type in the semantic model from Moq library.
75+
var mockBehaviorType = context.Compilation.GetTypeByMetadataName("Moq.MockBehavior");
76+
77+
if (mockBehaviorType is null)
78+
{
79+
// Moq not installed (dependency of the Moq package missing), so we stop analysis.
80+
return;
81+
}
82+
83+
// Check that the "memberAccessExpression.Expression" is applied on the Moq MockBehavior type.
84+
var firstArgumentType = context.SemanticModel.GetSymbolInfo(memberAccessExpression.Expression);
85+
86+
if (!SymbolEqualityComparer.Default.Equals(firstArgumentType.Symbol, mockBehaviorType))
87+
{
88+
var diagnostic = Diagnostic.Create(Rule, objectCreation.GetLocation());
89+
context.ReportDiagnostic(diagnostic);
90+
91+
return;
92+
}
93+
94+
// Find the Strict field in the semantic model
95+
var strictField = mockBehaviorType.GetMembers("Strict").SingleOrDefault();
96+
97+
if (strictField is null)
98+
{
99+
// The field Strict seam to not exists (dependency of the Moq package missing ? Or something wrong ?), so we stop analysis.
100+
return;
101+
}
102+
103+
// Check that the memberAccessExpression.Name reference the Strict field
104+
var firstArgumentField = context.SemanticModel.GetSymbolInfo(memberAccessExpression.Name);
105+
106+
if (!SymbolEqualityComparer.Default.Equals(firstArgumentField.Symbol, strictField))
107+
{
108+
var diagnostic = Diagnostic.Create(Rule, memberAccessExpression.Name.GetLocation());
109+
context.ReportDiagnostic(diagnostic);
110+
111+
return;
112+
}
113+
}
114+
}
115+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
namespace PosInformatique.Moq.Analyzers
2+
{
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CSharp;
5+
using Microsoft.CodeAnalysis.CSharp.Syntax;
6+
using Microsoft.CodeAnalysis.Diagnostics;
7+
using System.Collections.Immutable;
8+
using System.Linq;
9+
10+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
11+
public class VerifyAllShouldBeCalledAnalyzer : DiagnosticAnalyzer
12+
{
13+
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
14+
"MQ2000",
15+
"Check Verify() or VerifyAll() methods are called when instantiate a Mock<T> instances",
16+
"The Verify() or VerifyAll() method should be called",
17+
"Design",
18+
DiagnosticSeverity.Warning,
19+
isEnabledByDefault: true,
20+
description: "VerifyAll() or VerifyAll() methods should be called in the test methods.");
21+
22+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
23+
24+
public override void Initialize(AnalysisContext context)
25+
{
26+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
27+
context.EnableConcurrentExecution();
28+
29+
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.ObjectCreationExpression);
30+
}
31+
32+
private static void Analyze(SyntaxNodeAnalysisContext context)
33+
{
34+
var objectCreation = (ObjectCreationExpressionSyntax)context.Node;
35+
36+
if (!MockExpressionHelper.IsMockCreation(objectCreation))
37+
{
38+
return;
39+
}
40+
41+
// Retrieve the variable name
42+
var variableName = objectCreation.Ancestors().OfType<VariableDeclaratorSyntax>().FirstOrDefault();
43+
44+
if (variableName is null)
45+
{
46+
// No variable set on the left for the "new Mock<T>()". Skip it.
47+
return;
48+
}
49+
50+
var variableNameModel = context.SemanticModel.GetDeclaredSymbol(variableName);
51+
52+
// Check if there is a VerifyAll() invocation in the method's parent block.
53+
var parentMethod = objectCreation.Ancestors().OfType<MethodDeclarationSyntax>().FirstOrDefault();
54+
55+
if (parentMethod is null)
56+
{
57+
// Parent method not found, skip it.
58+
return;
59+
}
60+
61+
// Retrieve all method invocation expressions.
62+
var invocationExpressions = parentMethod.DescendantNodes().OfType<InvocationExpressionSyntax>();
63+
64+
var verifyAllCalled = invocationExpressions.Any(expression => IsMockVerifyAllInvocation(expression, variableNameModel, context.SemanticModel));
65+
66+
if (!verifyAllCalled)
67+
{
68+
var diagnostic = Diagnostic.Create(Rule, objectCreation.GetLocation());
69+
context.ReportDiagnostic(diagnostic);
70+
}
71+
}
72+
73+
private static bool IsMockVerifyAllInvocation(InvocationExpressionSyntax invocation, ISymbol variableNameSymbol, SemanticModel semanticModel)
74+
{
75+
if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess)
76+
{
77+
return false;
78+
}
79+
80+
// We check if a "VerifyAll()" method is called (currently we don't know if it is on the Mock object, it can be on other object type)
81+
// but we try to use this condition here to stop quickly the analysis.
82+
if (!IsVerifyMethod(memberAccess.Name.Identifier.ValueText))
83+
{
84+
return false;
85+
}
86+
87+
// Gets the variable name symbol.
88+
var identifierSymbol = semanticModel.GetSymbolInfo(memberAccess.Expression);
89+
90+
// If the variable name of .VerifyAll() does not match the variable, so the VerifyAll() was for other Mock instance.
91+
if (!SymbolEqualityComparer.Default.Equals(identifierSymbol.Symbol, variableNameSymbol))
92+
{
93+
return false;
94+
}
95+
96+
return true;
97+
}
98+
99+
private static bool IsVerifyMethod(string name)
100+
{
101+
return name switch
102+
{
103+
"VerifyAll" => true,
104+
"Verify" => true,
105+
_ => false
106+
};
107+
}
108+
}
109+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
namespace PosInformatique.Moq.Analyzers
2+
{
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CodeActions;
5+
using Microsoft.CodeAnalysis.CodeFixes;
6+
using Microsoft.CodeAnalysis.CSharp;
7+
using Microsoft.CodeAnalysis.CSharp.Syntax;
8+
using Microsoft.CodeAnalysis.Rename;
9+
using Microsoft.CodeAnalysis.Text;
10+
using System;
11+
using System.Collections.Generic;
12+
using System.Collections.Immutable;
13+
using System.Composition;
14+
using System.Linq;
15+
using System.Threading;
16+
using System.Threading.Tasks;
17+
18+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SetBehaviorToStrictCodeFixProvider)), Shared]
19+
public class SetBehaviorToStrictCodeFixProvider : CodeFixProvider
20+
{
21+
public sealed override ImmutableArray<string> FixableDiagnosticIds
22+
{
23+
get { return ImmutableArray.Create(MockInstanceShouldBeStrictBehaviorAnalyzer.DiagnosticId); }
24+
}
25+
26+
public sealed override FixAllProvider GetFixAllProvider()
27+
{
28+
return WellKnownFixAllProviders.BatchFixer;
29+
}
30+
31+
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
32+
{
33+
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
34+
35+
// Gets the location where is the issue in the code.
36+
var diagnostic = context.Diagnostics.First();
37+
var diagnosticSpan = diagnostic.Location.SourceSpan;
38+
39+
// Find the "ObjectCreationExpressionSyntax" in the parent of the location where is located the issue in the code.
40+
var mockCreationExpression = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<ObjectCreationExpressionSyntax>().First();
41+
42+
// Register a code to fix the enumeration.
43+
context.RegisterCodeFix(
44+
CodeAction.Create(
45+
title: "Defines the MockBehavior to Strict",
46+
createChangedDocument: cancellationToken => AddMockBehiavorStrict(context.Document, mockCreationExpression, cancellationToken),
47+
equivalenceKey: "Defines the MockBehavior to Strict"),
48+
diagnostic);
49+
}
50+
51+
private async Task<Document> AddMockBehiavorStrict(Document document, ObjectCreationExpressionSyntax oldMockCreationExpression, CancellationToken cancellationToken)
52+
{
53+
var mockBehaviorArgument = SyntaxFactory.Argument(
54+
SyntaxFactory.MemberAccessExpression(
55+
SyntaxKind.SimpleMemberAccessExpression,
56+
SyntaxFactory.IdentifierName("MockBehavior"),
57+
SyntaxFactory.IdentifierName("Strict")));
58+
59+
var arguments = new List<ArgumentSyntax>()
60+
{
61+
mockBehaviorArgument,
62+
};
63+
64+
if (oldMockCreationExpression.ArgumentList is not null && oldMockCreationExpression.ArgumentList.Arguments.Count > 0)
65+
{
66+
var firstArgument = oldMockCreationExpression.ArgumentList.Arguments.First();
67+
68+
if (IsMockBehaviorArgument(firstArgument))
69+
{
70+
// The old first argument is MockBehavior.xxxxx, so we take the following arguments
71+
// and ignore it.
72+
arguments.AddRange(oldMockCreationExpression.ArgumentList.Arguments.Skip(1));
73+
}
74+
else
75+
{
76+
// Retrieves all the arguments of the "new Mock<T>(...)" instantiation.
77+
arguments.AddRange(oldMockCreationExpression.ArgumentList.Arguments);
78+
}
79+
}
80+
81+
var newMockCreationExpression = oldMockCreationExpression.WithArgumentList(
82+
SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(arguments)));
83+
84+
var oldRoot = await document.GetSyntaxRootAsync(cancellationToken);
85+
var newRoot = oldRoot.ReplaceNode(oldMockCreationExpression, newMockCreationExpression);
86+
87+
return document.WithSyntaxRoot(newRoot);
88+
}
89+
90+
private bool IsMockBehaviorArgument(ArgumentSyntax argument)
91+
{
92+
if (argument.Expression is not MemberAccessExpressionSyntax memberAccessExpression)
93+
{
94+
return false;
95+
}
96+
97+
if (memberAccessExpression.Expression is not IdentifierNameSyntax targetExpression)
98+
{
99+
return false;
100+
}
101+
102+
if (targetExpression.Identifier.ValueText != "MockBehavior")
103+
{
104+
return false;
105+
}
106+
107+
return true;
108+
}
109+
}
110+
}

0 commit comments

Comments
 (0)