From 6e9ed72eb2de16b37f499c9b0255a520ed0e2751 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Fri, 16 Aug 2024 16:37:09 +0200 Subject: [PATCH 1/7] Fix the PosInfoMoq2006 rule when Setup() a member in inherited class (fixes #31). --- .github/workflows/github-actions-release.yml | 2 +- PosInformatique.Moq.Analyzers.sln | 10 ++++++++++ ...UsedWithProtectedOrInternalMembersAnalyzer.cs | 2 +- src/Moq.Analyzers/SymbolExtensions.cs | 16 ++++++++++++++++ ...WithProtectedOrInternalMembersAnalyzerTest.cs | 9 +++++++++ 5 files changed, 37 insertions(+), 2 deletions(-) diff --git a/.github/workflows/github-actions-release.yml b/.github/workflows/github-actions-release.yml index 2b9cec9..e36436c 100644 --- a/.github/workflows/github-actions-release.yml +++ b/.github/workflows/github-actions-release.yml @@ -7,7 +7,7 @@ on: type: string description: The version of the library required: true - default: 1.9.2 + default: 1.9.3 VersionSuffix: type: string description: The version suffix of the library (for example rc.1) diff --git a/PosInformatique.Moq.Analyzers.sln b/PosInformatique.Moq.Analyzers.sln index 26db6a1..5195eea 100644 --- a/PosInformatique.Moq.Analyzers.sln +++ b/PosInformatique.Moq.Analyzers.sln @@ -53,6 +53,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Compilation", "Compilation" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Analyzers.Sandbox", "tests\Moq.Analyzers.Sandbox\Moq.Analyzers.Sandbox.csproj", "{07F970A1-1477-4D4C-B233-C9B4DA6E3AD6}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{FA7258E1-65F0-4C40-A122-D76A1F0C79A0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{0EACA365-AFB2-4200-91FD-A20DC9617799}" + ProjectSection(SolutionItems) = preProject + .github\workflows\github-actions-ci.yaml = .github\workflows\github-actions-ci.yaml + .github\workflows\github-actions-release.yml = .github\workflows\github-actions-release.yml + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -80,6 +88,8 @@ Global {3C20D95F-AB5F-44EC-8FB6-CB9827B7FD63} = {1D59B801-B4D3-44FC-A2BE-F2F53AC54061} {815BE8D0-C7D5-4B4E-82E0-DE29C11F258E} = {3C20D95F-AB5F-44EC-8FB6-CB9827B7FD63} {D9C84D36-7F9C-4EFB-BE6F-9F7A05FE957D} = {3C20D95F-AB5F-44EC-8FB6-CB9827B7FD63} + {FA7258E1-65F0-4C40-A122-D76A1F0C79A0} = {1D59B801-B4D3-44FC-A2BE-F2F53AC54061} + {0EACA365-AFB2-4200-91FD-A20DC9617799} = {FA7258E1-65F0-4C40-A122-D76A1F0C79A0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3307E7F7-9CD7-4C12-B34A-943F5A8B62A4} diff --git a/src/Moq.Analyzers/Analyzers/SetupProtectedMustBeUsedWithProtectedOrInternalMembersAnalyzer.cs b/src/Moq.Analyzers/Analyzers/SetupProtectedMustBeUsedWithProtectedOrInternalMembersAnalyzer.cs index 526a7de..6a6194e 100644 --- a/src/Moq.Analyzers/Analyzers/SetupProtectedMustBeUsedWithProtectedOrInternalMembersAnalyzer.cs +++ b/src/Moq.Analyzers/Analyzers/SetupProtectedMustBeUsedWithProtectedOrInternalMembersAnalyzer.cs @@ -81,7 +81,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context) } // Check if a method exists with the specified name - foreach (var method in mockedType.GetMembers(methodName).OfType()) + foreach (var method in mockedType.GetAllMembers(methodName).OfType()) { if (!method.IsAbstract && !method.IsVirtual && !method.IsOverride) { diff --git a/src/Moq.Analyzers/SymbolExtensions.cs b/src/Moq.Analyzers/SymbolExtensions.cs index 8a8c5d4..8c5a0c1 100644 --- a/src/Moq.Analyzers/SymbolExtensions.cs +++ b/src/Moq.Analyzers/SymbolExtensions.cs @@ -32,5 +32,21 @@ public static bool IsOrInheritFrom(this ITypeSymbol? symbol, ITypeSymbol type) return IsOrInheritFrom(symbol.BaseType, type); } + + public static IEnumerable GetAllMembers(this ITypeSymbol symbol, string name) + { + IEnumerable baseTypeMembers; + + if (symbol.BaseType is not null) + { + baseTypeMembers = symbol.BaseType.GetAllMembers(name); + } + else + { + baseTypeMembers = Enumerable.Empty(); + } + + return symbol.GetMembers(name).Concat(baseTypeMembers); + } } } diff --git a/tests/Moq.Analyzers.Tests/Analyzers/SetupProtectedMustBeUsedWithProtectedOrInternalMembersAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/SetupProtectedMustBeUsedWithProtectedOrInternalMembersAnalyzerTest.cs index 327d033..1652c4e 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/SetupProtectedMustBeUsedWithProtectedOrInternalMembersAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/SetupProtectedMustBeUsedWithProtectedOrInternalMembersAnalyzerTest.cs @@ -31,8 +31,11 @@ public void TestMethod() mock1.Protected().Setup(""ProtectedAbstractMethod""); mock1.Protected().Setup(""ProtectedInternalAbstractMethod""); mock1.Protected().Setup(""InternalAbstractMethod""); + mock1.Protected().Setup(""ProtectedMethod""); mock1.Protected().Setup(""ProtectedOverrideMethod""); mock1.Protected().Setup(""ProtectedInternalOverrideMethod""); + mock1.Protected().Setup(""ProtectedInternalMethod""); + mock1.Protected().Setup(""InternalMethod""); mock1.Protected().Setup(""InternalOverrideMethod""); mock1.Protected().Setup(""ProtectedVirtualMethod""); @@ -75,9 +78,15 @@ public abstract class BaseClass { protected virtual void ProtectedOverrideMethod() { } + protected virtual void ProtectedMethod() { } + protected internal virtual void ProtectedInternalOverrideMethod() { } + protected internal virtual void ProtectedInternalMethod() { } + internal virtual void InternalOverrideMethod() { } + + internal virtual void InternalMethod() { } } }"; From e266c5d3222ea349d8da15778303ed2273209f50 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Fri, 16 Aug 2024 16:44:41 +0200 Subject: [PATCH 2/7] Fix the unit tests failed. --- ...ProtectedMustBeUsedWithProtectedOrInternalMembersAnalyzer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Moq.Analyzers/Analyzers/SetupProtectedMustBeUsedWithProtectedOrInternalMembersAnalyzer.cs b/src/Moq.Analyzers/Analyzers/SetupProtectedMustBeUsedWithProtectedOrInternalMembersAnalyzer.cs index 6a6194e..7c93bdd 100644 --- a/src/Moq.Analyzers/Analyzers/SetupProtectedMustBeUsedWithProtectedOrInternalMembersAnalyzer.cs +++ b/src/Moq.Analyzers/Analyzers/SetupProtectedMustBeUsedWithProtectedOrInternalMembersAnalyzer.cs @@ -90,7 +90,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context) if (method.IsSealed) { - continue; + break; } if (method.DeclaredAccessibility == Accessibility.Protected) From eb2eaacb5144bd2d5ed4195b086c9161b68133b4 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Mon, 19 Aug 2024 06:21:54 +0200 Subject: [PATCH 3/7] Fix the release notes. --- src/Moq.Analyzers/Moq.Analyzers.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Moq.Analyzers/Moq.Analyzers.csproj b/src/Moq.Analyzers/Moq.Analyzers.csproj index 241b850..7b91a1c 100644 --- a/src/Moq.Analyzers/Moq.Analyzers.csproj +++ b/src/Moq.Analyzers/Moq.Analyzers.csproj @@ -17,6 +17,9 @@ https://github.com/PosInformatique/PosInformatique.Moq.Analyzers README.md + 1.9.3 + - Fix the PosInfoMoq2006 when Setup() a method/property in inherited class. + 1.9.2 - Fix the PosInfoMoq1003 to raise warnings when using InSequence() method. - Fix the PosInfoMoq2003 to raise errors when using InSequence() method. From a9e928656c5d941a019f918f419037697baf7dcf Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 28 Aug 2024 11:43:27 +0200 Subject: [PATCH 4/7] Add the PosInfoMoq2012 and PosInfoMoq2013 rules. --- PosInformatique.Moq.Analyzers.sln | 2 + README.md | 2 + docs/Compilation/PosInfoMoq2012.md | 63 +++ docs/Compilation/PosInfoMoq2013.md | 69 ++++ src/Moq.Analyzers/AnalyzerReleases.Shipped.md | 10 +- ...odDelegateMustMatchMockedMethodAnalyzer.cs | 145 +++++++ src/Moq.Analyzers/ChainMembersInvocation.cs | 26 ++ ...legateMustMatchMockedMethodAnalyzerTest.cs | 389 ++++++++++++++++++ 8 files changed, 705 insertions(+), 1 deletion(-) create mode 100644 docs/Compilation/PosInfoMoq2012.md create mode 100644 docs/Compilation/PosInfoMoq2013.md create mode 100644 src/Moq.Analyzers/Analyzers/ReturnsMethodDelegateMustMatchMockedMethodAnalyzer.cs create mode 100644 tests/Moq.Analyzers.Tests/Analyzers/ReturnsMethodDelegateMustMatchMockedMethodAnalyzerTest.cs diff --git a/PosInformatique.Moq.Analyzers.sln b/PosInformatique.Moq.Analyzers.sln index 5195eea..c4ca56b 100644 --- a/PosInformatique.Moq.Analyzers.sln +++ b/PosInformatique.Moq.Analyzers.sln @@ -49,6 +49,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Compilation", "Compilation" docs\Compilation\PosInfoMoq2009.md = docs\Compilation\PosInfoMoq2009.md docs\Compilation\PosInfoMoq2010.md = docs\Compilation\PosInfoMoq2010.md docs\Compilation\PosInfoMoq2011.md = docs\Compilation\PosInfoMoq2011.md + docs\Compilation\PosInfoMoq2012.md = docs\Compilation\PosInfoMoq2012.md + docs\Compilation\PosInfoMoq2013.md = docs\Compilation\PosInfoMoq2013.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Moq.Analyzers.Sandbox", "tests\Moq.Analyzers.Sandbox\Moq.Analyzers.Sandbox.csproj", "{07F970A1-1477-4D4C-B233-C9B4DA6E3AD6}" diff --git a/README.md b/README.md index 9f2f2d0..f84a756 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,8 @@ All the rules of this category should not be disabled (or changed their severity | [PosInfoMoq2009: `Mock.Of` method must be used only to mock non-sealed class](docs/Compilation/PosInfoMoq2009.md) | The `Mock.Of` method can mock only interfaces or non-`sealed` classes | | [PosInfoMoq2010: `Mock.Of` method must be used only with types that contains parameterless contructor](docs/Compilation/PosInfoMoq2010.md) | The `Mock.Of` method requires a non-private parameterless contructor | | [PosInfoMoq2011: Constructor of the mocked class must be accessible.](docs/Compilation/PosInfoMoq2011.md) | The constructor of the instantiate mocked class must non-private. | +| [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. | +| [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. | diff --git a/docs/Compilation/PosInfoMoq2012.md b/docs/Compilation/PosInfoMoq2012.md new file mode 100644 index 0000000..b626c70 --- /dev/null +++ b/docs/Compilation/PosInfoMoq2012.md @@ -0,0 +1,63 @@ +# PosInfoMoq2012: The delegate in the argument of the `Returns()` method must return a value with same type of the mocked method. + +| Property | Value | +|-------------------------------------|-------------------------------------------------------------------------| +| **Rule ID** | PosInfoMoq2012 | +| **Title** | The delegate in the argument of the `Returns()` method must return a value with same type of the mocked method. | +| **Category** | Compilation | +| **Default severity** | Error | + +## Cause + +The delegate in the argument of the `Returns()` must return return a value of the same type as the mocked method or property. + +## Rule description + +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. + +```csharp +[Fact] +public void Test() +{ + var validMock = new Mock(); + validMock.Setup(s => s.GetData()) + .Returns(() => + { + return 1234; // OK, the mocked GetData() method return an int. + }); + validMock.Setup(s => s.IsAvailable) + .Returns(() => + { + return true; // OK, the mocked IsAvailable property return a bool. + }); + + var invalidMock = new Mock(); + invalidMock.Setup(s => s.GetData()) + .Returns(() => + { + return "Foobar"; // Error, the mocked GetData() method must return an int. + }); + invalidMock.Setup(s => s.IsAvailable) + .Returns(() => + { + return "Foobar"; // Error, the mocked IsAvailable property must return a bool. + }); +} + +public interface IService +{ + int GetData(); + + bool IsAvailable { get; } +} +``` + +## How to fix violations + +To fix a violation of this rule, be sure the lambda expression, anonymous method or method as parameter of the `Returns()` +method returns values with the same type of mocked method/property. + +## When to suppress warnings + +Do not suppress an error from this rule. If bypassed, the execution of the unit test will be failed with a `ArgumentException` +thrown with the *"Invalid callback. Setup on method with return type 'xxx' cannot invoke callback with return type 'yyy'."* message. diff --git a/docs/Compilation/PosInfoMoq2013.md b/docs/Compilation/PosInfoMoq2013.md new file mode 100644 index 0000000..a101d52 --- /dev/null +++ b/docs/Compilation/PosInfoMoq2013.md @@ -0,0 +1,69 @@ +# PosInfoMoq2013: The delegate in the argument of the `Returns()`/`ReturnsAsync()` method must have the same parameter types of the mocked method/property. + +| Property | Value | +|-------------------------------------|-------------------------------------------------------------------------| +| **Rule ID** | PosInfoMoq2013 | +| **Title** | The delegate in the argument of the `Returns()`/`ReturnsAsync()` method must have the same parameter types of the mocked method/property. | +| **Category** | Compilation | +| **Default severity** | Error | + +## Cause + +The delegate in the argument of the `Returns()`/`ReturnsAsync()` method must have the same parameter types of the mocked method/property. + +## Rule description + +The lambda expression, anonymous method or method in the argument of the `Returns()` must have the same parameter types of the mocked method/property. +> NB: Moq allows to pass a delegate with no argument in the `Returns()`/`ReturnsAsync()` method even the setup method contains arguments. + +```csharp +[Fact] +public void Test() +{ + var validMock = new Mock(); + validMock.Setup(s => s.GetData(1234)) + .Returns((int a) => // OK, the mocked GetData() take a int value as argument. + { + return 1234; + }); + validMock.Setup(s => s.GetData(1234)) + .Returns(() => // OK, Moq allows no arguments. + { + return 1234; + }); + validMock.Setup(s => s.IsAvailable) // OK, property don't have arguments. + .Returns(() => + { + return true; + }); + + var invalidMock = new Mock(); + invalidMock.Setup(s => s.GetData(1234)) + .Returns((string s) => // Error, the mocked GetData() take a int value as argument. + { + return "Foobar"; + }); + invalidMock.Setup(s => s.IsAvailable) + .Returns((string s) => // Error, mocked property have no arguments. + { + return "Foobar"; + }); +} + +public interface IService +{ + int GetData(int id); + + bool IsAvailable { get; } +} +``` + +## How to fix violations + +To fix a violation of this rule, be sure the lambda expression, anonymous method or method as parameter of the `Returns()`/`ReturnsAsync()` +method must have the same arguments type of the setup property/method. + +## When to suppress warnings + +Do not suppress an error from this rule. If bypassed, the execution of the unit test will be failed with a `ArgumentException` +thrown with the *"Object of type 'xxx' cannot be converted to type 'yyy'."* message. diff --git a/src/Moq.Analyzers/AnalyzerReleases.Shipped.md b/src/Moq.Analyzers/AnalyzerReleases.Shipped.md index baaa713..2111b71 100644 --- a/src/Moq.Analyzers/AnalyzerReleases.Shipped.md +++ b/src/Moq.Analyzers/AnalyzerReleases.Shipped.md @@ -1,4 +1,12 @@ -## Release 1.9.1 +## Release 1.10.0 + +### New Rules +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +PosInfoMoq2012 | Compilation | Error | The delegate in the argument of the `Returns()` method must return a value with same type of the mocked method. +PosInfoMoq2013 | Compilation | Error | The delegate in the argument of the `Returns()`/`ReturnsAsync()` method must have the same parameter types of the mocked method/property. + +## Release 1.9.1 ### New Rules Rule ID | Category | Severity | Notes diff --git a/src/Moq.Analyzers/Analyzers/ReturnsMethodDelegateMustMatchMockedMethodAnalyzer.cs b/src/Moq.Analyzers/Analyzers/ReturnsMethodDelegateMustMatchMockedMethodAnalyzer.cs new file mode 100644 index 0000000..5b072d2 --- /dev/null +++ b/src/Moq.Analyzers/Analyzers/ReturnsMethodDelegateMustMatchMockedMethodAnalyzer.cs @@ -0,0 +1,145 @@ +//----------------------------------------------------------------------- +// +// 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; + + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class ReturnsMethodDelegateMustMatchMockedMethodAnalyzer : DiagnosticAnalyzer + { + internal static readonly DiagnosticDescriptor ReturnValueMustMatchRule = new DiagnosticDescriptor( + "PosInfoMoq2012", + "The delegate in the argument of the Returns() method must return a value with same type of the mocked method/property", + "The delegate in the argument of the Returns() method must return a '{0}' type value", + "Compilation", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "The delegate in the argument of the Returns() method must return a value with same type of the mocked method/property.", + helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2012.html"); + + private static readonly DiagnosticDescriptor ArgumentMustMatchRule = new DiagnosticDescriptor( + "PosInfoMoq2013", + "The delegate in the argument of the Returns()/ReturnsAsync() method must have the same parameter types of the mocked method/property", + "The delegate in the argument of the Returns()/ReturnsAsync() method must have the same parameter types of the mocked method/property", + "Compilation", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "The delegate in the argument of the Returns()/ReturnsAsync() method must have the same parameter types of the mocked method/property.", + helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2012.html"); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(ReturnValueMustMatchRule, ArgumentMustMatchRule); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression); + } + + private static void Analyze(SyntaxNodeAnalysisContext context) + { + var invocationExpression = (InvocationExpressionSyntax)context.Node; + + var moqSymbols = MoqSymbols.FromCompilation(context.Compilation); + + if (moqSymbols is null) + { + return; + } + + var invocationExpressionSymbol = context.SemanticModel.GetSymbolInfo(invocationExpression, context.CancellationToken); + + if (invocationExpressionSymbol.Symbol is not IMethodSymbol methodSymbol) + { + return; + } + + if (!moqSymbols.IsReturnsMethod(methodSymbol) && !moqSymbols.IsReturnsAsyncMethod(methodSymbol)) + { + return; + } + + // Gets the first argument of the Returns() method. + if (invocationExpression.ArgumentList.Arguments.Count != 1) + { + return; + } + + var firstArgumentExpression = invocationExpression.ArgumentList.Arguments[0].Expression; + + if (firstArgumentExpression is not ParenthesizedLambdaExpressionSyntax delegateMethodSyntax) + { + return; + } + + var firstArgumentSymbol = context.SemanticModel.GetSymbolInfo(firstArgumentExpression, context.CancellationToken); + + if (firstArgumentSymbol.Symbol is not IMethodSymbol delegateMethodSymbol) + { + return; + } + + var moqExpressionAnalyzer = new MoqExpressionAnalyzer(moqSymbols, context.SemanticModel); + + // Extract the Setup() method. + var setupMethod = moqExpressionAnalyzer.ExtractSetupMethod(invocationExpression, context.CancellationToken); + + if (setupMethod is null) + { + return; + } + + // Check the return type + if (!moqSymbols.IsReturnsAsyncMethod(methodSymbol)) + { + var expectedReturnType = setupMethod.ReturnType; + + if (!SymbolEqualityComparer.Default.Equals(delegateMethodSymbol.ReturnType, expectedReturnType)) + { + context.ReportDiagnostic(ReturnValueMustMatchRule, firstArgumentExpression.GetLocation(), expectedReturnType.Name); + } + } + + // Check the argument types. + if (setupMethod.IsProperty) + { + if (delegateMethodSymbol.Parameters.Length > 0) + { + // With property, the Returns() method must have no arguments. + context.ReportDiagnostic(ArgumentMustMatchRule, delegateMethodSyntax.ParameterList.GetLocation()); + } + + return; + } + + if (delegateMethodSymbol.Parameters.Length == 0) + { + // No argument in the delegate method, Moq accept it. + return; + } + + if (delegateMethodSymbol.Parameters.Length != setupMethod.InvocationArguments.Count) + { + context.ReportDiagnostic(ArgumentMustMatchRule, delegateMethodSyntax.ParameterList.GetLocation()); + return; + } + + for (var i = 0; i < delegateMethodSymbol.Parameters.Length; i++) + { + if (!SymbolEqualityComparer.Default.Equals(delegateMethodSymbol.Parameters[i].Type, setupMethod.InvocationArguments[i].ParameterSymbol.Type)) + { + context.ReportDiagnostic(ArgumentMustMatchRule, delegateMethodSyntax.ParameterList.Parameters[i].GetLocation()); + } + } + } + } +} diff --git a/src/Moq.Analyzers/ChainMembersInvocation.cs b/src/Moq.Analyzers/ChainMembersInvocation.cs index 05e2a54..7e1ee71 100644 --- a/src/Moq.Analyzers/ChainMembersInvocation.cs +++ b/src/Moq.Analyzers/ChainMembersInvocation.cs @@ -20,6 +20,32 @@ public ChainMembersInvocation(IReadOnlyList members, IReadOnlyList< public IReadOnlyList InvocationArguments { get; } + public ITypeSymbol ReturnType + { + get + { + if (this.Members[0].Symbol is IMethodSymbol methodSymbol) + { + return methodSymbol.ReturnType; + } + + return ((IPropertySymbol)this.Members[0].Symbol).Type; + } + } + + public bool IsProperty + { + get + { + if (this.Members[0].Symbol is IMethodSymbol methodSymbol) + { + return false; + } + + return true; + } + } + public bool HasSameMembers(ChainMembersInvocation otherChainInvocation) { if (this.Members.Count != otherChainInvocation.Members.Count) diff --git a/tests/Moq.Analyzers.Tests/Analyzers/ReturnsMethodDelegateMustMatchMockedMethodAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/ReturnsMethodDelegateMustMatchMockedMethodAnalyzerTest.cs new file mode 100644 index 0000000..3aac2c0 --- /dev/null +++ b/tests/Moq.Analyzers.Tests/Analyzers/ReturnsMethodDelegateMustMatchMockedMethodAnalyzerTest.cs @@ -0,0 +1,389 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Moq.Analyzers.Tests +{ + using Microsoft.CodeAnalysis.Testing; + using Verifier = MoqCSharpAnalyzerVerifier; + + public class ReturnsMethodDelegateMustMatchMockedMethodAnalyzerTest + { + [Theory] + [InlineData("Returns")] + [InlineData("ReturnsAsync")] + public async Task Returns_ValidReturnsType_Method_NoDiagnosticReported(string methodName) + { + var returnType = "int"; + + if (methodName == "ReturnsAsync") + { + returnType = "Task<" + returnType + ">"; + } + + var source = @" + namespace ConsoleApplication1 + { + using Moq; + using System.Threading.Tasks; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(); + mock1.Setup(i => i.TestMethod()) + .Callback(() => { }) + ." + methodName + @"(() => { return 1234; }); + } + } + + public interface I + { + " + returnType + @" TestMethod(); + } + }"; + + await Verifier.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task Returns_InvalidReturnsType_Method_DiagnosticReported() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(); + mock1.Setup(i => i.TestMethod()) + .Callback(() => { }) + .Returns(() => { return ""Foobar""; }); + } + } + + public interface I + { + int TestMethod(); + } + }"; + + await Verifier.VerifyAnalyzerAsync( + source, + new DiagnosticResult(ReturnsMethodDelegateMustMatchMockedMethodAnalyzer.ReturnValueMustMatchRule) + .WithSpan(13, 42, 13, 68).WithArguments("Int32")); + } + + [Fact] + public async Task Returns_InvalidReturnsType_Property_DiagnosticReported() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(); + mock1.Setup(i => i.TestProperty) + .Callback(() => { }) + .Returns(() => { return ""Foobar""; }); + } + } + + public interface I + { + int TestProperty { get; set; } + } + }"; + + await Verifier.VerifyAnalyzerAsync( + source, + new DiagnosticResult(ReturnsMethodDelegateMustMatchMockedMethodAnalyzer.ReturnValueMustMatchRule) + .WithSpan(13, 42, 13, 68).WithArguments("Int32")); + } + + [Theory] + [InlineData("TestProperty")] + [InlineData("GetData()")] + public async Task Returns_ValidReturnsValue_NoDiagnosticReported(string member) + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(); + mock1.Setup(i => i." + member + @") + .Callback(() => { }) + .Returns(1234); + } + } + + public interface I + { + int GetData(); + + int TestProperty { get; set; } + } + }"; + + await Verifier.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task Returns_ValidReturnsType_Property_NoDiagnosticReported() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(); + mock1.Setup(i => i.TestProperty) + .Callback(() => { }) + .Returns(() => { return 1234; }); + } + } + + public interface I + { + int TestProperty { get; set; } + } + }"; + + await Verifier.VerifyAnalyzerAsync(source); + } + + [Theory] + [InlineData("Returns")] + [InlineData("ReturnsAsync")] + public async Task Returns_InvalidArgumentType_DiagnosticReported(string methodName) + { + var returnType = "int"; + + if (methodName == "ReturnsAsync") + { + returnType = "Task<" + returnType + ">"; + } + + var source = @" + namespace ConsoleApplication1 + { + using Moq; + using System.Threading.Tasks; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(); + mock1.Setup(i => i.TestMethod(""Ignored"")) + .Callback(() => { }) + ." + methodName + @"(({|PosInfoMoq2013:int s|}) => { return 1234; }); + } + } + + public interface I + { + " + returnType + @" TestMethod(string a); + } + }"; + + await Verifier.VerifyAnalyzerAsync(source); + } + + [Theory] + [InlineData("Returns")] + [InlineData("ReturnsAsync")] + public async Task Returns_DifferentArgumentCount_DiagnosticReported(string methodName) + { + var returnType = "int"; + + if (methodName == "ReturnsAsync") + { + returnType = "Task<" + returnType + ">"; + } + + var source = @" + namespace ConsoleApplication1 + { + using Moq; + using System.Threading.Tasks; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(); + mock1.Setup(i => i.TestMethod(""Ignored"")) + .Callback(() => { }) + ." + methodName + @"({|PosInfoMoq2013:(int s, bool b)|} => { return 1234; }); + } + } + + public interface I + { + " + returnType + @" TestMethod(string a); + } + }"; + + await Verifier.VerifyAnalyzerAsync(source); + } + + [Theory] + [InlineData("Returns")] + [InlineData("ReturnsAsync")] + public async Task Returns_ValidArgumentType_NoDiagnosticReported(string methodName) + { + var returnType = "int"; + + if (methodName == "ReturnsAsync") + { + returnType = "Task<" + returnType + ">"; + } + + var source = @" + namespace ConsoleApplication1 + { + using Moq; + using System.Threading.Tasks; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(); + mock1.Setup(i => i.TestMethod(""Ignored"")) + .Callback(() => { }) + ." + methodName + @"((string z) => { return 1234; }); + } + } + + public interface I + { + " + returnType + @" TestMethod(string a); + } + }"; + + await Verifier.VerifyAnalyzerAsync(source); + } + + [Theory] + [InlineData("Returns")] + [InlineData("ReturnsAsync")] + public async Task Returns_WithNoArgument_NoDiagnosticReported(string methodName) + { + var returnType = "int"; + + if (methodName == "ReturnsAsync") + { + returnType = "Task<" + returnType + ">"; + } + + var source = @" + namespace ConsoleApplication1 + { + using Moq; + using System.Threading.Tasks; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(); + mock1.Setup(i => i.TestMethod(""Ignored"")) + .Callback(() => { }) + ." + methodName + @"(() => { return 1234; }); + } + } + + public interface I + { + " + returnType + @" TestMethod(string a); + } + }"; + + await Verifier.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task Returns_WithArgumentsForProperty_DiagnosticReported() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(); + mock1.Setup(i => i.TestProperty) + .Callback(() => { }) + .Returns({|PosInfoMoq2013:(int s)|} => { return 1234; }); + } + } + + public interface I + { + int TestProperty { get; set; } + } + }"; + + await Verifier.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task NoMoqLibrary() + { + var source = @" + namespace ConsoleApplication1 + { + using OtherNamespace; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(); + mock1.Setup(); + } + } + + public interface I + { + } + } + + namespace OtherNamespace + { + public class Mock + { + public Mock() { } + + public void Setup() { } + } + }"; + + await Verifier.VerifyAnalyzerWithNoMoqLibraryAsync(source); + } + } +} \ No newline at end of file From 296ae6005bf435209afb0dc0d22a872f2ee616e2 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 28 Aug 2024 11:44:27 +0200 Subject: [PATCH 5/7] Change the version to 1.10.0 --- .github/workflows/github-actions-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-actions-release.yml b/.github/workflows/github-actions-release.yml index e36436c..f7f58c8 100644 --- a/.github/workflows/github-actions-release.yml +++ b/.github/workflows/github-actions-release.yml @@ -7,7 +7,7 @@ on: type: string description: The version of the library required: true - default: 1.9.3 + default: 1.10.0 VersionSuffix: type: string description: The version suffix of the library (for example rc.1) From d61a76d01078c3555045eaa2a35e96a7f403ddca Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 28 Aug 2024 13:00:28 +0200 Subject: [PATCH 6/7] Fix the PosInfoMoq2012 when the setup method return It.IsAny. --- ...odDelegateMustMatchMockedMethodAnalyzer.cs | 2 +- ...legateMustMatchMockedMethodAnalyzerTest.cs | 41 ++++++++++++++++--- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/Moq.Analyzers/Analyzers/ReturnsMethodDelegateMustMatchMockedMethodAnalyzer.cs b/src/Moq.Analyzers/Analyzers/ReturnsMethodDelegateMustMatchMockedMethodAnalyzer.cs index 5b072d2..28ee368 100644 --- a/src/Moq.Analyzers/Analyzers/ReturnsMethodDelegateMustMatchMockedMethodAnalyzer.cs +++ b/src/Moq.Analyzers/Analyzers/ReturnsMethodDelegateMustMatchMockedMethodAnalyzer.cs @@ -103,7 +103,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context) { var expectedReturnType = setupMethod.ReturnType; - if (!SymbolEqualityComparer.Default.Equals(delegateMethodSymbol.ReturnType, expectedReturnType)) + if (!moqSymbols.IsAnyType(expectedReturnType) && !SymbolEqualityComparer.Default.Equals(delegateMethodSymbol.ReturnType, expectedReturnType)) { context.ReportDiagnostic(ReturnValueMustMatchRule, firstArgumentExpression.GetLocation(), expectedReturnType.Name); } diff --git a/tests/Moq.Analyzers.Tests/Analyzers/ReturnsMethodDelegateMustMatchMockedMethodAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/ReturnsMethodDelegateMustMatchMockedMethodAnalyzerTest.cs index 3aac2c0..fedde52 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/ReturnsMethodDelegateMustMatchMockedMethodAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/ReturnsMethodDelegateMustMatchMockedMethodAnalyzerTest.cs @@ -12,12 +12,12 @@ namespace PosInformatique.Moq.Analyzers.Tests public class ReturnsMethodDelegateMustMatchMockedMethodAnalyzerTest { [Theory] - [InlineData("Returns")] - [InlineData("ReturnsAsync")] - public async Task Returns_ValidReturnsType_Method_NoDiagnosticReported(string methodName) + [InlineData("Returns", "int")] + [InlineData("ReturnsAsync", "int")] + [InlineData("Returns", "object")] + [InlineData("ReturnsAsync", "object")] + public async Task Returns_ValidReturnsType_Method_NoDiagnosticReported(string methodName, string returnType) { - var returnType = "int"; - if (methodName == "ReturnsAsync") { returnType = "Task<" + returnType + ">"; @@ -49,6 +49,37 @@ public interface I await Verifier.VerifyAnalyzerAsync(source); } + [Theory] + [InlineData("1234")] + [InlineData("\"Foobar\"")] + public async Task Returns_ValidReturnsType_Method_Generic_NoDiagnosticReported(string value) + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + using System.Threading.Tasks; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(); + mock1.Setup(i => i.TestMethod()) + .Callback(() => { }) + .Returns(() => { return " + value + @"; }); + } + } + + public interface I + { + T TestMethod(); + } + }"; + + await Verifier.VerifyAnalyzerAsync(source); + } + [Fact] public async Task Returns_InvalidReturnsType_Method_DiagnosticReported() { From bffde3158e16e3c792886de4debce63f9b128aa0 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Mon, 2 Sep 2024 15:35:18 +0200 Subject: [PATCH 7/7] Fix release notes --- src/Moq.Analyzers/Moq.Analyzers.csproj | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Moq.Analyzers/Moq.Analyzers.csproj b/src/Moq.Analyzers/Moq.Analyzers.csproj index 7b91a1c..a2d0d4d 100644 --- a/src/Moq.Analyzers/Moq.Analyzers.csproj +++ b/src/Moq.Analyzers/Moq.Analyzers.csproj @@ -17,16 +17,21 @@ https://github.com/PosInformatique/PosInformatique.Moq.Analyzers README.md + 1.10.0 + - Add new rules: + - 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.9.3 - Fix the PosInfoMoq2006 when Setup() a method/property in inherited class. - + 1.9.2 - Fix the PosInfoMoq1003 to raise warnings when using InSequence() method. - Fix the PosInfoMoq2003 to raise errors when using InSequence() method. 1.9.1 - Add new rules: - - PosInfoMoq2009: Mock.Of<T> method must be used only to mock non-sealed class + - PosInfoMoq2009: Mock.Of<T> method must be used only to mock non-sealed class - PosInfoMoq2010: Mock.Of<T> method must be used only with types that contains parameterless contructor - PosInfoMoq2011: Constructor of the mocked class must be accessible. - Fix the PosInfoMoq1001 to check the MockBehavior usage to strict.