From 6b78a9a2e86e92be35b6ed55f51795a3cc58dfe6 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Tue, 31 Oct 2023 21:17:25 +0100 Subject: [PATCH 1/6] Initial version --- PosInformatique.Moq.Analyzers.sln | 38 +++ src/Moq.Analyzers/AnalyzerReleases.Shipped.md | 8 + .../AnalyzerReleases.Unshipped.md | 1 + ...kInstanceShouldBeStrictBehaviorAnalyzer.cs | 115 +++++++ .../VerifyAllShouldBeCalledAnalyzer.cs | 109 ++++++ .../SetBehaviorToStrictCodeFixProvider.cs | 110 +++++++ src/Moq.Analyzers/MockExpressionHelper.cs | 22 ++ src/Moq.Analyzers/Moq.Analyzers.csproj | 19 ++ ...tanceShouldBeStrictBehaviorAnalyzerTest.cs | 309 ++++++++++++++++++ .../VerifyAllShouldBeCalledAnalyzerTest.cs | 206 ++++++++++++ .../SetBehaviorToStrictCodeFixProviderTest.cs | 287 ++++++++++++++++ .../Moq.Analyzers.Tests.csproj | 30 ++ 12 files changed, 1254 insertions(+) create mode 100644 PosInformatique.Moq.Analyzers.sln create mode 100644 src/Moq.Analyzers/AnalyzerReleases.Shipped.md create mode 100644 src/Moq.Analyzers/AnalyzerReleases.Unshipped.md create mode 100644 src/Moq.Analyzers/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzer.cs create mode 100644 src/Moq.Analyzers/Analyzers/VerifyAllShouldBeCalledAnalyzer.cs create mode 100644 src/Moq.Analyzers/CodeFixes/SetBehaviorToStrictCodeFixProvider.cs create mode 100644 src/Moq.Analyzers/MockExpressionHelper.cs create mode 100644 src/Moq.Analyzers/Moq.Analyzers.csproj create mode 100644 tests/Moq.Analyzers.Tests/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzerTest.cs create mode 100644 tests/Moq.Analyzers.Tests/Analyzers/VerifyAllShouldBeCalledAnalyzerTest.cs create mode 100644 tests/Moq.Analyzers.Tests/CodeFixes/SetBehaviorToStrictCodeFixProviderTest.cs create mode 100644 tests/Moq.Analyzers.Tests/Moq.Analyzers.Tests.csproj diff --git a/PosInformatique.Moq.Analyzers.sln b/PosInformatique.Moq.Analyzers.sln new file mode 100644 index 0000000..fd51bce --- /dev/null +++ b/PosInformatique.Moq.Analyzers.sln @@ -0,0 +1,38 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.34221.43 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Analyzers.Tests", "tests\Moq.Analyzers.Tests\Moq.Analyzers.Tests.csproj", "{DEE86A7E-8338-4C3D-822B-E8FB976D9905}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Analyzers", "src\Moq.Analyzers\Moq.Analyzers.csproj", "{1962BEF9-E6DF-4485-A113-E255C84177D4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1D59B801-B4D3-44FC-A2BE-F2F53AC54061}" + ProjectSection(SolutionItems) = preProject + .gitignore = .gitignore + LICENSE = LICENSE + README.md = README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DEE86A7E-8338-4C3D-822B-E8FB976D9905}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DEE86A7E-8338-4C3D-822B-E8FB976D9905}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DEE86A7E-8338-4C3D-822B-E8FB976D9905}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DEE86A7E-8338-4C3D-822B-E8FB976D9905}.Release|Any CPU.Build.0 = Release|Any CPU + {1962BEF9-E6DF-4485-A113-E255C84177D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1962BEF9-E6DF-4485-A113-E255C84177D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1962BEF9-E6DF-4485-A113-E255C84177D4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1962BEF9-E6DF-4485-A113-E255C84177D4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {3307E7F7-9CD7-4C12-B34A-943F5A8B62A4} + EndGlobalSection +EndGlobal diff --git a/src/Moq.Analyzers/AnalyzerReleases.Shipped.md b/src/Moq.Analyzers/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000..d9dad6d --- /dev/null +++ b/src/Moq.Analyzers/AnalyzerReleases.Shipped.md @@ -0,0 +1,8 @@ +## Release 1.0 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|-------------------- +MQ2000 | Design | Warning | +MQ2001 | Design | Warning | MockInstanceShouldBeStrictBehavior \ No newline at end of file diff --git a/src/Moq.Analyzers/AnalyzerReleases.Unshipped.md b/src/Moq.Analyzers/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/src/Moq.Analyzers/AnalyzerReleases.Unshipped.md @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Moq.Analyzers/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzer.cs b/src/Moq.Analyzers/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzer.cs new file mode 100644 index 0000000..340a691 --- /dev/null +++ b/src/Moq.Analyzers/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzer.cs @@ -0,0 +1,115 @@ +namespace PosInformatique.Moq.Analyzers +{ + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using Microsoft.CodeAnalysis.Diagnostics; + using System; + using System.Collections.Immutable; + using System.Linq; + + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class MockInstanceShouldBeStrictBehaviorAnalyzer : DiagnosticAnalyzer + { + public const string DiagnosticId = "MQ2001"; + + private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( + DiagnosticId, + "Check that Mock instances are instantiate with the Strict behavior", + "The Mock instance behavior should be defined to Strict mode", + "Design", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "The Mock instance behavior should be defined to Strict mode."); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.ObjectCreationExpression); + } + + private static void Analyze(SyntaxNodeAnalysisContext context) + { + var objectCreation = (ObjectCreationExpressionSyntax)context.Node; + + // Check there is "new Mock()" statement. + if (!MockExpressionHelper.IsMockCreation(objectCreation)) + { + return; + } + + // Check that the "new Mock()" statement have at least one argument (else Strict is missing...). + if (objectCreation.ArgumentList is null) + { + var diagnostic = Diagnostic.Create(Rule, objectCreation.GetLocation()); + context.ReportDiagnostic(diagnostic); + + return; + } + + var firstArgument = objectCreation.ArgumentList.Arguments.FirstOrDefault(); + + if (firstArgument is null) + { + var diagnostic = Diagnostic.Create(Rule, objectCreation.GetLocation()); + context.ReportDiagnostic(diagnostic); + + return; + } + + // Gets the first argument of "new Mock(...)" and ensures it is a MemberAccessExpressionSyntax + // (because we searching for MockBehavior.Strict). + if (firstArgument.Expression is not MemberAccessExpressionSyntax memberAccessExpression) + { + var diagnostic = Diagnostic.Create(Rule, objectCreation.GetLocation()); + context.ReportDiagnostic(diagnostic); + + return; + } + + // Find the "MockBehavior" type in the semantic model from Moq library. + var mockBehaviorType = context.Compilation.GetTypeByMetadataName("Moq.MockBehavior"); + + if (mockBehaviorType is null) + { + // Moq not installed (dependency of the Moq package missing), so we stop analysis. + return; + } + + // Check that the "memberAccessExpression.Expression" is applied on the Moq MockBehavior type. + var firstArgumentType = context.SemanticModel.GetSymbolInfo(memberAccessExpression.Expression); + + if (!SymbolEqualityComparer.Default.Equals(firstArgumentType.Symbol, mockBehaviorType)) + { + var diagnostic = Diagnostic.Create(Rule, objectCreation.GetLocation()); + context.ReportDiagnostic(diagnostic); + + return; + } + + // Find the Strict field in the semantic model + var strictField = mockBehaviorType.GetMembers("Strict").SingleOrDefault(); + + if (strictField is null) + { + // The field Strict seam to not exists (dependency of the Moq package missing ? Or something wrong ?), so we stop analysis. + return; + } + + // Check that the memberAccessExpression.Name reference the Strict field + var firstArgumentField = context.SemanticModel.GetSymbolInfo(memberAccessExpression.Name); + + if (!SymbolEqualityComparer.Default.Equals(firstArgumentField.Symbol, strictField)) + { + var diagnostic = Diagnostic.Create(Rule, memberAccessExpression.Name.GetLocation()); + context.ReportDiagnostic(diagnostic); + + return; + } + } + } +} diff --git a/src/Moq.Analyzers/Analyzers/VerifyAllShouldBeCalledAnalyzer.cs b/src/Moq.Analyzers/Analyzers/VerifyAllShouldBeCalledAnalyzer.cs new file mode 100644 index 0000000..89288e1 --- /dev/null +++ b/src/Moq.Analyzers/Analyzers/VerifyAllShouldBeCalledAnalyzer.cs @@ -0,0 +1,109 @@ +namespace PosInformatique.Moq.Analyzers +{ + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using Microsoft.CodeAnalysis.Diagnostics; + using System.Collections.Immutable; + using System.Linq; + + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class VerifyAllShouldBeCalledAnalyzer : DiagnosticAnalyzer + { + private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( + "MQ2000", + "Check Verify() or VerifyAll() methods are called when instantiate a Mock instances", + "The Verify() or VerifyAll() method should be called", + "Design", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "VerifyAll() or VerifyAll() methods should be called in the test methods."); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.ObjectCreationExpression); + } + + private static void Analyze(SyntaxNodeAnalysisContext context) + { + var objectCreation = (ObjectCreationExpressionSyntax)context.Node; + + if (!MockExpressionHelper.IsMockCreation(objectCreation)) + { + return; + } + + // Retrieve the variable name + var variableName = objectCreation.Ancestors().OfType().FirstOrDefault(); + + if (variableName is null) + { + // No variable set on the left for the "new Mock()". Skip it. + return; + } + + var variableNameModel = context.SemanticModel.GetDeclaredSymbol(variableName); + + // Check if there is a VerifyAll() invocation in the method's parent block. + var parentMethod = objectCreation.Ancestors().OfType().FirstOrDefault(); + + if (parentMethod is null) + { + // Parent method not found, skip it. + return; + } + + // Retrieve all method invocation expressions. + var invocationExpressions = parentMethod.DescendantNodes().OfType(); + + var verifyAllCalled = invocationExpressions.Any(expression => IsMockVerifyAllInvocation(expression, variableNameModel, context.SemanticModel)); + + if (!verifyAllCalled) + { + var diagnostic = Diagnostic.Create(Rule, objectCreation.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + } + + private static bool IsMockVerifyAllInvocation(InvocationExpressionSyntax invocation, ISymbol variableNameSymbol, SemanticModel semanticModel) + { + if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) + { + return false; + } + + // 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) + // but we try to use this condition here to stop quickly the analysis. + if (!IsVerifyMethod(memberAccess.Name.Identifier.ValueText)) + { + return false; + } + + // Gets the variable name symbol. + var identifierSymbol = semanticModel.GetSymbolInfo(memberAccess.Expression); + + // If the variable name of .VerifyAll() does not match the variable, so the VerifyAll() was for other Mock instance. + if (!SymbolEqualityComparer.Default.Equals(identifierSymbol.Symbol, variableNameSymbol)) + { + return false; + } + + return true; + } + + private static bool IsVerifyMethod(string name) + { + return name switch + { + "VerifyAll" => true, + "Verify" => true, + _ => false + }; + } + } +} diff --git a/src/Moq.Analyzers/CodeFixes/SetBehaviorToStrictCodeFixProvider.cs b/src/Moq.Analyzers/CodeFixes/SetBehaviorToStrictCodeFixProvider.cs new file mode 100644 index 0000000..19e621b --- /dev/null +++ b/src/Moq.Analyzers/CodeFixes/SetBehaviorToStrictCodeFixProvider.cs @@ -0,0 +1,110 @@ +namespace PosInformatique.Moq.Analyzers +{ + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CodeActions; + using Microsoft.CodeAnalysis.CodeFixes; + using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.CSharp.Syntax; + using Microsoft.CodeAnalysis.Rename; + using Microsoft.CodeAnalysis.Text; + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Composition; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SetBehaviorToStrictCodeFixProvider)), Shared] + public class SetBehaviorToStrictCodeFixProvider : CodeFixProvider + { + public sealed override ImmutableArray FixableDiagnosticIds + { + get { return ImmutableArray.Create(MockInstanceShouldBeStrictBehaviorAnalyzer.DiagnosticId); } + } + + public sealed override FixAllProvider GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + // Gets the location where is the issue in the code. + var diagnostic = context.Diagnostics.First(); + var diagnosticSpan = diagnostic.Location.SourceSpan; + + // Find the "ObjectCreationExpressionSyntax" in the parent of the location where is located the issue in the code. + var mockCreationExpression = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType().First(); + + // Register a code to fix the enumeration. + context.RegisterCodeFix( + CodeAction.Create( + title: "Defines the MockBehavior to Strict", + createChangedDocument: cancellationToken => AddMockBehiavorStrict(context.Document, mockCreationExpression, cancellationToken), + equivalenceKey: "Defines the MockBehavior to Strict"), + diagnostic); + } + + private async Task AddMockBehiavorStrict(Document document, ObjectCreationExpressionSyntax oldMockCreationExpression, CancellationToken cancellationToken) + { + var mockBehaviorArgument = SyntaxFactory.Argument( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("MockBehavior"), + SyntaxFactory.IdentifierName("Strict"))); + + var arguments = new List() + { + mockBehaviorArgument, + }; + + if (oldMockCreationExpression.ArgumentList is not null && oldMockCreationExpression.ArgumentList.Arguments.Count > 0) + { + var firstArgument = oldMockCreationExpression.ArgumentList.Arguments.First(); + + if (IsMockBehaviorArgument(firstArgument)) + { + // The old first argument is MockBehavior.xxxxx, so we take the following arguments + // and ignore it. + arguments.AddRange(oldMockCreationExpression.ArgumentList.Arguments.Skip(1)); + } + else + { + // Retrieves all the arguments of the "new Mock(...)" instantiation. + arguments.AddRange(oldMockCreationExpression.ArgumentList.Arguments); + } + } + + var newMockCreationExpression = oldMockCreationExpression.WithArgumentList( + SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(arguments))); + + var oldRoot = await document.GetSyntaxRootAsync(cancellationToken); + var newRoot = oldRoot.ReplaceNode(oldMockCreationExpression, newMockCreationExpression); + + return document.WithSyntaxRoot(newRoot); + } + + private bool IsMockBehaviorArgument(ArgumentSyntax argument) + { + if (argument.Expression is not MemberAccessExpressionSyntax memberAccessExpression) + { + return false; + } + + if (memberAccessExpression.Expression is not IdentifierNameSyntax targetExpression) + { + return false; + } + + if (targetExpression.Identifier.ValueText != "MockBehavior") + { + return false; + } + + return true; + } + } +} diff --git a/src/Moq.Analyzers/MockExpressionHelper.cs b/src/Moq.Analyzers/MockExpressionHelper.cs new file mode 100644 index 0000000..5a4bdca --- /dev/null +++ b/src/Moq.Analyzers/MockExpressionHelper.cs @@ -0,0 +1,22 @@ +namespace PosInformatique.Moq.Analyzers +{ + using Microsoft.CodeAnalysis.CSharp.Syntax; + + internal static class MockExpressionHelper + { + public static bool IsMockCreation(ObjectCreationExpressionSyntax expression) + { + if (expression.Type is not GenericNameSyntax genericName) + { + return false; + } + + if (genericName.Identifier.ValueText != "Mock") + { + return false; + } + + return true; + } + } +} diff --git a/src/Moq.Analyzers/Moq.Analyzers.csproj b/src/Moq.Analyzers/Moq.Analyzers.csproj new file mode 100644 index 0000000..e35a6eb --- /dev/null +++ b/src/Moq.Analyzers/Moq.Analyzers.csproj @@ -0,0 +1,19 @@ + + + + netstandard2.0 + false + latest + + + *$(MSBuildProjectFile)* + + true + + + + + + + + diff --git a/tests/Moq.Analyzers.Tests/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzerTest.cs new file mode 100644 index 0000000..5cbff45 --- /dev/null +++ b/tests/Moq.Analyzers.Tests/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzerTest.cs @@ -0,0 +1,309 @@ +namespace PosInformatique.Moq.Analyzers.Tests +{ + using System.Threading.Tasks; + using Xunit; + using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier< + MockInstanceShouldBeStrictBehaviorAnalyzer, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; + + public class MockInstanceShouldBeStrictBehaviorAnalyzerTest + { + [Fact] + public async Task NoMock() + { + var source = @" + namespace ConsoleApplication1 + { + public class TestClass + { + public void TestMethod() + { + } + } + + public interface I + { + } + }"; + + await Verify.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task BehaviorStrict() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(MockBehavior.Strict); + } + } + + public interface I + { + } + } + + namespace Moq + { + public class Mock + { + public Mock(MockBehavior _) { } + } + + public enum MockBehavior { Strict, Loose } + }"; + + await Verify.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task BehaviorLoose() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(MockBehavior.[|Loose|]); + } + } + + public interface I + { + } + } + + namespace Moq + { + public class Mock + { + public Mock(MockBehavior _) { } + } + + public enum MockBehavior { Strict, Loose } + }"; + + await Verify.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task NoBehavior() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = [|new Mock()|]; + } + } + + public interface I + { + } + } + + namespace Moq + { + public class Mock + { + public Mock() { } + } + }"; + + await Verify.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task NoBehavior_WithNullArgumentList() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = [|new Mock { }|]; + } + } + + public interface I + { + } + } + + namespace Moq + { + public class Mock + { + public Mock() { } + } + }"; + + await Verify.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task Behavior_NotMemberAccessExpression() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock([|default|], 1, 2); + } + } + + public interface I + { + } + } + + namespace Moq + { + public class Mock + { + public Mock(MockBehavior _, int a, int b) { } + } + + public enum MockBehavior { Strict, Loose } + }"; + + await Verify.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task NoMoqNamespace() + { + var source = @" + namespace ConsoleApplication1 + { + using OtherNamespace; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(MockBehavior.Strict); + } + } + + public interface I + { + } + } + + namespace OtherNamespace + { + public class Mock + { + public Mock(MockBehavior _) { } + } + + public enum MockBehavior { Strict, Loose } + }"; + + await Verify.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task MockBehaviorTypeNotFromMoq() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + using OtherNamespace; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock([|NotMockBehavior.Strict|]); + } + } + + public interface I + { + } + } + + namespace OtherNamespace + { + public enum NotMockBehavior { Strict, Loose } + } + + namespace Moq + { + using OtherNamespace; + + public class Mock + { + public Mock(NotMockBehavior _) { } + } + + public enum MockBehavior { Strict, Loose } + }"; + + await Verify.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task NoBehaviorStrict_InMoq() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(MockBehavior.Loose); + } + } + + public interface I + { + } + } + + namespace Moq + { + public class Mock + { + public Mock(MockBehavior _) { } + } + + public enum MockBehavior { Loose } + }"; + + await Verify.VerifyAnalyzerAsync(source); + } + + } +} \ No newline at end of file diff --git a/tests/Moq.Analyzers.Tests/Analyzers/VerifyAllShouldBeCalledAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/VerifyAllShouldBeCalledAnalyzerTest.cs new file mode 100644 index 0000000..99cc389 --- /dev/null +++ b/tests/Moq.Analyzers.Tests/Analyzers/VerifyAllShouldBeCalledAnalyzerTest.cs @@ -0,0 +1,206 @@ +namespace PosInformatique.Moq.Analyzers.Tests +{ + using System.Threading.Tasks; + using Xunit; + using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier< + VerifyAllShouldBeCalledAnalyzer, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; + + public class VerifyAllShouldBeCalledAnalyzerTest + { + [Fact] + public async Task Verify_Called() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(); + + var o = new object(); + o.ToString(); + + var mock2 = new Mock(); + + new Mock(); // No variable (ignored) + + new System.Action(() => { })(); // Ignored + + mock1.Property = 1234; // Property access are ignored. + + mock1.VerifyAll(); + mock2.Verify(1, 2); + } + } + + public interface I + { + } + } + + namespace Moq + { + public class Mock + { + public void VerifyAll() { } + public void Verify(int a, int b) { } + public object Property { get; set; } + } + }"; + + await Verify.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task Verify_Missing_Calls() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = [|new Mock()|]; + var mock2 = [|new Mock()|]; + + new Mock(); // No variable (ignored) + + mock1.Property = 1234; // Property access are ignored. + } + } + + public interface I + { + } + } + + namespace Moq + { + public class Mock + { + public object Property { get; set; } + } + }"; + + await Verify.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task Verify_Missing_Calls_WithArguments() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = [|new Mock(1, 2, 3)|]; + var mock2 = [|new Mock(""A"", ""B"", ""C"")|]; + } + } + + public interface I + { + } + } + + namespace Moq + { + public class Mock + { + public Mock(params object[] args) + { + } + } + }"; + + await Verify.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task NoMock_Instance() + { + var source = @" + namespace ConsoleApplication1 + { + public class TestClass + { + public void TestMethod() + { + var o = new object(); + o.ToString(); + } + } + }"; + + await Verify.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task NoMock_Instance_OtherGenericType() + { + var source = @" + namespace ConsoleApplication1 + { + public class TestClass + { + public void TestMethod() + { + var o = new Generic(); + o.ToString(); + } + } + + public interface I + { + } + + public class Generic + { + } + }"; + + await Verify.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task Mock_NotInMethod_Ignored() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public Mock mock1 = new Mock(); + } + + public interface I + { + } + } + + namespace Moq + { + public class Mock + { + public object Property { get; set; } + } + }"; + + await Verify.VerifyAnalyzerAsync(source); + } + } +} diff --git a/tests/Moq.Analyzers.Tests/CodeFixes/SetBehaviorToStrictCodeFixProviderTest.cs b/tests/Moq.Analyzers.Tests/CodeFixes/SetBehaviorToStrictCodeFixProviderTest.cs new file mode 100644 index 0000000..793c600 --- /dev/null +++ b/tests/Moq.Analyzers.Tests/CodeFixes/SetBehaviorToStrictCodeFixProviderTest.cs @@ -0,0 +1,287 @@ +namespace PosInformatique.Moq.Analyzers.Tests +{ + using System.Threading.Tasks; + using Xunit; + using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixVerifier< + MockInstanceShouldBeStrictBehaviorAnalyzer, + SetBehaviorToStrictCodeFixProvider, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; + + public class SetBehaviorToStrictCodeFixProviderTest + { + [Fact] + public async Task Fix_Loose() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock = new Mock(MockBehavior.[|Loose|]); + } + } + + public interface I + { + } + } + + namespace Moq + { + public class Mock + { + public Mock(MockBehavior _, params object[] args) + { + } + + public Mock(MockBehavior _) + { + } + + public Mock(params object[] args) + { + } + + public Mock() + { + } + } + + public enum MockBehavior { Strict, Loose } + }"; + + var expectedFixedSource = + @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock = new Mock(MockBehavior.Strict); + } + } + + public interface I + { + } + } + + namespace Moq + { + public class Mock + { + public Mock(MockBehavior _, params object[] args) + { + } + + public Mock(MockBehavior _) + { + } + + public Mock(params object[] args) + { + } + + public Mock() + { + } + } + + public enum MockBehavior { Strict, Loose } + }"; + + await Verify.VerifyCodeFixAsync(source, expectedFixedSource); + } + + [Fact] + public async Task Fix_MissingBehavior() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = [|new Mock()|]; + var mock2 = [|new Mock { }|]; + } + } + + public interface I + { + } + } + + namespace Moq + { + public class Mock + { + public Mock(MockBehavior _, params object[] args) + { + } + + public Mock(MockBehavior _) + { + } + + public Mock(params object[] args) + { + } + + public Mock() + { + } + } + + public enum MockBehavior { Strict, Loose } + }"; + + var expectedFixedSource = + @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(MockBehavior.Strict); + var mock2 = new Mock(MockBehavior.Strict) { }; + } + } + + public interface I + { + } + } + + namespace Moq + { + public class Mock + { + public Mock(MockBehavior _, params object[] args) + { + } + + public Mock(MockBehavior _) + { + } + + public Mock(params object[] args) + { + } + + public Mock() + { + } + } + + public enum MockBehavior { Strict, Loose } + }"; + + await Verify.VerifyCodeFixAsync(source, expectedFixedSource); + } + + [Fact] + public async Task Fix_MissingBehavior_WithArguments() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock = [|new Mock(1, 2, 3)|]; + } + } + + public interface I + { + } + } + + namespace Moq + { + public class Mock + { + public Mock(MockBehavior _, params object[] args) + { + } + + public Mock(MockBehavior _) + { + } + + public Mock(params object[] args) + { + } + + public Mock() + { + } + } + + public enum MockBehavior { Strict, Loose } + }"; + + var expectedFixedSource = + @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock = new Mock(MockBehavior.Strict, 1, 2, 3); + } + } + + public interface I + { + } + } + + namespace Moq + { + public class Mock + { + public Mock(MockBehavior _, params object[] args) + { + } + + public Mock(MockBehavior _) + { + } + + public Mock(params object[] args) + { + } + + public Mock() + { + } + } + + public enum MockBehavior { Strict, Loose } + }"; + + await Verify.VerifyCodeFixAsync(source, expectedFixedSource); + } + } +} diff --git a/tests/Moq.Analyzers.Tests/Moq.Analyzers.Tests.csproj b/tests/Moq.Analyzers.Tests/Moq.Analyzers.Tests.csproj new file mode 100644 index 0000000..f9e54b9 --- /dev/null +++ b/tests/Moq.Analyzers.Tests/Moq.Analyzers.Tests.csproj @@ -0,0 +1,30 @@ + + + + net7.0 + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + From fb595698a5630090783c1935a242e0b00d4fe412 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Tue, 31 Oct 2023 22:44:47 +0100 Subject: [PATCH 2/6] Various fixes (Stylecop, README,...etc). --- .editorconfig | 12 +++ CodeCoverage.runsettings | 51 ++++++++++ Directory.Build.props | 48 ++++++++++ PosInformatique.Moq.Analyzers.sln | 29 ++++++ README.md | 30 +++++- build/azure-pipelines-ci.yaml | 24 +++++ build/azure-pipelines-release.yaml | 54 +++++++++++ docs/design/MQ1000.md | 45 +++++++++ docs/design/MQ1001.md | 87 ++++++++++++++++++ src/Moq.Analyzers/AnalyzerReleases.Shipped.md | 6 +- ...kInstanceShouldBeStrictBehaviorAnalyzer.cs | 22 +++-- .../VerifyAllShouldBeCalledAnalyzer.cs | 19 ++-- .../SetBehaviorToStrictCodeFixProvider.cs | 29 +++--- src/Moq.Analyzers/Icon.png | Bin 0 -> 6829 bytes src/Moq.Analyzers/MockExpressionHelper.cs | 8 +- src/Moq.Analyzers/Moq.Analyzers.csproj | 30 +++++- stylecop.json | 9 ++ tests/.editorconfig | 6 ++ ...tanceShouldBeStrictBehaviorAnalyzerTest.cs | 9 +- .../VerifyAllShouldBeCalledAnalyzerTest.cs | 8 +- .../SetBehaviorToStrictCodeFixProviderTest.cs | 8 +- 21 files changed, 490 insertions(+), 44 deletions(-) create mode 100644 .editorconfig create mode 100644 CodeCoverage.runsettings create mode 100644 Directory.Build.props create mode 100644 build/azure-pipelines-ci.yaml create mode 100644 build/azure-pipelines-release.yaml create mode 100644 docs/design/MQ1000.md create mode 100644 docs/design/MQ1001.md create mode 100644 src/Moq.Analyzers/Icon.png create mode 100644 stylecop.json create mode 100644 tests/.editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ab2cf13 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +[*.cs] + +# StyleCop + +# SA1600: Elements should be documented +dotnet_diagnostic.SA1600.severity = none + +# SA1601: Partial elements should be documented +dotnet_diagnostic.SA1601.severity = none + +# SA1602: Enumeration items should be documented +dotnet_diagnostic.SA1602.severity = none diff --git a/CodeCoverage.runsettings b/CodeCoverage.runsettings new file mode 100644 index 0000000..c9bffdc --- /dev/null +++ b/CodeCoverage.runsettings @@ -0,0 +1,51 @@ + + + + + + + + + + + + + .*\.dll$ + + + .*xunit.* + .*webjobs.* + moq.* + .*durabletask.* + microsoft.* + bouncycastle.* + .*tests\.dll$ + + + + + + + ^System\.CodeDom\.Compiler\.GeneratedCodeAttribute$ + ^System\.Diagnostics\.CodeAnalysis\.ExcludeFromCodeCoverageAttribute$ + + + + + + .*PosInformatique.* + + + + + True + True + True + False + + + + + + + \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..b51a475 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,48 @@ + + + + + Gilles TOURREAU + P.O.S Informatique + P.O.S Informatique + Copyright (c) P.O.S Informatique. All rights reserved. + https://github.com/PosInformatique/PosInformatique.Moq.Analyzers + git + + + latest + + + enable + + + false + + + $(NoWarn);SA0001 + + + PosInformatique.$(MSBuildProjectName) + PosInformatique.$(MSBuildProjectName.Replace(" ", "_")) + + + + + stylecop.json + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + \ No newline at end of file diff --git a/PosInformatique.Moq.Analyzers.sln b/PosInformatique.Moq.Analyzers.sln index fd51bce..73bb350 100644 --- a/PosInformatique.Moq.Analyzers.sln +++ b/PosInformatique.Moq.Analyzers.sln @@ -9,9 +9,32 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Analyzers", "src\Moq.An EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1D59B801-B4D3-44FC-A2BE-F2F53AC54061}" ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig .gitignore = .gitignore + CodeCoverage.runsettings = CodeCoverage.runsettings + Directory.Build.props = Directory.Build.props LICENSE = LICENSE README.md = README.md + stylecop.json = stylecop.json + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{4C3E1C72-8977-4ABD-8360-A8707DBBA5AE}" + ProjectSection(SolutionItems) = preProject + build\azure-pipelines-ci.yaml = build\azure-pipelines-ci.yaml + build\azure-pipelines-release.yaml = build\azure-pipelines-release.yaml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{DC2CDF77-88A9-490D-84ED-34832943104A}" + ProjectSection(SolutionItems) = preProject + tests\.editorconfig = tests\.editorconfig + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{3C20D95F-AB5F-44EC-8FB6-CB9827B7FD63}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "design", "design", "{815BE8D0-C7D5-4B4E-82E0-DE29C11F258E}" + ProjectSection(SolutionItems) = preProject + docs\design\MQ1000.md = docs\design\MQ1000.md + docs\design\MQ1001.md = docs\design\MQ1001.md EndProjectSection EndProject Global @@ -32,6 +55,12 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {4C3E1C72-8977-4ABD-8360-A8707DBBA5AE} = {1D59B801-B4D3-44FC-A2BE-F2F53AC54061} + {DC2CDF77-88A9-490D-84ED-34832943104A} = {1D59B801-B4D3-44FC-A2BE-F2F53AC54061} + {3C20D95F-AB5F-44EC-8FB6-CB9827B7FD63} = {1D59B801-B4D3-44FC-A2BE-F2F53AC54061} + {815BE8D0-C7D5-4B4E-82E0-DE29C11F258E} = {3C20D95F-AB5F-44EC-8FB6-CB9827B7FD63} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3307E7F7-9CD7-4C12-B34A-943F5A8B62A4} EndGlobalSection diff --git a/README.md b/README.md index 9bb1dfd..d83cd23 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,30 @@ # PosInformatique.Moq.Analyzers -PosInformatique.Moq.Analyzers is a library to verify syntax and code design when writing the unit tests using the Moq library. +PosInformatique.Moq.Analyzers is a library to verify syntax and code design when writing the unit tests using the [Moq](https://github.com/devlooped/moq) library. + +## Installing from NuGet +The [PosInformatique.Moq.Analyzers](https://www.nuget.org/packages/PosInformatique.FluentAssertions.Json/) +library is available directly on the +[![Nuget](https://img.shields.io/nuget/v/PosInformatique.Moq.Analyzers)](https://www.nuget.org/packages/PosInformatique.Moq.Analyzers/) +official website. + +To download and install the library to your Visual Studio unit test projects use the following NuGet command line + +``` +Install-Package PosInformatique.Moq.Analyzers +``` + +The analyzers are automatically added and activated with their default warning levels. + +## Rules + +This section describes the list of the rules analyzed by the library to improve code quality of the unit tests using +the [Moq](https://github.com/devlooped/moq) library. + +### Design + +Design rules used to make your unit tests more strongly strict. + +| Rule | Description | +| - | - | +| [MQ1000: Verify() or VerifyAll() methods should be called when instantiate a Mock instances](docs/design/MQ1000.md) | When a static member of a generic type is called, the type argument must be specified for the type. When a generic instance member that does not support inference is called, the type argument must be specified for the member. In these two cases, the syntax for specifying the type argument is different and easily confused. | +| [MQ1001: The Mock instance behavior should be defined to Strict mode](docs/design/MQ1001.md) | A class declares and implements an instance field that is a System.IDisposable type and the class does not implement IDisposable. A class that declares an IDisposable field indirectly owns an unmanaged resource and should implement the IDisposable interface. | diff --git a/build/azure-pipelines-ci.yaml b/build/azure-pipelines-ci.yaml new file mode 100644 index 0000000..753769c --- /dev/null +++ b/build/azure-pipelines-ci.yaml @@ -0,0 +1,24 @@ +trigger: none + +pool: + vmImage: ubuntu-latest + +jobs: +- job: Build + displayName: Build the library + steps: + - task: DotNetCoreCLI@2 + name: BuildLibrary + displayName: Build the library + inputs: + command: 'build' + projects: 'PosInformatique.Moq.Analyzers.sln' + arguments: '--property:Configuration=Debug' + + - task: DotNetCoreCLI@2 + name: ExecuteUnitTests + displayName: Execute the unit tests + inputs: + command: 'test' + projects: 'PosInformatique.Moq.Analyzers.sln' + arguments: '--property:Configuration=Debug' \ No newline at end of file diff --git a/build/azure-pipelines-release.yaml b/build/azure-pipelines-release.yaml new file mode 100644 index 0000000..86762a3 --- /dev/null +++ b/build/azure-pipelines-release.yaml @@ -0,0 +1,54 @@ +parameters: +- name: VersionPrefix + displayName: The version of the library + type: string + default: 1.0.0 +- name: VersionSuffix + displayName: The version suffix of the library (rc.1). Use a space ' ' if no suffix. + type: string + default: rc.1 + +trigger: none +pr: none + +pool: + vmImage: ubuntu-latest + +jobs: +- job: Build + displayName: Build the library + steps: + - task: PowerShell@2 + name: UpdateBuildNumber + displayName: Update build number + inputs: + targetType: 'inline' + script: ' + if ("${{parameters.VersionSuffix}}".Trim() -eq "") + { + Write-Host "##vso[build.updatebuildnumber]${{parameters.VersionPrefix}}" + } + else + { + Write-Host "##vso[build.updatebuildnumber]${{parameters.VersionPrefix}}-${{parameters.VersionSuffix}}" + }' + + - task: DotNetCoreCLI@2 + name: BuildLibrary + displayName: Build the library + inputs: + command: 'pack' + packagesToPack: 'src/Moq.Analyzers/Moq.Analyzers.csproj' + configuration: 'Release' + versioningScheme: 'off' + buildProperties: 'VersionPrefix=${{parameters.VersionPrefix}};VersionSuffix=${{parameters.VersionSuffix}}' + verbosityPack: 'Normal' + + - task: NuGetCommand@2 + name: PublishNuGetPackages + displayName: Publish to NuGet + inputs: + command: 'push' + packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;!$(Build.ArtifactStagingDirectory)/**/*.symbols.nupkg' + nuGetFeedType: 'external' + publishFeedCredentials: 'nuget.org' \ No newline at end of file diff --git a/docs/design/MQ1000.md b/docs/design/MQ1000.md new file mode 100644 index 0000000..5d18504 --- /dev/null +++ b/docs/design/MQ1000.md @@ -0,0 +1,45 @@ +# MQ1000: `Verify()` and `VerifyAll()` methods should be called when instantiate a Mock instances + +| Property | Value | +|-------------------------------------|--------------------------------------------------------------------------------------------| +| **Rule ID** | MQ1000 | +| **Title** | `Verify()` and `VerifyAll()` methods should be called when instantiate a Mock instances | +| **Category** | Design | +| **Default severity** | Warning | + +## Cause + +A `Verify()` or `VerifyAll()` of an `Mock` instance has not been called in the *Assert* phase +of an unit test. + +## Rule description + +When instantiating a `Mock` in the *Arrange* phase of an unit test, `Verify()` or `VerifyAll()` method +should be called in the *Assert* phase to check the setup methods has been called. + +```csharp +[Fact] +public void GetCustomer_ShouldCallRepository() +{ + // Arrange + var smtpService = new Mock(); + smtpService.Setup(s => s.SendMail("sender@domain.com", "Gilles")); + + var service = new CustomerService(repository.Object); + + // Act + service.SendMail("Gilles"); + + // Arrange + smtpService.VerifyAll(); // The VerifyAll() will check that the mocked ISmtpService.SendMail() has been called. +} +``` + +## How to fix violations + +To fix a violation of this rule, call the `Verify()` or `VerifyAll()` in the *Assert* phase +on the `Mock` instances created during the *Arrange* phase. + +## When to suppress warnings + +Do not suppress a warning from this rule. Normally all setup methods must be called. diff --git a/docs/design/MQ1001.md b/docs/design/MQ1001.md new file mode 100644 index 0000000..78d5b9f --- /dev/null +++ b/docs/design/MQ1001.md @@ -0,0 +1,87 @@ +# MQ1001: The `Mock` instance behavior should be defined to Strict mode + +| Property | Value | +|-------------------------------------|------------------------------------------------------------------| +| **Rule ID** | MQ1001 | +| **Title** | The `Mock` instance behavior should be defined to Strict mode | +| **Category** | Design | +| **Default severity** | Warning | + +## Cause + +A Mock instance has been created with the `Loose` behavior instead of `Strict`. + +## Rule description + +When instantiating a `Mock` instance, the `MockBehavior` of the `Mock` instance should be defined to `Strict`. + +By default, [Moq](https://github.com/devlooped/moq) build instances of mocked instance which have a `Loose` behavior for non-setup methods. + +For example with the following code: +```csharp +public interface IRepository +{ + int GetData(); +} + +public class Service +{ + private readonly IRepository repository; + + public Service(IRepository repository) + { + this.repository = repository; + } + + public int GetDataFromRepository() + { + return this.repository.GetData(); + } +} +``` + +If a `Mock` instance of `IRepository` is created with the `Loose` behavior (default behavior of [Moq](https://github.com/devlooped/moq)) and +the `GetData()` method has not been setup, the default `int` value (`0`) will be returned. + +```csharp +[Fact] +public void GetDataFromRepository() +{ + // Arrange + var repository = new Mock(); // Default behavior (Loose) + + // /!\ No methods on the ISmtpService has been setup ! + + var service = new CustomerService(repository.Object); + + // Act + var result = service.GetDataFromRepository(); // "result" will be defined to 0. +} +``` + +To fix the violation of this rule for the previous example, +set the `MockBehavior` to `Strict` in the constructor of the `Mock` class. + +```csharp +[Fact] +public void GetDataFromRepository() +{ + // Arrange + var repository = new Mock(MockBehavior.Strict); // Strict behavior (Loose) + + // /!\ No methods on the ISmtpService has been setup ! + + var service = new CustomerService(repository.Object); + + // Act + var result = service.GetDataFromRepository(); // A "MoqException" will be raised to indicate that the GetData() method has not been setup ! +} +``` + +## How to fix violations + +To fix a violation of this rule, set the `MockBehavior` to `Strict` in the constructor of the `Mock` class. + +## When to suppress warnings + +Do not suppress a warning from this rule. Normally all methods called on a mocked instance must be setup. diff --git a/src/Moq.Analyzers/AnalyzerReleases.Shipped.md b/src/Moq.Analyzers/AnalyzerReleases.Shipped.md index d9dad6d..ee26229 100644 --- a/src/Moq.Analyzers/AnalyzerReleases.Shipped.md +++ b/src/Moq.Analyzers/AnalyzerReleases.Shipped.md @@ -1,8 +1,8 @@ -## Release 1.0 +## Release 1.0.0 ### New Rules Rule ID | Category | Severity | Notes --------|----------|----------|-------------------- -MQ2000 | Design | Warning | -MQ2001 | Design | Warning | MockInstanceShouldBeStrictBehavior \ No newline at end of file +MQ1000 | Design | Warning | Verify() and VerifyAll() methods should be called when instantiate a Mock instances +MQ1001 | Design | Warning | The `Mock` instance behavior should be defined to Strict mode \ No newline at end of file diff --git a/src/Moq.Analyzers/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzer.cs b/src/Moq.Analyzers/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzer.cs index 340a691..9598dcd 100644 --- a/src/Moq.Analyzers/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzer.cs +++ b/src/Moq.Analyzers/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzer.cs @@ -1,21 +1,25 @@ -namespace PosInformatique.Moq.Analyzers +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Moq.Analyzers { + using System.Collections.Immutable; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; - using System; - using System.Collections.Immutable; - using System.Linq; [DiagnosticAnalyzer(LanguageNames.CSharp)] public class MockInstanceShouldBeStrictBehaviorAnalyzer : DiagnosticAnalyzer { - public const string DiagnosticId = "MQ2001"; + public const string DiagnosticId = "MQ1001"; private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( DiagnosticId, - "Check that Mock instances are instantiate with the Strict behavior", + "The Mock instance behavior should be defined to Strict mode", "The Mock instance behavior should be defined to Strict mode", "Design", DiagnosticSeverity.Warning, @@ -56,8 +60,8 @@ private static void Analyze(SyntaxNodeAnalysisContext context) if (firstArgument is null) { var diagnostic = Diagnostic.Create(Rule, objectCreation.GetLocation()); - context.ReportDiagnostic(diagnostic); - + context.ReportDiagnostic(diagnostic); + return; } @@ -65,7 +69,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context) // (because we searching for MockBehavior.Strict). if (firstArgument.Expression is not MemberAccessExpressionSyntax memberAccessExpression) { - var diagnostic = Diagnostic.Create(Rule, objectCreation.GetLocation()); + var diagnostic = Diagnostic.Create(Rule, objectCreation.GetLocation()); context.ReportDiagnostic(diagnostic); return; diff --git a/src/Moq.Analyzers/Analyzers/VerifyAllShouldBeCalledAnalyzer.cs b/src/Moq.Analyzers/Analyzers/VerifyAllShouldBeCalledAnalyzer.cs index 89288e1..6264082 100644 --- a/src/Moq.Analyzers/Analyzers/VerifyAllShouldBeCalledAnalyzer.cs +++ b/src/Moq.Analyzers/Analyzers/VerifyAllShouldBeCalledAnalyzer.cs @@ -1,23 +1,28 @@ -namespace PosInformatique.Moq.Analyzers +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Moq.Analyzers { + using System.Collections.Immutable; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; - using System.Collections.Immutable; - using System.Linq; [DiagnosticAnalyzer(LanguageNames.CSharp)] public class VerifyAllShouldBeCalledAnalyzer : DiagnosticAnalyzer { private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( - "MQ2000", - "Check Verify() or VerifyAll() methods are called when instantiate a Mock instances", - "The Verify() or VerifyAll() method should be called", + "MQ1000", + "Verify() and VerifyAll() methods should be called when instantiate a Mock instances", + "The Verify() or VerifyAll() method should be called at the end of the unit test", "Design", DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: "VerifyAll() or VerifyAll() methods should be called in the test methods."); + description: "VerifyAll() or VerifyAll() methods should be called at the end of the unit test method."); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); diff --git a/src/Moq.Analyzers/CodeFixes/SetBehaviorToStrictCodeFixProvider.cs b/src/Moq.Analyzers/CodeFixes/SetBehaviorToStrictCodeFixProvider.cs index 19e621b..aaf91a8 100644 --- a/src/Moq.Analyzers/CodeFixes/SetBehaviorToStrictCodeFixProvider.cs +++ b/src/Moq.Analyzers/CodeFixes/SetBehaviorToStrictCodeFixProvider.cs @@ -1,21 +1,22 @@ -namespace PosInformatique.Moq.Analyzers +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Moq.Analyzers { + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Composition; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; - using Microsoft.CodeAnalysis.Rename; - using Microsoft.CodeAnalysis.Text; - using System; - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Composition; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SetBehaviorToStrictCodeFixProvider)), Shared] + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SetBehaviorToStrictCodeFixProvider))] + [Shared] public class SetBehaviorToStrictCodeFixProvider : CodeFixProvider { public sealed override ImmutableArray FixableDiagnosticIds @@ -43,12 +44,12 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) context.RegisterCodeFix( CodeAction.Create( title: "Defines the MockBehavior to Strict", - createChangedDocument: cancellationToken => AddMockBehiavorStrict(context.Document, mockCreationExpression, cancellationToken), + createChangedDocument: cancellationToken => AddMockBehiavorStrictArgumentAsync(context.Document, mockCreationExpression, cancellationToken), equivalenceKey: "Defines the MockBehavior to Strict"), diagnostic); } - private async Task AddMockBehiavorStrict(Document document, ObjectCreationExpressionSyntax oldMockCreationExpression, CancellationToken cancellationToken) + private static async Task AddMockBehiavorStrictArgumentAsync(Document document, ObjectCreationExpressionSyntax oldMockCreationExpression, CancellationToken cancellationToken) { var mockBehaviorArgument = SyntaxFactory.Argument( SyntaxFactory.MemberAccessExpression( @@ -87,7 +88,7 @@ private async Task AddMockBehiavorStrict(Document document, ObjectCrea return document.WithSyntaxRoot(newRoot); } - private bool IsMockBehaviorArgument(ArgumentSyntax argument) + private static bool IsMockBehaviorArgument(ArgumentSyntax argument) { if (argument.Expression is not MemberAccessExpressionSyntax memberAccessExpression) { diff --git a/src/Moq.Analyzers/Icon.png b/src/Moq.Analyzers/Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..41cf4062f3028f6aed379ac62d7cc65b2c14b25a GIT binary patch literal 6829 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 b00960reLzkUBjK-00000NkvXXu0mjfsqI4` literal 0 HcmV?d00001 diff --git a/src/Moq.Analyzers/MockExpressionHelper.cs b/src/Moq.Analyzers/MockExpressionHelper.cs index 5a4bdca..4161bad 100644 --- a/src/Moq.Analyzers/MockExpressionHelper.cs +++ b/src/Moq.Analyzers/MockExpressionHelper.cs @@ -1,4 +1,10 @@ -namespace PosInformatique.Moq.Analyzers +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Moq.Analyzers { using Microsoft.CodeAnalysis.CSharp.Syntax; diff --git a/src/Moq.Analyzers/Moq.Analyzers.csproj b/src/Moq.Analyzers/Moq.Analyzers.csproj index e35a6eb..4bd9ab1 100644 --- a/src/Moq.Analyzers/Moq.Analyzers.csproj +++ b/src/Moq.Analyzers/Moq.Analyzers.csproj @@ -3,12 +3,21 @@ netstandard2.0 false - latest + true + + PosInformatique.Moq.Analyzers is a library to verify syntax and code design when writing the unit tests using the Moq library. + Icon.png + MIT + https://github.com/PosInformatique/PosInformatique.Moq.Analyzers + README.md + + 1.0.0 + - Initial version with the followings rules: + - MQ1000: Verify() and VerifyAll() methods should be called when instantiate a Mock<T> instances + - MQ1001: The Mock<T> instance behavior should be defined to Strict mode. + + moq analyzers unittest c# roselyn compiler source code - - *$(MSBuildProjectFile)* - - true @@ -16,4 +25,15 @@ + + + + + + + True + \ + + + diff --git a/stylecop.json b/stylecop.json new file mode 100644 index 0000000..b747ad1 --- /dev/null +++ b/stylecop.json @@ -0,0 +1,9 @@ +{ + "settings": { + "documentationRules": { + "companyName": "P.O.S Informatique", + "copyrightText": "Copyright (c) {companyName}. All rights reserved.", + "documentInternalElements": false + } + } +} \ No newline at end of file diff --git a/tests/.editorconfig b/tests/.editorconfig new file mode 100644 index 0000000..e7f11ff --- /dev/null +++ b/tests/.editorconfig @@ -0,0 +1,6 @@ +[*.cs] + +# StyleCop + +# SA1135: Using directives should be qualified +dotnet_diagnostic.SA1135.severity = none diff --git a/tests/Moq.Analyzers.Tests/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzerTest.cs index 5cbff45..63c17cf 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzerTest.cs @@ -1,4 +1,10 @@ -namespace PosInformatique.Moq.Analyzers.Tests +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Moq.Analyzers.Tests { using System.Threading.Tasks; using Xunit; @@ -304,6 +310,5 @@ public enum MockBehavior { Loose } await Verify.VerifyAnalyzerAsync(source); } - } } \ No newline at end of file diff --git a/tests/Moq.Analyzers.Tests/Analyzers/VerifyAllShouldBeCalledAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/VerifyAllShouldBeCalledAnalyzerTest.cs index 99cc389..52f89d1 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/VerifyAllShouldBeCalledAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/VerifyAllShouldBeCalledAnalyzerTest.cs @@ -1,4 +1,10 @@ -namespace PosInformatique.Moq.Analyzers.Tests +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Moq.Analyzers.Tests { using System.Threading.Tasks; using Xunit; diff --git a/tests/Moq.Analyzers.Tests/CodeFixes/SetBehaviorToStrictCodeFixProviderTest.cs b/tests/Moq.Analyzers.Tests/CodeFixes/SetBehaviorToStrictCodeFixProviderTest.cs index 793c600..1aa6234 100644 --- a/tests/Moq.Analyzers.Tests/CodeFixes/SetBehaviorToStrictCodeFixProviderTest.cs +++ b/tests/Moq.Analyzers.Tests/CodeFixes/SetBehaviorToStrictCodeFixProviderTest.cs @@ -1,4 +1,10 @@ -namespace PosInformatique.Moq.Analyzers.Tests +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Moq.Analyzers.Tests { using System.Threading.Tasks; using Xunit; From e27b34d0006d773b4eabb1f5aaeebb274cf38237 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Tue, 31 Oct 2023 23:08:57 +0100 Subject: [PATCH 3/6] Fix the unit tests previously failed. --- .../MockInstanceShouldBeStrictBehaviorAnalyzer.cs | 2 +- ...ckInstanceShouldBeStrictBehaviorAnalyzerTest.cs | 3 ++- .../SetBehaviorToStrictCodeFixProviderTest.cs | 14 ++++++++++++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/Moq.Analyzers/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzer.cs b/src/Moq.Analyzers/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzer.cs index 9598dcd..ebda2ba 100644 --- a/src/Moq.Analyzers/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzer.cs +++ b/src/Moq.Analyzers/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzer.cs @@ -89,7 +89,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context) if (!SymbolEqualityComparer.Default.Equals(firstArgumentType.Symbol, mockBehaviorType)) { - var diagnostic = Diagnostic.Create(Rule, objectCreation.GetLocation()); + var diagnostic = Diagnostic.Create(Rule, firstArgument.GetLocation()); context.ReportDiagnostic(diagnostic); return; diff --git a/tests/Moq.Analyzers.Tests/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzerTest.cs index 63c17cf..9463f8e 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzerTest.cs @@ -24,6 +24,7 @@ public class TestClass { public void TestMethod() { + var obj = new object(); } } @@ -179,7 +180,7 @@ public class TestClass { public void TestMethod() { - var mock1 = new Mock([|default|], 1, 2); + var mock1 = [|new Mock(default, 1, 2)|]; } } diff --git a/tests/Moq.Analyzers.Tests/CodeFixes/SetBehaviorToStrictCodeFixProviderTest.cs b/tests/Moq.Analyzers.Tests/CodeFixes/SetBehaviorToStrictCodeFixProviderTest.cs index 1aa6234..7ab07b9 100644 --- a/tests/Moq.Analyzers.Tests/CodeFixes/SetBehaviorToStrictCodeFixProviderTest.cs +++ b/tests/Moq.Analyzers.Tests/CodeFixes/SetBehaviorToStrictCodeFixProviderTest.cs @@ -211,13 +211,18 @@ public class TestClass { public void TestMethod() { - var mock = [|new Mock(1, 2, 3)|]; + var mock1 = [|new Mock(1, 2, 3)|]; + var mock2 = new Mock(MockBehavior.[|Loose|], 1, 2, 3); + var mock3 = new Mock([|OtherEnum.A|], 1, 2, 3); + var mock4 = new Mock([|int.MaxValue|], 1, 2, 3); } } public interface I { } + + public enum OtherEnum { A } } namespace Moq @@ -254,13 +259,18 @@ public class TestClass { public void TestMethod() { - var mock = new Mock(MockBehavior.Strict, 1, 2, 3); + var mock1 = new Mock(MockBehavior.Strict, 1, 2, 3); + var mock2 = new Mock(MockBehavior.Strict, 1, 2, 3); + var mock3 = new Mock(MockBehavior.Strict, OtherEnum.A, 1, 2, 3); + var mock4 = new Mock(MockBehavior.Strict, int.MaxValue, 1, 2, 3); } } public interface I { } + + public enum OtherEnum { A } } namespace Moq From f4183cc5123d7f104faeaae24cf01cc6826aa0bf Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Tue, 31 Oct 2023 23:16:21 +0100 Subject: [PATCH 4/6] Various fixes --- README.md | 2 +- src/Moq.Analyzers/Moq.Analyzers.csproj | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index d83cd23..d484ae4 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ To download and install the library to your Visual Studio unit test projects use Install-Package PosInformatique.Moq.Analyzers ``` -The analyzers are automatically added and activated with their default warning levels. +The analyzer is automatically added and activated with their default severity levels. ## Rules diff --git a/src/Moq.Analyzers/Moq.Analyzers.csproj b/src/Moq.Analyzers/Moq.Analyzers.csproj index 4bd9ab1..781d844 100644 --- a/src/Moq.Analyzers/Moq.Analyzers.csproj +++ b/src/Moq.Analyzers/Moq.Analyzers.csproj @@ -2,7 +2,6 @@ netstandard2.0 - false true PosInformatique.Moq.Analyzers is a library to verify syntax and code design when writing the unit tests using the Moq library. From 6cc494e559d837bed63576f1a385417f33009ed4 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Tue, 31 Oct 2023 23:30:49 +0100 Subject: [PATCH 5/6] Fix the README files. --- README.md | 4 ++-- docs/design/MQ1000.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d484ae4..590a7b7 100644 --- a/README.md +++ b/README.md @@ -26,5 +26,5 @@ Design rules used to make your unit tests more strongly strict. | Rule | Description | | - | - | -| [MQ1000: Verify() or VerifyAll() methods should be called when instantiate a Mock instances](docs/design/MQ1000.md) | When a static member of a generic type is called, the type argument must be specified for the type. When a generic instance member that does not support inference is called, the type argument must be specified for the member. In these two cases, the syntax for specifying the type argument is different and easily confused. | -| [MQ1001: The Mock instance behavior should be defined to Strict mode](docs/design/MQ1001.md) | A class declares and implements an instance field that is a System.IDisposable type and the class does not implement IDisposable. A class that declares an IDisposable field indirectly owns an unmanaged resource and should implement the IDisposable interface. | +| [MQ1000: `Verify()` and `VerifyAll()` methods should be called when instantiate a `Mock` instances](docs/design/MQ1000.md) | When instantiating a `Mock` in the *Arrange* phase of an unit test, `Verify()` or `VerifyAll()` method should be called in the *Assert* phase to check the setup methods has been called. | +| [MQ1001: The `Mock` instance behavior should be defined to Strict mode](docs/design/MQ1001.md) | When instantiating a `Mock` instance, the `MockBehavior` of the `Mock` instance should be defined to `Strict`. | diff --git a/docs/design/MQ1000.md b/docs/design/MQ1000.md index 5d18504..baf3117 100644 --- a/docs/design/MQ1000.md +++ b/docs/design/MQ1000.md @@ -1,4 +1,4 @@ -# MQ1000: `Verify()` and `VerifyAll()` methods should be called when instantiate a Mock instances +# MQ1000: `Verify()` and `VerifyAll()` methods should be called when instantiate a `Mock` instances | Property | Value | |-------------------------------------|--------------------------------------------------------------------------------------------| From 65e50a50df50d16b1ee0e5fbee02234521fddc31 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Tue, 31 Oct 2023 23:54:42 +0100 Subject: [PATCH 6/6] Variouis fixes for packaging as Analyzer. --- src/Moq.Analyzers/Moq.Analyzers.csproj | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Moq.Analyzers/Moq.Analyzers.csproj b/src/Moq.Analyzers/Moq.Analyzers.csproj index 781d844..fc36000 100644 --- a/src/Moq.Analyzers/Moq.Analyzers.csproj +++ b/src/Moq.Analyzers/Moq.Analyzers.csproj @@ -3,10 +3,14 @@ netstandard2.0 true + false + true + + true + true PosInformatique.Moq.Analyzers is a library to verify syntax and code design when writing the unit tests using the Moq library. Icon.png - MIT https://github.com/PosInformatique/PosInformatique.Moq.Analyzers README.md @@ -15,7 +19,9 @@ - MQ1000: Verify() and VerifyAll() methods should be called when instantiate a Mock<T> instances - MQ1001: The Mock<T> instance behavior should be defined to Strict mode. - moq analyzers unittest c# roselyn compiler source code + moq analyzers unittest c# roselyn compiler source code mock + + $(TargetsForTfmSpecificContentInPackage);_AddAnalyzersToOutput @@ -35,4 +41,10 @@ + + + + + +