From 350aca562fa8460c05073dd4db74b733c119bfb3 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Mon, 14 Oct 2024 09:52:16 +0200 Subject: [PATCH 01/15] Fix the documentation link of the PosInfoMoq2013 rule. --- .../ReturnsMethodDelegateMustMatchMockedMethodAnalyzer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Moq.Analyzers/Analyzers/ReturnsMethodDelegateMustMatchMockedMethodAnalyzer.cs b/src/Moq.Analyzers/Analyzers/ReturnsMethodDelegateMustMatchMockedMethodAnalyzer.cs index 28ee368..7a7c8b0 100644 --- a/src/Moq.Analyzers/Analyzers/ReturnsMethodDelegateMustMatchMockedMethodAnalyzer.cs +++ b/src/Moq.Analyzers/Analyzers/ReturnsMethodDelegateMustMatchMockedMethodAnalyzer.cs @@ -33,7 +33,7 @@ public class ReturnsMethodDelegateMustMatchMockedMethodAnalyzer : DiagnosticAnal 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"); + helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2013.html"); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(ReturnValueMustMatchRule, ArgumentMustMatchRule); From e5bda959d260fa52d9b094d81507b1a08d141571 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Tue, 22 Oct 2024 17:07:59 +0200 Subject: [PATCH 02/15] Add a new PosInfoMoq2014 rule to check if a delegate in the Callback() method does not a return a value (fixes: #35). --- PosInformatique.Moq.Analyzers.sln | 1 + README.md | 1 + docs/Compilation/PosInfoMoq2014.md | 45 +++++++++++++++ src/Moq.Analyzers/AnalyzerReleases.Shipped.md | 9 ++- ...ckDelegateMustMatchMockedMethodAnalyzer.cs | 55 +++++++++++++++++-- src/Moq.Analyzers/Moq.Analyzers.csproj | 4 ++ .../SyntaxNodeAnalysisContextExtensions.cs | 6 ++ ...legateMustMatchMockedMethodAnalyzerTest.cs | 50 +++++++++++++---- 8 files changed, 154 insertions(+), 17 deletions(-) create mode 100644 docs/Compilation/PosInfoMoq2014.md diff --git a/PosInformatique.Moq.Analyzers.sln b/PosInformatique.Moq.Analyzers.sln index c4ca56b..5d9a6d4 100644 --- a/PosInformatique.Moq.Analyzers.sln +++ b/PosInformatique.Moq.Analyzers.sln @@ -51,6 +51,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Compilation", "Compilation" docs\Compilation\PosInfoMoq2011.md = docs\Compilation\PosInfoMoq2011.md docs\Compilation\PosInfoMoq2012.md = docs\Compilation\PosInfoMoq2012.md docs\Compilation\PosInfoMoq2013.md = docs\Compilation\PosInfoMoq2013.md + docs\Compilation\PosInfoMoq2014.md = docs\Compilation\PosInfoMoq2014.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 f84a756..12ab764 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ All the rules of this category should not be disabled (or changed their severity | [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. | +| [PosInfoMoq2014: The `Callback()` delegate expression must not return a value.](docs/Compilation/PosInfoMoq2014.md) | The `Callback()` delegate expression must not return a value. | diff --git a/docs/Compilation/PosInfoMoq2014.md b/docs/Compilation/PosInfoMoq2014.md new file mode 100644 index 0000000..b935a48 --- /dev/null +++ b/docs/Compilation/PosInfoMoq2014.md @@ -0,0 +1,45 @@ +# PosInfoMoq2014: The `Callback()` delegate expression must not return a value. + +| Property | Value | +|-------------------------------------|-----------------------------------------------------------------------------------------| +| **Rule ID** | PosInfoMoq2014 | +| **Title** | The `Callback()` delegate expression must not return a value. | +| **Category** | Compilation | +| **Default severity** | Error | + +## Cause + +The delegate in the argument of the `Callback()` method must not return a value. + +## Rule description + +The lambda expression in the argument of the `Callback()` method must not return a value. + +```csharp +[Fact] +public void Test() +{ + var service = new Mock(); + service.Setup(s => s.GetData("TOURREAU", 1234)) + .Callback((string n, int age) => + { + // ... + return 1234; // The delegate in the Callback() method must not return a value. + }) + .Returns(10); +} + +public interface IService +{ + public int GetData(string name, int age) { } +} +``` + +## How to fix violations + +To fix a violation of this rule, be sure that the delegate method in the `Callback()` method does not return a value. + +## 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. This overload of the "Callback" method only accepts "void" (C#) or "Sub" (VB.NET) delegates with parameter types matching those of the set up method. (Parameter 'callback')"* message. diff --git a/src/Moq.Analyzers/AnalyzerReleases.Shipped.md b/src/Moq.Analyzers/AnalyzerReleases.Shipped.md index 2111b71..31e25bd 100644 --- a/src/Moq.Analyzers/AnalyzerReleases.Shipped.md +++ b/src/Moq.Analyzers/AnalyzerReleases.Shipped.md @@ -1,4 +1,11 @@ -## Release 1.10.0 +## Release 1.11.0 + +### New Rules +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +PosInfoMoq2014 | Compilation | Error | CallBackDelegateMustMatchMockedMethodAnalyzer, [Documentation](https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2014.html)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.10.0 ### New Rules Rule ID | Category | Severity | Notes diff --git a/src/Moq.Analyzers/Analyzers/CallBackDelegateMustMatchMockedMethodAnalyzer.cs b/src/Moq.Analyzers/Analyzers/CallBackDelegateMustMatchMockedMethodAnalyzer.cs index 6205c65..59c34d1 100644 --- a/src/Moq.Analyzers/Analyzers/CallBackDelegateMustMatchMockedMethodAnalyzer.cs +++ b/src/Moq.Analyzers/Analyzers/CallBackDelegateMustMatchMockedMethodAnalyzer.cs @@ -15,7 +15,7 @@ namespace PosInformatique.Moq.Analyzers [DiagnosticAnalyzer(LanguageNames.CSharp)] public class CallBackDelegateMustMatchMockedMethodAnalyzer : DiagnosticAnalyzer { - private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( + internal static readonly DiagnosticDescriptor CallbackMustMatchSignature = new DiagnosticDescriptor( "PosInfoMoq2003", "The Callback() delegate expression must match the signature of the mocked method", "The Callback() delegate expression must match the signature of the mocked method", @@ -25,7 +25,17 @@ public class CallBackDelegateMustMatchMockedMethodAnalyzer : DiagnosticAnalyzer description: "The Callback() delegate expression must match the signature of the mocked method.", helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2003.html"); - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + internal static readonly DiagnosticDescriptor CallbackMustNotReturnValue = new DiagnosticDescriptor( + "PosInfoMoq2014", + "The Callback() delegate expression must not return a value", + "The Callback() delegate expression must not return a value", + "Compilation", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "The Callback() delegate expression must not return a value.", + helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2014.html"); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(CallbackMustMatchSignature, CallbackMustNotReturnValue); public override void Initialize(AnalysisContext context) { @@ -56,6 +66,15 @@ private static void Analyze(SyntaxNodeAnalysisContext context) return; } + if (!callBackLambdaExpressionSymbol.ReturnsVoid) + { + var allReturnsStatements = ReturnStatementSyntaxFinder.GetAllReturnStatements(lambdaExpression!.Block!); + + var allReturnsStatementsLocations = allReturnsStatements.Select(statement => statement.GetLocation()); + + context.ReportDiagnostic(CallbackMustNotReturnValue, allReturnsStatementsLocations); + } + // Extracts the setup method from the Callback() method call. var setupMethod = moqExpressionAnalyzer.ExtractSetupMethod(invocationExpression, context.CancellationToken); @@ -68,7 +87,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context) // 1- Compare the number of the parameters if (callBackLambdaExpressionSymbol.Parameters.Length != setupMethod.InvocationArguments.Count) { - var diagnostic = Diagnostic.Create(Rule, lambdaExpression!.ParameterList.GetLocation()); + var diagnostic = Diagnostic.Create(CallbackMustMatchSignature, lambdaExpression!.ParameterList.GetLocation()); context.ReportDiagnostic(diagnostic); return; @@ -83,7 +102,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context) // The callback parameter associated must be an object. if (callBackLambdaExpressionSymbol.Parameters[i].Type.SpecialType != SpecialType.System_Object) { - var diagnostic = Diagnostic.Create(Rule, lambdaExpression!.ParameterList.Parameters[i].GetLocation()); + var diagnostic = Diagnostic.Create(CallbackMustMatchSignature, lambdaExpression!.ParameterList.Parameters[i].GetLocation()); context.ReportDiagnostic(diagnostic); continue; @@ -91,12 +110,38 @@ private static void Analyze(SyntaxNodeAnalysisContext context) } else if (!SymbolEqualityComparer.Default.Equals(callBackLambdaExpressionSymbol.Parameters[i].Type, setupMethod.InvocationArguments[i].ParameterSymbol.Type)) { - var diagnostic = Diagnostic.Create(Rule, lambdaExpression!.ParameterList.Parameters[i].GetLocation()); + var diagnostic = Diagnostic.Create(CallbackMustMatchSignature, lambdaExpression!.ParameterList.Parameters[i].GetLocation()); context.ReportDiagnostic(diagnostic); continue; } } } + + private sealed class ReturnStatementSyntaxFinder : CSharpSyntaxWalker + { + private readonly List returnStatements; + + private ReturnStatementSyntaxFinder() + { + this.returnStatements = new List(); + } + + public static IList GetAllReturnStatements(BlockSyntax blockSyntax) + { + var finder = new ReturnStatementSyntaxFinder(); + + finder.Visit(blockSyntax); + + return finder.returnStatements; + } + + public override void VisitReturnStatement(ReturnStatementSyntax node) + { + this.returnStatements.Add(node); + + base.VisitReturnStatement(node); + } + } } } diff --git a/src/Moq.Analyzers/Moq.Analyzers.csproj b/src/Moq.Analyzers/Moq.Analyzers.csproj index a2d0d4d..d6fcc5c 100644 --- a/src/Moq.Analyzers/Moq.Analyzers.csproj +++ b/src/Moq.Analyzers/Moq.Analyzers.csproj @@ -17,6 +17,10 @@ https://github.com/PosInformatique/PosInformatique.Moq.Analyzers README.md + 1.10.0 + - Add new rules: + - PosInfoMoq2014: The delegate in the argument of the Returns() method must return a value with same type of the mocked method. + 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. diff --git a/src/Moq.Analyzers/SyntaxNodeAnalysisContextExtensions.cs b/src/Moq.Analyzers/SyntaxNodeAnalysisContextExtensions.cs index 76d28fa..3acc7ef 100644 --- a/src/Moq.Analyzers/SyntaxNodeAnalysisContextExtensions.cs +++ b/src/Moq.Analyzers/SyntaxNodeAnalysisContextExtensions.cs @@ -16,5 +16,11 @@ public static void ReportDiagnostic(this SyntaxNodeAnalysisContext context, Diag var diagnostic = Diagnostic.Create(descriptor, location, messageArgs); context.ReportDiagnostic(diagnostic); } + + public static void ReportDiagnostic(this SyntaxNodeAnalysisContext context, DiagnosticDescriptor descriptor, IEnumerable locations, params object[]? messageArgs) + { + var diagnostic = Diagnostic.Create(descriptor, locations.First(), locations.Skip(1), messageArgs); + context.ReportDiagnostic(diagnostic); + } } } diff --git a/tests/Moq.Analyzers.Tests/Analyzers/CallBackDelegateMustMatchMockedMethodAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/CallBackDelegateMustMatchMockedMethodAnalyzerTest.cs index e57bf06..7316795 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/CallBackDelegateMustMatchMockedMethodAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/CallBackDelegateMustMatchMockedMethodAnalyzerTest.cs @@ -6,6 +6,7 @@ namespace PosInformatique.Moq.Analyzers.Tests { + using Microsoft.CodeAnalysis.Testing; using Verifier = MoqCSharpAnalyzerVerifier; public class CallBackDelegateMustMatchMockedMethodAnalyzerTest @@ -104,35 +105,57 @@ public void TestMethod() var mock1 = new Mock(); mock1" + sequence + @".Setup(m => m.TestMethod()) - .Callback([|(int too, int much, int parameters)|] => { }) + .Callback({|PosInfoMoq2003:(int too, int much, int parameters)|} => { }) .Throws(new Exception()); mock1" + sequence + @".Setup(m => m.TestMethod(default)) - .Callback([|()|] => { }) + .Callback({|PosInfoMoq2003:()|} => { }) .Throws(new Exception()); mock1" + sequence + @".Setup(m => m.TestMethod(default)) - .Callback(([|int otherType|]) => { }) + .Callback(({|PosInfoMoq2003:int otherType|}) => { }) .Throws(new Exception()); mock1" + sequence + @".Setup(m => m.TestMethod(default)) - .Callback([|(int too, int much, int parameters)|] => { }) + .Callback({|PosInfoMoq2003:(int too, int much, int parameters)|} => { }) .Throws(new Exception()); mock1" + sequence + @".Setup(m => m.TestGenericMethod(1234)) - .Callback(([|string x|]) => { }) + .Callback(({|PosInfoMoq2003:string x|}) => { }) .Throws(new Exception()); mock1" + sequence + @".Setup(m => m.TestGenericMethod(It.IsAny())) - .Callback(([|string x|]) => { }) + .Callback(({|PosInfoMoq2003:string x|}) => { }) .Throws(new Exception()); mock1" + sequence + @".Setup(m => m.TestMethodReturn()) - .Callback([|(int too, int much, int parameters)|] => { }) + .Callback({|PosInfoMoq2003:(int too, int much, int parameters)|} => { }) .Returns(1234); mock1" + sequence + @".Setup(m => m.TestMethodReturn(default)) - .Callback([|()|] => { }) + .Callback({|PosInfoMoq2003:()|} => { }) .Returns(1234); mock1" + sequence + @".Setup(m => m.TestMethodReturn(default)) - .Callback(([|int otherType|]) => { }) + .Callback(({|PosInfoMoq2003:int otherType|}) => { }) .Returns(1234); mock1" + sequence + @".Setup(m => m.TestMethodReturn(default)) - .Callback([|(int too, int much, int parameters)|] => { }) + .Callback({|PosInfoMoq2003:(int too, int much, int parameters)|} => { }) + .Returns(1234); + + // Callback with return value + mock1" + sequence + @".Setup(m => m.TestMethodReturn()) + .Callback(() => { {|PosInfoMoq2014:return false;|} }) + .Returns(1234); + mock1" + sequence + @".Setup(m => m.TestMethodReturn()) + .Callback(() => + { + var b = false; + if (b) + { + for (int i=0; i<10; i++) + { + if (i == 0) {|#0:return false;|}; + } + + {|#1:return false;|} + } + + {|#2:return true;|} + }) .Returns(1234); } } @@ -155,7 +178,12 @@ public interface I } }"; - await Verifier.VerifyAnalyzerAsync(source); + await Verifier.VerifyAnalyzerAsync( + source, + [ + new DiagnosticResult(CallBackDelegateMustMatchMockedMethodAnalyzer.CallbackMustNotReturnValue) + .WithSpan(59, 57, 59, 70).WithSpan(62, 41, 62, 54).WithSpan(65, 37, 65, 49), + ]); } [Fact] From 0a57f9907e4086efedf0b46b5cd6873100f806bb Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 23 Oct 2024 10:03:55 +0200 Subject: [PATCH 03/15] Add the PosInfoMoq2015 rule to check the return type of the Protected() setup methods (fixes #38). --- PosInformatique.Moq.Analyzers.sln | 1 + README.md | 1 + docs/Compilation/PosInfoMoq2006.md | 2 +- docs/Compilation/PosInfoMoq2015.md | 50 +++++++++ src/Moq.Analyzers/AnalyzerReleases.Shipped.md | 3 +- ...dWithProtectedOrInternalMembersAnalyzer.cs | 83 ++++++++++++-- src/Moq.Analyzers/Moq.Analyzers.csproj | 1 + ...hProtectedOrInternalMembersAnalyzerTest.cs | 101 ++++++++++++++---- 8 files changed, 211 insertions(+), 31 deletions(-) create mode 100644 docs/Compilation/PosInfoMoq2015.md diff --git a/PosInformatique.Moq.Analyzers.sln b/PosInformatique.Moq.Analyzers.sln index 5d9a6d4..5bf8b7a 100644 --- a/PosInformatique.Moq.Analyzers.sln +++ b/PosInformatique.Moq.Analyzers.sln @@ -52,6 +52,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Compilation", "Compilation" docs\Compilation\PosInfoMoq2012.md = docs\Compilation\PosInfoMoq2012.md docs\Compilation\PosInfoMoq2013.md = docs\Compilation\PosInfoMoq2013.md docs\Compilation\PosInfoMoq2014.md = docs\Compilation\PosInfoMoq2014.md + docs\Compilation\PosInfoMoq2015.md = docs\Compilation\PosInfoMoq2015.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 12ab764..4221644 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ All the rules of this category should not be disabled (or changed their severity | [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. | | [PosInfoMoq2014: The `Callback()` delegate expression must not return a value.](docs/Compilation/PosInfoMoq2014.md) | The `Callback()` delegate expression must not return a value. | +| [PosInfoMoq2014: PosInfoMoq2015: The `Protected().Setup()` method must match the return type of the mocked method](docs/Compilation/PosInfoMoq2015.md) | The method setup with `Protected().Setup()` must match the return type of the mocked method. | diff --git a/docs/Compilation/PosInfoMoq2006.md b/docs/Compilation/PosInfoMoq2006.md index c479384..253ff40 100644 --- a/docs/Compilation/PosInfoMoq2006.md +++ b/docs/Compilation/PosInfoMoq2006.md @@ -24,7 +24,7 @@ and must be overridable (`virtual`, `abstract` and `override`, but not `sealed`) public void Test() { var service = new Mock(); - service.Protected().Setup("GetData") // The GetData() is public and can be mocked with Protected() feature. + service.Protected().Setup("GetData") // The GetData() is public and can't be mocked with Protected() feature. .Returns(10); service.Protected().Setup("NotExists") // The NotExists() method does not exist. .Returns(10); diff --git a/docs/Compilation/PosInfoMoq2015.md b/docs/Compilation/PosInfoMoq2015.md new file mode 100644 index 0000000..26ef987 --- /dev/null +++ b/docs/Compilation/PosInfoMoq2015.md @@ -0,0 +1,50 @@ +# PosInfoMoq2015: The `Protected().Setup()` method must match the return type of the mocked method + +| Property | Value | +|-------------------------------------|----------------------------------------------------------------------------------------------| +| **Rule ID** | PosInfoMoq2015 | +| **Title** | The `Protected().Setup()` method must match the return type of the mocked method. | +| **Category** | Compilation | +| **Default severity** | Error | + +## Cause + +The method setup with `Protected().Setup()` must match the return type of the mocked method. + +## Rule description + +When using the `Protected().Setup()`, the return type of mocked method must match of the generic +argument specified in the `Setup()` method. + +```csharp +[Fact] +public void Test() +{ + var service = new Mock(); + service.Protected().Setup("GetData") // OK. + .Returns(10); + service.Protected().Setup("GetData") // Error: The GetData() return an int, Setup() must be use. + service.Protected().Setup("SendEmail") // Error: The SendEmail() method does not return a value, the `int` generic argument must be remove.0 + .Returns(10); + service.Protected().Setup("GetData") // Error: The GetData() return an int, Setup() must be use. + .Returns("The data"); +} + +public abstract class Service +{ + protected abstract int GetData(); + + protected abstract void SendEmail(); +} +``` + +## How to fix violations + +To fix a violation of this rule, use the generic parameter of the `Setup()` method if the protected mocked +method return a value. Else do not specify a generic parameter for the `Setup()` method of the protected mocked +method does not return a value (`void`). + +## 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 *"Can't set return value for void method xxx."* message. diff --git a/src/Moq.Analyzers/AnalyzerReleases.Shipped.md b/src/Moq.Analyzers/AnalyzerReleases.Shipped.md index 31e25bd..a40b80c 100644 --- a/src/Moq.Analyzers/AnalyzerReleases.Shipped.md +++ b/src/Moq.Analyzers/AnalyzerReleases.Shipped.md @@ -3,7 +3,8 @@ ### New Rules Rule ID | Category | Severity | Notes --------|----------|----------|------- -PosInfoMoq2014 | Compilation | Error | CallBackDelegateMustMatchMockedMethodAnalyzer, [Documentation](https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2014.html)PosInfoMoq2013 | Compilation | Error | The delegate in the argument of the `Returns()`/`ReturnsAsync()` method must have the same parameter types of the mocked method/property. +PosInfoMoq2014 | Compilation | Error | The `Callback()` delegate expression must not return a value. +PosInfoMoq2015 | Compilation | Error | The `Protected().Setup()` method must match the return type of the mocked method. ## Release 1.10.0 diff --git a/src/Moq.Analyzers/Analyzers/SetupProtectedMustBeUsedWithProtectedOrInternalMembersAnalyzer.cs b/src/Moq.Analyzers/Analyzers/SetupProtectedMustBeUsedWithProtectedOrInternalMembersAnalyzer.cs index 7c93bdd..a6424c9 100644 --- a/src/Moq.Analyzers/Analyzers/SetupProtectedMustBeUsedWithProtectedOrInternalMembersAnalyzer.cs +++ b/src/Moq.Analyzers/Analyzers/SetupProtectedMustBeUsedWithProtectedOrInternalMembersAnalyzer.cs @@ -15,7 +15,7 @@ namespace PosInformatique.Moq.Analyzers [DiagnosticAnalyzer(LanguageNames.CSharp)] public class SetupProtectedMustBeUsedWithProtectedOrInternalMembersAnalyzer : DiagnosticAnalyzer { - private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( + private static readonly DiagnosticDescriptor SetupMustBeOnOverridableMethods = new DiagnosticDescriptor( "PosInfoMoq2006", "The Protected().Setup() method must be use with overridable protected or internal methods", "The Protected().Setup() method must be use with overridable protected or internal methods", @@ -25,7 +25,17 @@ public class SetupProtectedMustBeUsedWithProtectedOrInternalMembersAnalyzer : Di description: "The Protected().Setup() method must be use with overridable protected or internal methods.", helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2006.html"); - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + private static readonly DiagnosticDescriptor SetupReturnTypeMustMatch = new DiagnosticDescriptor( + "PosInfoMoq2015", + "The Protected().Setup() method must match the return type of the mocked method", + "The Protected().Setup() method must match the return type of the mocked method", + "Compilation", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "The Protected().Setup() method must match the return type of the mocked method.", + helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2015.html"); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(SetupMustBeOnOverridableMethods, SetupReturnTypeMustMatch); public override void Initialize(AnalysisContext context) { @@ -81,6 +91,8 @@ private static void Analyze(SyntaxNodeAnalysisContext context) } // Check if a method exists with the specified name + IMethodSymbol? methodMatch = null; + foreach (var method in mockedType.GetAllMembers(methodName).OfType()) { if (!method.IsAbstract && !method.IsVirtual && !method.IsOverride) @@ -95,23 +107,78 @@ private static void Analyze(SyntaxNodeAnalysisContext context) if (method.DeclaredAccessibility == Accessibility.Protected) { - return; + methodMatch = method; + break; } if (method.DeclaredAccessibility == Accessibility.Internal) { - return; + methodMatch = method; + break; } if (method.DeclaredAccessibility == Accessibility.ProtectedOrInternal) { - return; + methodMatch = method; + break; + } + } + + if (methodMatch is null) + { + // No method match, raise an error. + context.ReportDiagnostic(SetupMustBeOnOverridableMethods, literalExpression.GetLocation()); + return; + } + + // Else check the argument type of the Setup() method and the method found. + var setupMethodSymbol = context.SemanticModel.GetSymbolInfo(invocationExpression, context.CancellationToken); + + if (setupMethodSymbol.Symbol is not IMethodSymbol setupMethod) + { + return; + } + + if (invocationExpression.Expression is not MemberAccessExpressionSyntax memberAccessExpressionSyntax) + { + return; + } + + if (methodMatch.ReturnsVoid) + { + if (setupMethod.TypeArguments.Length > 0) + { + if (memberAccessExpressionSyntax.Name is not GenericNameSyntax genericNameSyntax) + { + return; + } + + // The method mocked is void and no generic arguments has been specified in the Setup() method. + context.ReportDiagnostic(SetupReturnTypeMustMatch, genericNameSyntax.TypeArgumentList.Arguments[0].GetLocation()); } + + return; + } + + if (setupMethod.TypeArguments.Length != 1) + { + // No generic type has been specified in the Setup(). + context.ReportDiagnostic(SetupReturnTypeMustMatch, memberAccessExpressionSyntax.Name.GetLocation()); + return; } - // No returns method has been specified with Strict mode. Report the diagnostic issue. - var diagnostic = Diagnostic.Create(Rule, literalExpression.GetLocation()); - context.ReportDiagnostic(diagnostic); + if (!SymbolEqualityComparer.Default.Equals(setupMethod.TypeArguments[0], methodMatch.ReturnType)) + { + if (memberAccessExpressionSyntax.Name is not GenericNameSyntax genericNameSyntax) + { + return; + } + + // The method mocked return a type which does not match the argument type of the Setup() method. + context.ReportDiagnostic(SetupReturnTypeMustMatch, genericNameSyntax.TypeArgumentList.Arguments[0].GetLocation()); + + return; + } } } } diff --git a/src/Moq.Analyzers/Moq.Analyzers.csproj b/src/Moq.Analyzers/Moq.Analyzers.csproj index d6fcc5c..8cd3932 100644 --- a/src/Moq.Analyzers/Moq.Analyzers.csproj +++ b/src/Moq.Analyzers/Moq.Analyzers.csproj @@ -20,6 +20,7 @@ 1.10.0 - Add new rules: - PosInfoMoq2014: The delegate in the argument of the Returns() method must return a value with same type of the mocked method. + - PosInfoMoq2015: The Protected().Setup() method must match the return type of the mocked method. 1.10.0 - Add new rules: diff --git a/tests/Moq.Analyzers.Tests/Analyzers/SetupProtectedMustBeUsedWithProtectedOrInternalMembersAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/SetupProtectedMustBeUsedWithProtectedOrInternalMembersAnalyzerTest.cs index 1652c4e..bbd918c 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/SetupProtectedMustBeUsedWithProtectedOrInternalMembersAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/SetupProtectedMustBeUsedWithProtectedOrInternalMembersAnalyzerTest.cs @@ -38,15 +38,16 @@ public void TestMethod() mock1.Protected().Setup(""InternalMethod""); mock1.Protected().Setup(""InternalOverrideMethod""); - mock1.Protected().Setup(""ProtectedVirtualMethod""); - mock1.Protected().Setup(""ProtectedInternalVirtualMethod""); - mock1.Protected().Setup(""InternalVirtualMethod""); - mock1.Protected().Setup(""ProtectedAbstractMethod""); - mock1.Protected().Setup(""ProtectedInternalAbstractMethod""); - mock1.Protected().Setup(""InternalAbstractMethod""); - mock1.Protected().Setup(""ProtectedOverrideMethod""); - mock1.Protected().Setup(""ProtectedInternalOverrideMethod""); - mock1.Protected().Setup(""InternalOverrideMethod""); + mock1.Protected().Setup(""ProtectedVirtualMethodReturnValue""); + mock1.Protected().Setup(""ProtectedInternalVirtualMethodReturnValue""); + mock1.Protected().Setup(""InternalVirtualMethodReturnValue""); + mock1.Protected().Setup(""ProtectedAbstractMethodReturnValue""); + mock1.Protected().Setup(""ProtectedInternalAbstractMethodReturnValue""); + mock1.Protected().Setup(""InternalAbstractMethodReturnValue""); + mock1.Protected().Setup(""ProtectedOverrideMethodReturnValue""); + mock1.Protected().Setup(""ProtectedInternalOverrideMethodReturnValue""); + mock1.Protected().Setup(""InternalOverrideMethodReturnValue""); + mock1.Protected().Setup(""InternalMethodReturnValue""); } } @@ -55,38 +56,64 @@ public abstract class C : BaseClass // Virtual protected virtual void ProtectedVirtualMethod() { } + protected virtual int ProtectedVirtualMethodReturnValue() { return 10;} + protected internal virtual void ProtectedInternalVirtualMethod() { } + protected internal virtual int ProtectedInternalVirtualMethodReturnValue() { return 10; } + internal virtual void InternalVirtualMethod() { } + internal virtual int InternalVirtualMethodReturnValue() { return 10; } + // Abstract protected abstract void ProtectedAbstractMethod(); + protected abstract int ProtectedAbstractMethodReturnValue(); + protected internal abstract void ProtectedInternalAbstractMethod(); + protected internal abstract int ProtectedInternalAbstractMethodReturnValue(); + internal abstract void InternalAbstractMethod(); + internal abstract int InternalAbstractMethodReturnValue(); + // Override protected override void ProtectedOverrideMethod() { } + protected override int ProtectedOverrideMethodReturnValue() { return 10; } + protected internal override void ProtectedInternalOverrideMethod() { } + protected internal override int ProtectedInternalOverrideMethodReturnValue() { return 10; } + internal override void InternalOverrideMethod() { } + + internal override int InternalOverrideMethodReturnValue() { return 10; } } public abstract class BaseClass { protected virtual void ProtectedOverrideMethod() { } + protected virtual int ProtectedOverrideMethodReturnValue() { return 10; } + protected virtual void ProtectedMethod() { } protected internal virtual void ProtectedInternalOverrideMethod() { } + protected internal virtual int ProtectedInternalOverrideMethodReturnValue() { return 10; } + protected internal virtual void ProtectedInternalMethod() { } internal virtual void InternalOverrideMethod() { } + internal virtual int InternalOverrideMethodReturnValue() { return 10; } + internal virtual void InternalMethod() { } + + internal virtual int InternalMethodReturnValue() { return 10; } } }"; @@ -108,8 +135,8 @@ public class TestClass public void TestMethod() { var mock1 = new Mock(); - mock1.Protected().Setup([|""TestMethodPublic""|]); - mock1.Protected().Setup([|""TestMethodPublic""|]); + mock1.Protected().Setup({|PosInfoMoq2006:""TestMethodPublic""|}); + mock1.Protected().Setup({|PosInfoMoq2006:""TestMethodPublic""|}); } } @@ -137,8 +164,8 @@ public class TestClass public void TestMethod() { var mock1 = new Mock(); - mock1.Protected().Setup([|""TestMethod""|]); - mock1.Protected().Setup([|""TestMethod""|]); + mock1.Protected().Setup({|PosInfoMoq2006:""TestMethod""|}); + mock1.Protected().Setup({|PosInfoMoq2006:""TestMethod""|}); } } @@ -171,8 +198,8 @@ public class TestClass public void TestMethod() { var mock1 = new Mock(); - mock1.Protected().Setup([|""TestMethodPublic""|]); - mock1.Protected().Setup([|""TestMethodPublic""|]); + mock1.Protected().Setup({|PosInfoMoq2006:""TestMethodPublic""|}); + mock1.Protected().Setup({|PosInfoMoq2006:""TestMethodPublic""|}); } } @@ -200,13 +227,13 @@ public class TestClass public void TestMethod() { var mock1 = new Mock(); - mock1.Protected().Setup([|""NonOverridableProtectedMethod""|]); - mock1.Protected().Setup([|""NonOverridableProtectedInternalMethod""|]); - mock1.Protected().Setup([|""NonOverridableInternalMethod""|]); + mock1.Protected().Setup({|PosInfoMoq2006:""NonOverridableProtectedMethod""|}); + mock1.Protected().Setup({|PosInfoMoq2006:""NonOverridableProtectedInternalMethod""|}); + mock1.Protected().Setup({|PosInfoMoq2006:""NonOverridableInternalMethod""|}); - mock1.Protected().Setup([|""NonOverridableProtectedMethod""|]); - mock1.Protected().Setup([|""NonOverridableProtectedInternalMethod""|]); - mock1.Protected().Setup([|""NonOverridableInternalMethod""|]); + mock1.Protected().Setup({|PosInfoMoq2006:""NonOverridableProtectedMethod""|}); + mock1.Protected().Setup({|PosInfoMoq2006:""NonOverridableProtectedInternalMethod""|}); + mock1.Protected().Setup({|PosInfoMoq2006:""NonOverridableInternalMethod""|}); } } @@ -221,6 +248,38 @@ internal void NonOverridableInternalMethod() { } await Verifier.VerifyAnalyzerAsync(source); } + [Fact] + public async Task ReturnMethodTypeNotMatching_DiagnosticReported() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + using Moq.Protected; + using System; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(); + mock1.Protected().Setup<{|PosInfoMoq2015:long|}>(""TestMethodProtectedReturnValue""); + mock1.Protected().Setup<{|PosInfoMoq2015:long|}>(""TestMethodProtected""); + mock1.Protected().{|PosInfoMoq2015:Setup|}(""TestMethodProtectedReturnValue""); + } + } + + public class C + { + protected virtual int TestMethodProtectedReturnValue() { return 0; } + + protected virtual void TestMethodProtected() { } + } + }"; + + await Verifier.VerifyAnalyzerAsync(source); + } + [Fact] public async Task NoMoqLibrary() { From cd59a08d6772f0e145f00d974ca7a75e389e24ad Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 23 Oct 2024 11:57:06 +0200 Subject: [PATCH 04/15] Fix the PosInfoMoq1001 rule to support Mock instantiation with lambda expression argument (fixes #39). --- ...kInstanceShouldBeStrictBehaviorAnalyzer.cs | 4 +- .../SetBehaviorToStrictCodeFixProvider.cs | 35 ++++++-- src/Moq.Analyzers/Moq.Analyzers.csproj | 1 + src/Moq.Analyzers/MoqExpressionAnalyzer.cs | 88 +++++++++---------- src/Moq.Analyzers/MoqSymbols.cs | 19 ++++ ...tanceShouldBeStrictBehaviorAnalyzerTest.cs | 61 +++++++++++++ .../SetBehaviorToStrictCodeFixProviderTest.cs | 82 +++++------------ 7 files changed, 176 insertions(+), 114 deletions(-) diff --git a/src/Moq.Analyzers/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzer.cs b/src/Moq.Analyzers/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzer.cs index bc39864..f0d14a5 100644 --- a/src/Moq.Analyzers/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzer.cs +++ b/src/Moq.Analyzers/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzer.cs @@ -57,7 +57,7 @@ private static void AnalyzeObjectCreationExpression(SyntaxNodeAnalysisContext co return; } - if (!moqExpressionAnalyzer.IsMockCreationStrictBehavior(objectCreation.ArgumentList, context.CancellationToken)) + if (!moqExpressionAnalyzer.IsMockCreationStrictBehavior(objectCreation, context.CancellationToken)) { var diagnostic = Diagnostic.Create(Rule, objectCreation.GetLocation()); context.ReportDiagnostic(diagnostic); @@ -86,7 +86,7 @@ private static void AnalyzeInvocationExpression(SyntaxNodeAnalysisContext contex // Extract the type var moqExpressionAnalyzer = new MoqExpressionAnalyzer(moqSymbols, context.SemanticModel); - if (!moqExpressionAnalyzer.IsMockCreationStrictBehavior(invocationExpression.ArgumentList, context.CancellationToken, false)) + if (!moqExpressionAnalyzer.IsMockOfStrictBehavior(invocationExpression.ArgumentList, context.CancellationToken)) { var diagnostic = Diagnostic.Create(Rule, invocationExpression.GetLocation()); context.ReportDiagnostic(diagnostic); diff --git a/src/Moq.Analyzers/CodeFixes/SetBehaviorToStrictCodeFixProvider.cs b/src/Moq.Analyzers/CodeFixes/SetBehaviorToStrictCodeFixProvider.cs index a54aabd..3bfe6a1 100644 --- a/src/Moq.Analyzers/CodeFixes/SetBehaviorToStrictCodeFixProvider.cs +++ b/src/Moq.Analyzers/CodeFixes/SetBehaviorToStrictCodeFixProvider.cs @@ -86,16 +86,24 @@ private static async Task AddMockBehiavorStrictArgumentAsync(Document { var firstArgument = oldMockCreationExpression.ArgumentList.Arguments.First(); - if (IsMockBehaviorArgument(firstArgument)) + if (IsLambdaExpressionArgument(firstArgument)) { - // The old first argument is MockBehavior.xxxxx, so we take the following arguments - // and ignore it. - arguments.AddRange(oldMockCreationExpression.ArgumentList.Arguments.Skip(1)); + // The old first argument is lambda expression, so we insert it at the first argument position. + arguments.Insert(0, firstArgument); } else { - // Retrieves all the arguments of the "new Mock(...)" instantiation. - arguments.AddRange(oldMockCreationExpression.ArgumentList.Arguments); + 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); + } } } @@ -179,5 +187,20 @@ private static bool IsMockBehaviorArgument(ArgumentSyntax argument) return true; } + + private static bool IsLambdaExpressionArgument(ArgumentSyntax argument) + { + if (argument.Expression is not ParenthesizedLambdaExpressionSyntax lambdaExpression) + { + return false; + } + + if (lambdaExpression.ParameterList.Parameters.Count > 0) + { + return false; + } + + return true; + } } } diff --git a/src/Moq.Analyzers/Moq.Analyzers.csproj b/src/Moq.Analyzers/Moq.Analyzers.csproj index 8cd3932..0d0607d 100644 --- a/src/Moq.Analyzers/Moq.Analyzers.csproj +++ b/src/Moq.Analyzers/Moq.Analyzers.csproj @@ -21,6 +21,7 @@ - Add new rules: - PosInfoMoq2014: The delegate in the argument of the Returns() method must return a value with same type of the mocked method. - PosInfoMoq2015: The Protected().Setup() method must match the return type of the mocked method. + - Fix the PosInfoMoq1001 rule for the Mock<T> instantiation with the lambad expression to create mock object. 1.10.0 - Add new rules: diff --git a/src/Moq.Analyzers/MoqExpressionAnalyzer.cs b/src/Moq.Analyzers/MoqExpressionAnalyzer.cs index f179432..c588345 100644 --- a/src/Moq.Analyzers/MoqExpressionAnalyzer.cs +++ b/src/Moq.Analyzers/MoqExpressionAnalyzer.cs @@ -195,79 +195,55 @@ public bool IsMockSetupMethodProtected(InvocationExpressionSyntax invocationExpr return true; } - public bool IsMockCreationStrictBehavior(ArgumentListSyntax? argumentList, CancellationToken cancellationToken, bool firstOrLast = true) + public bool IsMockCreationStrictBehavior(ObjectCreationExpressionSyntax mockCreation, CancellationToken cancellationToken) { // Check that the "new Mock()" statement have at least one argument (else Strict is missing...). - if (argumentList is null) + if (mockCreation.ArgumentList is null) { return false; } - ArgumentSyntax? firstOrLastArgument; - - if (firstOrLast) - { - firstOrLastArgument = argumentList.Arguments.FirstOrDefault(); - } - else - { - firstOrLastArgument = argumentList.Arguments.LastOrDefault(); - } - - if (firstOrLastArgument is null) - { - return false; - } + var argument = mockCreation.ArgumentList.Arguments.FirstOrDefault(); - // Gets the first argument of "new Mock(...)" and ensures it is a MemberAccessExpressionSyntax - // (because we searching for MockBehavior.Strict). - if (!this.IsStrictBehaviorArgument(firstOrLastArgument, out var memberAccessExpression, cancellationToken)) + if (argument is null) { return false; } - // Check that the memberAccessExpression.Name reference the Strict field - var firstOrLastArgumentField = this.semanticModel.GetSymbolInfo(memberAccessExpression!.Name, cancellationToken); + // Check if the mock creation have factory parameter + var constructorSymbol = this.semanticModel.GetSymbolInfo(mockCreation, cancellationToken); - if (!this.moqSymbols.IsMockBehaviorStrictField(firstOrLastArgumentField.Symbol)) + if (this.moqSymbols.IsMockConstructorWithFactory(constructorSymbol.Symbol)) { - return false; + // The first argument is a lambda expression "Mock(Expression>)" to create + // instance of the mock, so check the second argument. + if (mockCreation.ArgumentList.Arguments.Count < 2) + { + return false; + } + + argument = mockCreation.ArgumentList.Arguments[1]; } - return true; + return this.IsStrictBehaviorArgument(argument, cancellationToken); } - public bool IsMockOfStrictBehavior(InvocationExpressionSyntax mockOfExpression, CancellationToken cancellationToken) + public bool IsMockOfStrictBehavior(ArgumentListSyntax? argumentList, CancellationToken cancellationToken) { - // Check that the "new Mock()" statement have at least one argument (else Strict is missing...). - if (mockOfExpression.ArgumentList is null) - { - return false; - } - - var firstArgument = mockOfExpression.ArgumentList.Arguments.FirstOrDefault(); - - if (firstArgument is null) + // Check that the "Mock.Of()" statement have at least one argument (else Strict is missing...). + if (argumentList is null) { return false; } - // Gets the first argument of "new Mock(...)" and ensures it is a MemberAccessExpressionSyntax - // (because we searching for MockBehavior.Strict). - if (!this.IsStrictBehaviorArgument(firstArgument, out var memberAccessExpression, cancellationToken)) - { - return false; - } + var lastArgument = argumentList.Arguments.LastOrDefault(); - // Check that the memberAccessExpression.Name reference the Strict field - var firstArgumentField = this.semanticModel.GetSymbolInfo(memberAccessExpression!.Name, cancellationToken); - - if (!this.moqSymbols.IsMockBehaviorStrictField(firstArgumentField.Symbol)) + if (lastArgument is null) { return false; } - return true; + return this.IsStrictBehaviorArgument(lastArgument, cancellationToken); } public bool IsStrictBehaviorArgument(ArgumentSyntax argument, out MemberAccessExpressionSyntax? memberAccessExpression, CancellationToken cancellationToken) @@ -303,7 +279,7 @@ public bool IsStrictBehavior(IdentifierNameSyntax localVariableExpression, Cance if (mockCreation is not null) { - if (this.IsMockCreationStrictBehavior(mockCreation.ArgumentList, cancellationToken)) + if (this.IsMockCreationStrictBehavior(mockCreation, cancellationToken)) { return true; } @@ -560,5 +536,23 @@ private bool IsMockSetupMethod(InvocationExpressionSyntax invocationExpression, return true; } + + private bool IsStrictBehaviorArgument(ArgumentSyntax argument, CancellationToken cancellationToken) + { + if (!this.IsStrictBehaviorArgument(argument, out var memberAccessExpression, cancellationToken)) + { + return false; + } + + // Check that the memberAccessExpression.Name reference the Strict field + var lastArgumentField = this.semanticModel.GetSymbolInfo(memberAccessExpression!.Name, cancellationToken); + + if (!this.moqSymbols.IsMockBehaviorStrictField(lastArgumentField.Symbol)) + { + return false; + } + + return true; + } } } diff --git a/src/Moq.Analyzers/MoqSymbols.cs b/src/Moq.Analyzers/MoqSymbols.cs index 315e399..471c093 100644 --- a/src/Moq.Analyzers/MoqSymbols.cs +++ b/src/Moq.Analyzers/MoqSymbols.cs @@ -41,6 +41,8 @@ internal sealed class MoqSymbols private readonly Lazy verifiesInterface; + private readonly Lazy mockConstructorWithFactory; + private MoqSymbols(INamedTypeSymbol mockGenericClass, Compilation compilation) { this.mockGenericClass = mockGenericClass; @@ -64,6 +66,8 @@ private MoqSymbols(INamedTypeSymbol mockGenericClass, Compilation compilation) this.verifiableMethods = new Lazy>(() => this.verifiesInterface.Value.GetMembers("Verifiable").OfType().ToArray()); this.mockOfMethods = new Lazy>(() => mockGenericClass.BaseType!.GetMembers("Of").Where(m => m.IsStatic).OfType().ToArray()); + + this.mockConstructorWithFactory = new Lazy(() => mockGenericClass.Constructors.Single(c => c.Parameters.Length == 2 && c.Parameters[0].Type.Name == "Expression")); } public static MoqSymbols? FromCompilation(Compilation compilation) @@ -396,6 +400,21 @@ public bool IsMockOfMethod(ISymbol? symbol) return false; } + public bool IsMockConstructorWithFactory(ISymbol? symbol) + { + if (symbol is null) + { + return false; + } + + if (!SymbolEqualityComparer.Default.Equals(symbol.OriginalDefinition, this.mockConstructorWithFactory.Value)) + { + return false; + } + + return true; + } + public bool IsAsMethod(IMethodSymbol method) { if (!SymbolEqualityComparer.Default.Equals(method.OriginalDefinition, this.asMethod.Value)) diff --git a/tests/Moq.Analyzers.Tests/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzerTest.cs index 41b793d..5195069 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/MockInstanceShouldBeStrictBehaviorAnalyzerTest.cs @@ -54,6 +54,37 @@ public interface I await Verifier.VerifyAnalyzerAsync(source); } + [Fact] + public async Task NewMock_BehaviorStrict_WithFactoryLambdaArgument() + { + var source = @" + namespace ConsoleApplication1 + { + using System; + using System.Linq.Expressions; + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(() => new C(), MockBehavior.Strict); + var mock2 = new Mock((Expression>)null, MockBehavior.Strict); + } + } + + public interface I + { + } + + public class C : I + { + } + }"; + + await Verifier.VerifyAnalyzerAsync(source); + } + [Theory] [InlineData("Loose", "")] [InlineData("Default", "")] @@ -82,6 +113,36 @@ public interface I await Verifier.VerifyAnalyzerAsync(source); } + [Theory] + [InlineData("Loose")] + [InlineData("Default")] + public async Task NewMock_BehaviorDifferentOfStrict_WithFactoryLambdaArgument(string mode) + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = [|new Mock(() => new C(), MockBehavior." + mode + @")|]; + } + } + + public interface I + { + } + + public class C : I + { + } + }"; + + await Verifier.VerifyAnalyzerAsync(source); + } + [Fact] public async Task NewMock_NoBehavior() { diff --git a/tests/Moq.Analyzers.Tests/CodeFixes/SetBehaviorToStrictCodeFixProviderTest.cs b/tests/Moq.Analyzers.Tests/CodeFixes/SetBehaviorToStrictCodeFixProviderTest.cs index 38bedaa..8e0ac44 100644 --- a/tests/Moq.Analyzers.Tests/CodeFixes/SetBehaviorToStrictCodeFixProviderTest.cs +++ b/tests/Moq.Analyzers.Tests/CodeFixes/SetBehaviorToStrictCodeFixProviderTest.cs @@ -10,8 +10,11 @@ namespace PosInformatique.Moq.Analyzers.Tests public class SetBehaviorToStrictCodeFixProviderTest { - [Fact] - public async Task NewMock_Fix_Loose() + [Theory] + [InlineData("")] + [InlineData("MockBehavior.Loose")] + [InlineData("MockBehavior.Default")] + public async Task NewMock_Fix(string behavior) { var source = @" namespace ConsoleApplication1 @@ -22,7 +25,7 @@ public class TestClass { public void TestMethod() { - var mock = [|new Mock(MockBehavior.Loose)|]; + var mock = [|new Mock(" + behavior + @")|]; } } @@ -53,8 +56,11 @@ public interface I await Verifier.VerifyCodeFixAsync(source, expectedFixedSource); } - [Fact] - public async Task NewMock_Fix_MissingBehavior() + [Theory] + [InlineData("")] + [InlineData(", MockBehavior.Loose")] + [InlineData(", MockBehavior.Default")] + public async Task NewMock_Fix_WithLambdaExpression(string behavior) { var source = @" namespace ConsoleApplication1 @@ -65,12 +71,11 @@ public class TestClass { public void TestMethod() { - var mock1 = [|new Mock()|]; - var mock2 = [|new Mock { }|]; + var mock = [|new Mock(() => new C()" + behavior + @")|]; } } - public interface I + public class C { } }"; @@ -85,12 +90,11 @@ public class TestClass { public void TestMethod() { - var mock1 = new Mock(MockBehavior.Strict); - var mock2 = new Mock(MockBehavior.Strict) { }; + var mock = new Mock(() => new C(), MockBehavior.Strict); } } - public interface I + public class C { } }"; @@ -104,7 +108,7 @@ public interface I [InlineData("MockBehavior.Default, 1, 2, 3", "MockBehavior.Strict, 1, 2, 3")] [InlineData("OtherEnum.A, 1, 2, 3", "MockBehavior.Strict, OtherEnum.A, 1, 2, 3")] [InlineData("int.MaxValue, 1, 2, 3", "MockBehavior.Strict, int.MaxValue, 1, 2, 3")] - public async Task NewMock_Fix_MissingBehavior_WithArguments(string arguments, string expectedArguments) + public async Task NewMock_Fix_WithArguments(string arguments, string expectedArguments) { var source = @" namespace ConsoleApplication1 @@ -150,8 +154,11 @@ public enum OtherEnum { A } await Verifier.VerifyCodeFixAsync(source, expectedFixedSource); } - [Fact] - public async Task MockOf_Fix_Loose() + [Theory] + [InlineData("")] + [InlineData("MockBehavior.Loose")] + [InlineData("MockBehavior.Default")] + public async Task MockOf_Fix(string behavior) { var source = @" namespace ConsoleApplication1 @@ -162,7 +169,7 @@ public class TestClass { public void TestMethod() { - var mock = [|Mock.Of(MockBehavior.Loose)|]; + var mock = [|Mock.Of(" + behavior + @")|]; } } @@ -193,56 +200,13 @@ public interface I await Verifier.VerifyCodeFixAsync(source, expectedFixedSource); } - [Fact] - public async Task MockOf_Fix_MissingBehavior() - { - var source = @" - namespace ConsoleApplication1 - { - using Moq; - - public class TestClass - { - public void TestMethod() - { - var mock1 = [|Mock.Of()|]; - } - } - - public interface I - { - } - }"; - - var expectedFixedSource = - @" - namespace ConsoleApplication1 - { - using Moq; - - public class TestClass - { - public void TestMethod() - { - var mock1 = Mock.Of(MockBehavior.Strict); - } - } - - public interface I - { - } - }"; - - await Verifier.VerifyCodeFixAsync(source, expectedFixedSource); - } - [Theory] [InlineData("", "MockBehavior.Strict")] [InlineData("MockBehavior.Loose", "MockBehavior.Strict")] [InlineData("i => true", "i => true, MockBehavior.Strict")] [InlineData("i => true, MockBehavior.Default", "i => true, MockBehavior.Strict")] [InlineData("i => true, MockBehavior.Loose", "i => true, MockBehavior.Strict")] - public async Task MockOf_Fix_MissingBehavior_WithArguments(string arguments, string expectedArguments) + public async Task MockOf_Fix_WithArguments(string arguments, string expectedArguments) { var source = @" namespace ConsoleApplication1 From bd80329ac7ea346fdd8434b7d45f780bd984b472 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 23 Oct 2024 12:10:25 +0200 Subject: [PATCH 05/15] Fix the PosInfoMoq2004 to not check the Mock instantiation with the factory lambda expression (fixes: #39). --- ...umentCannotBePassedForInterfaceAnalyzer.cs | 12 ++++++-- src/Moq.Analyzers/Moq.Analyzers.csproj | 3 +- ...tCannotBePassedForInterfaceAnalyzerTest.cs | 30 +++++++++++++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/Moq.Analyzers/Analyzers/ConstructorArgumentCannotBePassedForInterfaceAnalyzer.cs b/src/Moq.Analyzers/Analyzers/ConstructorArgumentCannotBePassedForInterfaceAnalyzer.cs index 8a31e27..b983433 100644 --- a/src/Moq.Analyzers/Analyzers/ConstructorArgumentCannotBePassedForInterfaceAnalyzer.cs +++ b/src/Moq.Analyzers/Analyzers/ConstructorArgumentCannotBePassedForInterfaceAnalyzer.cs @@ -46,6 +46,15 @@ private static void Analyze(SyntaxNodeAnalysisContext context) return; } + // Check if the mock instantiation is with a factory. + // In this case, we ignore the matching of the constructor arguments. + var constructorSymbol = context.SemanticModel.GetSymbolInfo(objectCreation, context.CancellationToken); + + if (moqSymbols.IsMockConstructorWithFactory(constructorSymbol.Symbol)) + { + return; + } + var moqExpressionAnalyzer = new MoqExpressionAnalyzer(moqSymbols, context.SemanticModel); // Check there is "new Mock()" statement. @@ -90,8 +99,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context) .Select(a => a.Expression.GetLocation()) .ToArray(); - var diagnostic = Diagnostic.Create(Rule, locations[0], locations.Skip(1)); - context.ReportDiagnostic(diagnostic); + context.ReportDiagnostic(Rule, locations); return; } diff --git a/src/Moq.Analyzers/Moq.Analyzers.csproj b/src/Moq.Analyzers/Moq.Analyzers.csproj index 0d0607d..a020d58 100644 --- a/src/Moq.Analyzers/Moq.Analyzers.csproj +++ b/src/Moq.Analyzers/Moq.Analyzers.csproj @@ -21,7 +21,8 @@ - Add new rules: - PosInfoMoq2014: The delegate in the argument of the Returns() method must return a value with same type of the mocked method. - PosInfoMoq2015: The Protected().Setup() method must match the return type of the mocked method. - - Fix the PosInfoMoq1001 rule for the Mock<T> instantiation with the lambad expression to create mock object. + - Fix the PosInfoMoq1001 rule for the Mock<T> instantiation with the lambda expression to create mock instances. + - Fix the PosInfoMoq2004 rule for the Mock<T> instantiation with the lambda expression to create mock instances. 1.10.0 - Add new rules: diff --git a/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentCannotBePassedForInterfaceAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentCannotBePassedForInterfaceAnalyzerTest.cs index 40dc526..6f48b13 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentCannotBePassedForInterfaceAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentCannotBePassedForInterfaceAnalyzerTest.cs @@ -113,6 +113,36 @@ await Verifier.VerifyAnalyzerAsync( .WithLocation(1).WithArguments("2")); } + [Theory] + [InlineData("")] + [InlineData(", MockBehavior.Strict")] + public async Task Interface_WithFactoryLambdaExpression(string behavior) + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(() => new C()" + behavior + @"); + } + } + + public interface I + { + } + + public class C : I + { + } + }"; + + await Verifier.VerifyAnalyzerAsync(source); + } + [Fact] public async Task Class() { From 97f6686edcae0be427dc863954da26cf4a4d4d1a Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 23 Oct 2024 12:46:44 +0200 Subject: [PATCH 06/15] Add the PosInfoMoq2016 rule to check the lambda expression factory are used only with class types (fixes: #40). --- PosInformatique.Moq.Analyzers.sln | 1 + README.md | 3 +- docs/Compilation/PosInfoMoq2016.md | 45 +++++++++++ src/Moq.Analyzers/AnalyzerReleases.Shipped.md | 2 +- .../ConstructorArgumentsMustMatchAnalyzer.cs | 32 +++++++- src/Moq.Analyzers/Moq.Analyzers.csproj | 3 +- ...nstructorArgumentsMustMatchAnalyzerTest.cs | 78 +++++++++++++++++++ 7 files changed, 159 insertions(+), 5 deletions(-) create mode 100644 docs/Compilation/PosInfoMoq2016.md diff --git a/PosInformatique.Moq.Analyzers.sln b/PosInformatique.Moq.Analyzers.sln index 5bf8b7a..ceee7c1 100644 --- a/PosInformatique.Moq.Analyzers.sln +++ b/PosInformatique.Moq.Analyzers.sln @@ -53,6 +53,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Compilation", "Compilation" docs\Compilation\PosInfoMoq2013.md = docs\Compilation\PosInfoMoq2013.md docs\Compilation\PosInfoMoq2014.md = docs\Compilation\PosInfoMoq2014.md docs\Compilation\PosInfoMoq2015.md = docs\Compilation\PosInfoMoq2015.md + docs\Compilation\PosInfoMoq2016.md = docs\Compilation\PosInfoMoq2016.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 4221644..a104dc6 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,8 @@ All the rules of this category should not be disabled (or changed their severity | [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. | | [PosInfoMoq2014: The `Callback()` delegate expression must not return a value.](docs/Compilation/PosInfoMoq2014.md) | The `Callback()` delegate expression must not return a value. | -| [PosInfoMoq2014: PosInfoMoq2015: The `Protected().Setup()` method must match the return type of the mocked method](docs/Compilation/PosInfoMoq2015.md) | The method setup with `Protected().Setup()` must match the return type of the mocked method. | +| [PosInfoMoq2015: The `Protected().Setup()` method must match the return type of the mocked method](docs/Compilation/PosInfoMoq2015.md) | The method setup with `Protected().Setup()` must match the return type of the mocked method. | +| [PosInfoMoq2016: `Mock` constructor with factory lambda expression can be used only with classes.](docs/Compilation/PosInfoMoq2016.md) | The factory lambda expression used in `Mock` instantiation must used only for the classes. | diff --git a/docs/Compilation/PosInfoMoq2016.md b/docs/Compilation/PosInfoMoq2016.md new file mode 100644 index 0000000..50a63ea --- /dev/null +++ b/docs/Compilation/PosInfoMoq2016.md @@ -0,0 +1,45 @@ +# PosInfoMoq2016: `Mock` constructor with factory lambda expression can be used only with classes. + +| Property | Value | +|-------------------------------------|-------------------------------------------------------------------------| +| **Rule ID** | PosInfoMoq2016 | +| **Title** | `Mock` constructor with factory lambda expression can be used only with classes. | +| **Category** | Compilation | +| **Default severity** | Error | + +## Cause + +The factory lambda expression used in `Mock` instantiation must used only for the classes. + +## Rule description + +When using a lambda expression in the constructor of Mock to create a mock instance, the mocked type must be a class. + +```csharp +[Fact] +public void Test() +{ + var service1 = new Mock(() => new Service()); // The factory lambda expression can be used only on classes type. + var service2 = new Mock(() => new Service()); // OK +} + +public interface IService +{ +} + +public class Service : IService: +{ + public Service(string a) + { + } +} +``` + +## How to fix violations + +To fix a violation of this rule, ensure that the lambda expression factory is used with a mocked type that is a class. + +## 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 *"Constructor arguments cannot be passed for interface mocks."* message. diff --git a/src/Moq.Analyzers/AnalyzerReleases.Shipped.md b/src/Moq.Analyzers/AnalyzerReleases.Shipped.md index a40b80c..04270ce 100644 --- a/src/Moq.Analyzers/AnalyzerReleases.Shipped.md +++ b/src/Moq.Analyzers/AnalyzerReleases.Shipped.md @@ -5,7 +5,7 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- PosInfoMoq2014 | Compilation | Error | The `Callback()` delegate expression must not return a value. PosInfoMoq2015 | Compilation | Error | The `Protected().Setup()` method must match the return type of the mocked method. - +PosInfoMoq2016 | Compilation | Error | `Mock` constructor with factory lambda expression can be used only with classes. ## Release 1.10.0 ### New Rules diff --git a/src/Moq.Analyzers/Analyzers/ConstructorArgumentsMustMatchAnalyzer.cs b/src/Moq.Analyzers/Analyzers/ConstructorArgumentsMustMatchAnalyzer.cs index 45d4bb7..39b6685 100644 --- a/src/Moq.Analyzers/Analyzers/ConstructorArgumentsMustMatchAnalyzer.cs +++ b/src/Moq.Analyzers/Analyzers/ConstructorArgumentsMustMatchAnalyzer.cs @@ -36,7 +36,20 @@ public class ConstructorArgumentsMustMatchAnalyzer : DiagnosticAnalyzer description: "Constructor of the mocked class must be accessible.", helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2011.html"); - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(ConstructorArgumentsMustMatchMockedClassRule, ConstructorMockedClassMustBeAccessibleRule); + private static readonly DiagnosticDescriptor ConstructorWithLambdaExpressionCanBeUseWithClassesOnlyRule = new DiagnosticDescriptor( + "PosInfoMoq2016", + "Mock constructor with factory lambda expression can be used only with classes", + "Mock constructor with factory lambda expression can be used only with classes", + "Compilation", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Mock constructor with factory lambda expression can be used only with classes.", + helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2016.html"); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( + ConstructorArgumentsMustMatchMockedClassRule, + ConstructorMockedClassMustBeAccessibleRule, + ConstructorWithLambdaExpressionCanBeUseWithClassesOnlyRule); public override void Initialize(AnalysisContext context) { @@ -60,12 +73,27 @@ private static void Analyze(SyntaxNodeAnalysisContext context) var moqExpressionAnalyzer = new MoqExpressionAnalyzer(moqSymbols, context.SemanticModel); // Check there is "new Mock()" statement. - var mockedType = moqExpressionAnalyzer.GetMockedType(objectCreation, out var _, context.CancellationToken); + var mockedType = moqExpressionAnalyzer.GetMockedType(objectCreation, out var typeExpression, context.CancellationToken); if (mockedType is null) { return; } + // Check if the mock instantiation is with a factory. + var constructorSymbol = context.SemanticModel.GetSymbolInfo(objectCreation, context.CancellationToken); + + if (moqSymbols.IsMockConstructorWithFactory(constructorSymbol.Symbol)) + { + // In this case, we ignore the matching of the constructor arguments. + // But we check it is an interface (else it is not supported). + if (mockedType.TypeKind != TypeKind.Class) + { + context.ReportDiagnostic(ConstructorWithLambdaExpressionCanBeUseWithClassesOnlyRule, typeExpression!.GetLocation()); + } + + return; + } + // Check the type is mockable if (!moqSymbols.IsMockable(mockedType)) { diff --git a/src/Moq.Analyzers/Moq.Analyzers.csproj b/src/Moq.Analyzers/Moq.Analyzers.csproj index a020d58..c9a70cd 100644 --- a/src/Moq.Analyzers/Moq.Analyzers.csproj +++ b/src/Moq.Analyzers/Moq.Analyzers.csproj @@ -17,10 +17,11 @@ https://github.com/PosInformatique/PosInformatique.Moq.Analyzers README.md - 1.10.0 + 1.11.0 - Add new rules: - PosInfoMoq2014: The delegate in the argument of the Returns() method must return a value with same type of the mocked method. - PosInfoMoq2015: The Protected().Setup() method must match the return type of the mocked method. + - PosInfoMoq2016: Mock<T> constructor with factory lambda expression can be used only with classes. - Fix the PosInfoMoq1001 rule for the Mock<T> instantiation with the lambda expression to create mock instances. - Fix the PosInfoMoq2004 rule for the Mock<T> instantiation with the lambda expression to create mock instances. diff --git a/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentsMustMatchAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentsMustMatchAnalyzerTest.cs index b053a46..1ea68ea 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentsMustMatchAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentsMustMatchAnalyzerTest.cs @@ -523,6 +523,84 @@ public C(int a, object c) await Verifier.VerifyAnalyzerAsync(source); } + [Theory] + [InlineData("")] + [InlineData(", MockBehavior.Strict")] + public async Task Arguments_WithFactory(string behavior) + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(() => new C(10)" + behavior + @"); + } + } + + public class C + { + public C(int a) + { + } + + public C(int a, string b) + { + } + + public C(int a, object c) + { + } + } + }"; + + await Verifier.VerifyAnalyzerAsync(source); + } + + [Theory] + [InlineData("")] + [InlineData(", MockBehavior.Strict")] + public async Task Arguments_WithFactory_NotClass(string behavior) + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock<{|PosInfoMoq2016:I|}>(() => new C(10)" + behavior + @"); + } + } + + public interface I + { + } + + public class C : I + { + public C(int a) + { + } + + public C(int a, string b) + { + } + + public C(int a, object c) + { + } + } + }"; + + await Verifier.VerifyAnalyzerAsync(source); + } + [Fact] public async Task NoMoqLibrary() { From d7e963989ec63a2f25901c9103e9b780f6c9888c Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 23 Oct 2024 12:58:03 +0200 Subject: [PATCH 07/15] Merge the PosInfoMoq2004 rule to the ConstructorArgumentsMustMatchAnalyzer analyzer. --- ...umentCannotBePassedForInterfaceAnalyzer.cs | 108 ---------- .../ConstructorArgumentsMustMatchAnalyzer.cs | 32 ++- ...tCannotBePassedForInterfaceAnalyzerTest.cs | 204 ------------------ ...nstructorArgumentsMustMatchAnalyzerTest.cs | 103 +++++++++ 4 files changed, 129 insertions(+), 318 deletions(-) delete mode 100644 src/Moq.Analyzers/Analyzers/ConstructorArgumentCannotBePassedForInterfaceAnalyzer.cs delete mode 100644 tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentCannotBePassedForInterfaceAnalyzerTest.cs diff --git a/src/Moq.Analyzers/Analyzers/ConstructorArgumentCannotBePassedForInterfaceAnalyzer.cs b/src/Moq.Analyzers/Analyzers/ConstructorArgumentCannotBePassedForInterfaceAnalyzer.cs deleted file mode 100644 index b983433..0000000 --- a/src/Moq.Analyzers/Analyzers/ConstructorArgumentCannotBePassedForInterfaceAnalyzer.cs +++ /dev/null @@ -1,108 +0,0 @@ -//----------------------------------------------------------------------- -// -// 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 ConstructorArgumentCannotBePassedForInterfaceAnalyzer : DiagnosticAnalyzer - { - internal static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( - "PosInfoMoq2004", - "Constructor arguments cannot be passed for interface mocks", - "Constructor arguments cannot be passed for interface mocks", - "Compilation", - DiagnosticSeverity.Error, - isEnabledByDefault: true, - description: "Constructor arguments cannot be passed for interface mocks.", - helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2004.html"); - - 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; - - var moqSymbols = MoqSymbols.FromCompilation(context.Compilation); - - if (moqSymbols is null) - { - return; - } - - // Check if the mock instantiation is with a factory. - // In this case, we ignore the matching of the constructor arguments. - var constructorSymbol = context.SemanticModel.GetSymbolInfo(objectCreation, context.CancellationToken); - - if (moqSymbols.IsMockConstructorWithFactory(constructorSymbol.Symbol)) - { - return; - } - - var moqExpressionAnalyzer = new MoqExpressionAnalyzer(moqSymbols, context.SemanticModel); - - // Check there is "new Mock()" statement. - var mockedType = moqExpressionAnalyzer.GetMockedType(objectCreation, out var typeExpression, context.CancellationToken); - if (mockedType is null) - { - return; - } - - // Check the mocked type is an interface - if (mockedType.TypeKind != TypeKind.Interface) - { - return; - } - - // Check that the "new Mock()" statement have at least one argument (else skip analysis). - if (objectCreation.ArgumentList is null) - { - return; - } - - // Gets the first argument (skip analysis if no argument) - var firstArgument = objectCreation.ArgumentList.Arguments.FirstOrDefault(); - - if (firstArgument is null) - { - return; - } - - // Check if the first argument is MockBehavior argument - var argumentCheckStart = 0; - - if (moqExpressionAnalyzer.IsStrictBehaviorArgument(firstArgument, out var _, context.CancellationToken)) - { - argumentCheckStart = 1; - } - - if (objectCreation.ArgumentList.Arguments.Count > argumentCheckStart) - { - var locations = objectCreation.ArgumentList.Arguments - .Skip(argumentCheckStart) - .Select(a => a.Expression.GetLocation()) - .ToArray(); - - context.ReportDiagnostic(Rule, locations); - - return; - } - } - } -} diff --git a/src/Moq.Analyzers/Analyzers/ConstructorArgumentsMustMatchAnalyzer.cs b/src/Moq.Analyzers/Analyzers/ConstructorArgumentsMustMatchAnalyzer.cs index 39b6685..205bbc5 100644 --- a/src/Moq.Analyzers/Analyzers/ConstructorArgumentsMustMatchAnalyzer.cs +++ b/src/Moq.Analyzers/Analyzers/ConstructorArgumentsMustMatchAnalyzer.cs @@ -16,6 +16,16 @@ namespace PosInformatique.Moq.Analyzers [DiagnosticAnalyzer(LanguageNames.CSharp)] public class ConstructorArgumentsMustMatchAnalyzer : DiagnosticAnalyzer { + internal static readonly DiagnosticDescriptor ConstructorArgumentsCanBePassedToInterfaceRule = new DiagnosticDescriptor( + "PosInfoMoq2004", + "Constructor arguments cannot be passed for interface mocks", + "Constructor arguments cannot be passed for interface mocks", + "Compilation", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Constructor arguments cannot be passed for interface mocks.", + helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2004.html"); + private static readonly DiagnosticDescriptor ConstructorArgumentsMustMatchMockedClassRule = new DiagnosticDescriptor( "PosInfoMoq2005", "Constructor arguments must match the constructors of the mocked class", @@ -47,6 +57,7 @@ public class ConstructorArgumentsMustMatchAnalyzer : DiagnosticAnalyzer helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2016.html"); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( + ConstructorArgumentsCanBePassedToInterfaceRule, ConstructorArgumentsMustMatchMockedClassRule, ConstructorMockedClassMustBeAccessibleRule, ConstructorWithLambdaExpressionCanBeUseWithClassesOnlyRule); @@ -100,12 +111,6 @@ private static void Analyze(SyntaxNodeAnalysisContext context) return; } - // Check the type is a class (other type are ignored) - if (mockedType.TypeKind != TypeKind.Class) - { - return; - } - // Check the type is a named type if (mockedType is not INamedTypeSymbol namedTypeSymbol) { @@ -131,6 +136,21 @@ private static void Analyze(SyntaxNodeAnalysisContext context) } } + // If the type is an interface and contains arguments, raise an error. + if (mockedType.TypeKind == TypeKind.Interface) + { + if (constructorArguments.Count > 0) + { + context.ReportDiagnostic(ConstructorArgumentsCanBePassedToInterfaceRule, constructorArguments.Select(a => a.GetLocation())); + } + } + + // Check the type is a class (other type are ignored) + if (mockedType.TypeKind != TypeKind.Class) + { + return; + } + var matchedConstructor = default(MatchedConstructor); // Iterate on each constructor and check if the arguments match. diff --git a/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentCannotBePassedForInterfaceAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentCannotBePassedForInterfaceAnalyzerTest.cs deleted file mode 100644 index 6f48b13..0000000 --- a/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentCannotBePassedForInterfaceAnalyzerTest.cs +++ /dev/null @@ -1,204 +0,0 @@ -//----------------------------------------------------------------------- -// -// Copyright (c) P.O.S Informatique. All rights reserved. -// -//----------------------------------------------------------------------- - -namespace PosInformatique.Moq.Analyzers.Tests -{ - using Microsoft.CodeAnalysis.Testing; - using Verifier = MoqCSharpAnalyzerVerifier; - - public class ConstructorArgumentCannotBePassedForInterfaceAnalyzerTest - { - [Fact] - public async Task Interface_NoMock() - { - var source = @" - namespace ConsoleApplication1 - { - public class TestClass - { - public void TestMethod() - { - var obj = new object(); - } - } - - public interface I - { - } - }"; - - await Verifier.VerifyAnalyzerAsync(source); - } - - [Fact] - public async Task Interface_WithNoArgument() - { - var source = @" - namespace ConsoleApplication1 - { - using Moq; - - public class TestClass - { - public void TestMethod() - { - var mock1 = new Mock(); - } - } - - public interface I - { - } - }"; - - await Verifier.VerifyAnalyzerAsync(source); - } - - [Fact] - public async Task Interface_WithoutBehaviorStrict() - { - var source = @" - namespace ConsoleApplication1 - { - using Moq; - - public class TestClass - { - public void TestMethod() - { - var mock1 = new Mock({|#0:1|}, {|#1:2|}); - } - } - - public interface I - { - } - }"; - - await Verifier.VerifyAnalyzerAsync( - source, - new DiagnosticResult(ConstructorArgumentCannotBePassedForInterfaceAnalyzer.Rule) - .WithLocation(0).WithArguments("1") - .WithLocation(1).WithArguments("2")); - } - - [Fact] - public async Task Interface_WithBehaviorStrict() - { - var source = @" - namespace ConsoleApplication1 - { - using Moq; - - public class TestClass - { - public void TestMethod() - { - var mock1 = new Mock(MockBehavior.Strict, {|#0:1|}, {|#1:2|}); - } - } - - public interface I - { - } - }"; - - await Verifier.VerifyAnalyzerAsync( - source, - new DiagnosticResult(ConstructorArgumentCannotBePassedForInterfaceAnalyzer.Rule) - .WithLocation(0).WithArguments("1") - .WithLocation(1).WithArguments("2")); - } - - [Theory] - [InlineData("")] - [InlineData(", MockBehavior.Strict")] - public async Task Interface_WithFactoryLambdaExpression(string behavior) - { - var source = @" - namespace ConsoleApplication1 - { - using Moq; - - public class TestClass - { - public void TestMethod() - { - var mock1 = new Mock(() => new C()" + behavior + @"); - } - } - - public interface I - { - } - - public class C : I - { - } - }"; - - await Verifier.VerifyAnalyzerAsync(source); - } - - [Fact] - public async Task Class() - { - var source = @" - namespace ConsoleApplication1 - { - using Moq; - - public class TestClass - { - public void TestMethod() - { - var mock1 = new Mock(1, 2, 3); - } - } - - public abstract class C - { - } - }"; - - await Verifier.VerifyAnalyzerAsync(source); - } - - [Fact] - public async Task Interface_NoMoqLibrary() - { - var source = @" - namespace ConsoleApplication1 - { - using OtherNamespace; - - public class TestClass - { - public void TestMethod() - { - var mock1 = new Mock(MockBehavior.Strict, 1, 2, 3); - } - } - - public interface I - { - } - } - - namespace OtherNamespace - { - public class Mock - { - public Mock(MockBehavior _, int a, int b, int c) { } - } - - public enum MockBehavior { Strict, Loose } - }"; - - await Verifier.VerifyAnalyzerWithNoMoqLibraryAsync(source); - } - } -} \ No newline at end of file diff --git a/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentsMustMatchAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentsMustMatchAnalyzerTest.cs index 1ea68ea..6a2b0e0 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentsMustMatchAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentsMustMatchAnalyzerTest.cs @@ -6,6 +6,7 @@ namespace PosInformatique.Moq.Analyzers.Tests { + using Microsoft.CodeAnalysis.Testing; using Verifier = MoqCSharpAnalyzerVerifier; public class ConstructorArgumentsMustMatchAnalyzerTest @@ -54,6 +55,108 @@ public void TestMethod() await Verifier.VerifyAnalyzerAsync(source); } + [Fact] + public async Task Interface_NoMock() + { + var source = @" + namespace ConsoleApplication1 + { + public class TestClass + { + public void TestMethod() + { + var obj = new object(); + } + } + + public interface I + { + } + }"; + + await Verifier.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task Interface_WithNoArgument() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(); + } + } + + public interface I + { + } + }"; + + await Verifier.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task Interface_WithoutBehaviorStrict() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock({|#0:1|}, {|#1:2|}); + } + } + + public interface I + { + } + }"; + + await Verifier.VerifyAnalyzerAsync( + source, + new DiagnosticResult(ConstructorArgumentsMustMatchAnalyzer.ConstructorArgumentsCanBePassedToInterfaceRule) + .WithLocation(0).WithArguments("1") + .WithLocation(1).WithArguments("2")); + } + + [Fact] + public async Task Interface_WithBehaviorStrict() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(MockBehavior.Strict, {|#0:1|}, {|#1:2|}); + } + } + + public interface I + { + } + }"; + + await Verifier.VerifyAnalyzerAsync( + source, + new DiagnosticResult(ConstructorArgumentsMustMatchAnalyzer.ConstructorArgumentsCanBePassedToInterfaceRule) + .WithLocation(0).WithArguments("1") + .WithLocation(1).WithArguments("2")); + } + [Fact] public async Task Arguments_Empty() { From 1aef55b215d7ee7371aef2789e053ee74c500fd6 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 23 Oct 2024 12:59:35 +0200 Subject: [PATCH 08/15] Rename the ConstructorArgumentsAnalyzer. --- ...atchAnalyzer.cs => ConstructorArgumentsAnalyzer.cs} | 4 ++-- ...yzerTest.cs => ConstructorArgumentsAnalyzerTest.cs} | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) rename src/Moq.Analyzers/Analyzers/{ConstructorArgumentsMustMatchAnalyzer.cs => ConstructorArgumentsAnalyzer.cs} (98%) rename tests/Moq.Analyzers.Tests/Analyzers/{ConstructorArgumentsMustMatchAnalyzerTest.cs => ConstructorArgumentsAnalyzerTest.cs} (98%) diff --git a/src/Moq.Analyzers/Analyzers/ConstructorArgumentsMustMatchAnalyzer.cs b/src/Moq.Analyzers/Analyzers/ConstructorArgumentsAnalyzer.cs similarity index 98% rename from src/Moq.Analyzers/Analyzers/ConstructorArgumentsMustMatchAnalyzer.cs rename to src/Moq.Analyzers/Analyzers/ConstructorArgumentsAnalyzer.cs index 205bbc5..11eb390 100644 --- a/src/Moq.Analyzers/Analyzers/ConstructorArgumentsMustMatchAnalyzer.cs +++ b/src/Moq.Analyzers/Analyzers/ConstructorArgumentsAnalyzer.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------- -// +// // Copyright (c) P.O.S Informatique. All rights reserved. // //----------------------------------------------------------------------- @@ -14,7 +14,7 @@ namespace PosInformatique.Moq.Analyzers using Microsoft.CodeAnalysis.Text; [DiagnosticAnalyzer(LanguageNames.CSharp)] - public class ConstructorArgumentsMustMatchAnalyzer : DiagnosticAnalyzer + public class ConstructorArgumentsAnalyzer : DiagnosticAnalyzer { internal static readonly DiagnosticDescriptor ConstructorArgumentsCanBePassedToInterfaceRule = new DiagnosticDescriptor( "PosInfoMoq2004", diff --git a/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentsMustMatchAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentsAnalyzerTest.cs similarity index 98% rename from tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentsMustMatchAnalyzerTest.cs rename to tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentsAnalyzerTest.cs index 6a2b0e0..447bdfc 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentsMustMatchAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentsAnalyzerTest.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------- -// +// // Copyright (c) P.O.S Informatique. All rights reserved. // //----------------------------------------------------------------------- @@ -7,9 +7,9 @@ namespace PosInformatique.Moq.Analyzers.Tests { using Microsoft.CodeAnalysis.Testing; - using Verifier = MoqCSharpAnalyzerVerifier; + using Verifier = MoqCSharpAnalyzerVerifier; - public class ConstructorArgumentsMustMatchAnalyzerTest + public class ConstructorArgumentsAnalyzerTest { [Fact] public async Task NoMock() @@ -124,7 +124,7 @@ public interface I await Verifier.VerifyAnalyzerAsync( source, - new DiagnosticResult(ConstructorArgumentsMustMatchAnalyzer.ConstructorArgumentsCanBePassedToInterfaceRule) + new DiagnosticResult(ConstructorArgumentsAnalyzer.ConstructorArgumentsCanBePassedToInterfaceRule) .WithLocation(0).WithArguments("1") .WithLocation(1).WithArguments("2")); } @@ -152,7 +152,7 @@ public interface I await Verifier.VerifyAnalyzerAsync( source, - new DiagnosticResult(ConstructorArgumentsMustMatchAnalyzer.ConstructorArgumentsCanBePassedToInterfaceRule) + new DiagnosticResult(ConstructorArgumentsAnalyzer.ConstructorArgumentsCanBePassedToInterfaceRule) .WithLocation(0).WithArguments("1") .WithLocation(1).WithArguments("2")); } From cb128a366d5fda24121794eaeb562e02ca941c4c Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Wed, 23 Oct 2024 13:00:33 +0200 Subject: [PATCH 09/15] Change the version to 1.11. --- .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 f7f58c8..4f233d2 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.10.0 + default: 1.11.0 VersionSuffix: type: string description: The version suffix of the library (for example rc.1) From 75956183814a2c4fcc2c372c23670e08163cc6a8 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 24 Oct 2024 10:27:33 +0200 Subject: [PATCH 10/15] Fix the BehaviorStrict fixer with Mock.Of() when using in constructors (fixes #41). --- .../SetBehaviorToStrictCodeFixProvider.cs | 25 ++++---- .../SetBehaviorToStrictCodeFixProviderTest.cs | 60 +++++++++++++++++++ 2 files changed, 73 insertions(+), 12 deletions(-) diff --git a/src/Moq.Analyzers/CodeFixes/SetBehaviorToStrictCodeFixProvider.cs b/src/Moq.Analyzers/CodeFixes/SetBehaviorToStrictCodeFixProvider.cs index 3bfe6a1..0e4c900 100644 --- a/src/Moq.Analyzers/CodeFixes/SetBehaviorToStrictCodeFixProvider.cs +++ b/src/Moq.Analyzers/CodeFixes/SetBehaviorToStrictCodeFixProvider.cs @@ -42,36 +42,37 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) var diagnostic = context.Diagnostics.First(); var diagnosticSpan = diagnostic.Location.SourceSpan; - // Find the "ObjectCreationExpressionSyntax" or "InvocationExpression" in the parent of the location where is located the issue in the code. - var parent = root.FindToken(diagnosticSpan.Start).Parent; + // Gets the syntax node where is located the issue in the code. + var node = root.FindNode(diagnosticSpan, getInnermostNodeForTie: true); - if (parent is null) + if (node is null) { return; } - var mockCreationExpression = parent.AncestorsAndSelf().OfType().FirstOrDefault(); - - if (mockCreationExpression is null) + if (node is ObjectCreationExpressionSyntax mockCreationExpression) { - var invocationExpression = parent.AncestorsAndSelf().OfType().First(); - + // Register a code to fix the enumeration. context.RegisterCodeFix( CodeAction.Create( title: "Defines the MockBehavior to Strict", - createChangedDocument: cancellationToken => AddMockBehiavorStrictArgumentAsync(context.Document, invocationExpression, cancellationToken), + createChangedDocument: cancellationToken => AddMockBehiavorStrictArgumentAsync(context.Document, mockCreationExpression, cancellationToken), equivalenceKey: "Defines the MockBehavior to Strict"), diagnostic); + + return; } - else + + if (node is InvocationExpressionSyntax invocationExpression) { - // Register a code to fix the enumeration. context.RegisterCodeFix( CodeAction.Create( title: "Defines the MockBehavior to Strict", - createChangedDocument: cancellationToken => AddMockBehiavorStrictArgumentAsync(context.Document, mockCreationExpression, cancellationToken), + createChangedDocument: cancellationToken => AddMockBehiavorStrictArgumentAsync(context.Document, invocationExpression, cancellationToken), equivalenceKey: "Defines the MockBehavior to Strict"), diagnostic); + + return; } } diff --git a/tests/Moq.Analyzers.Tests/CodeFixes/SetBehaviorToStrictCodeFixProviderTest.cs b/tests/Moq.Analyzers.Tests/CodeFixes/SetBehaviorToStrictCodeFixProviderTest.cs index 8e0ac44..70ec544 100644 --- a/tests/Moq.Analyzers.Tests/CodeFixes/SetBehaviorToStrictCodeFixProviderTest.cs +++ b/tests/Moq.Analyzers.Tests/CodeFixes/SetBehaviorToStrictCodeFixProviderTest.cs @@ -247,5 +247,65 @@ public interface I await Verifier.VerifyCodeFixAsync(source, expectedFixedSource); } + + [Theory] + [InlineData("")] + [InlineData("MockBehavior.Loose")] + [InlineData("MockBehavior.Default")] + public async Task MockOf_InConstructor_Fix(string behavior) + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock = new C([|Mock.Of(" + behavior + @")|], [|Mock.Of(" + behavior + @")|]); + } + } + + public interface I + { + } + + public class C + { + public C(I r1, I r2) + { + } + } + }"; + + var expectedFixedSource = + @" + namespace ConsoleApplication1 + { + using Moq; + + public class TestClass + { + public void TestMethod() + { + var mock = new C(Mock.Of(MockBehavior.Strict), Mock.Of(MockBehavior.Strict)); + } + } + + public interface I + { + } + + public class C + { + public C(I r1, I r2) + { + } + } + }"; + + await Verifier.VerifyCodeFixAsync(source, expectedFixedSource); + } } } From a06e5b91b7dc1dbcb517e51a60b2a961a542ea01 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 24 Oct 2024 12:38:34 +0200 Subject: [PATCH 11/15] Add the PosInfoMoq1005 rule to check usage of the SetupSet() method (fixes #42). --- PosInformatique.Moq.Analyzers.sln | 1 + README.md | 1 + docs/Design/PosInfoMoq1005.md | 86 +++++++++++++++ src/Moq.Analyzers/AnalyzerReleases.Shipped.md | 2 + .../Analyzers/SetupSetAnalyzer.cs | 68 ++++++++++++ src/Moq.Analyzers/Moq.Analyzers.csproj | 6 +- src/Moq.Analyzers/MoqSymbols.cs | 36 +++++++ .../Analyzers/SetupSetAnalyzerTest.cs | 102 ++++++++++++++++++ 8 files changed, 300 insertions(+), 2 deletions(-) create mode 100644 docs/Design/PosInfoMoq1005.md create mode 100644 src/Moq.Analyzers/Analyzers/SetupSetAnalyzer.cs create mode 100644 tests/Moq.Analyzers.Tests/Analyzers/SetupSetAnalyzerTest.cs diff --git a/PosInformatique.Moq.Analyzers.sln b/PosInformatique.Moq.Analyzers.sln index ceee7c1..ade5bc5 100644 --- a/PosInformatique.Moq.Analyzers.sln +++ b/PosInformatique.Moq.Analyzers.sln @@ -33,6 +33,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Design", "Design", "{815BE8 docs\Design\PosInfoMoq1002.md = docs\Design\PosInfoMoq1002.md docs\Design\PosInfoMoq1003.md = docs\Design\PosInfoMoq1003.md docs\Design\PosInfoMoq1004.md = docs\Design\PosInfoMoq1004.md + docs\Design\PosInfoMoq1005.md = docs\Design\PosInfoMoq1005.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Compilation", "Compilation", "{D9C84D36-7F9C-4EFB-BE6F-9F7A05FE957D}" diff --git a/README.md b/README.md index a104dc6..322835c 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Design rules used to make your unit tests more strongly strict. | [PosInfoMoq1002: `Verify()` methods should be called when `Verifiable()` has been setup](docs/Design/PosInfoMoq1002.md) | When a mocked member has been setup with the `Verifiable()` method, the `Verify()` method must be called at the end of the unit test. | | [PosInfoMoq1003: The `Callback()` method should be used to check the parameters when mocking a method with `It.IsAny()` arguments](docs/Design/PosInfoMoq1003.md) | When a mocked method contains a `It.IsAny()` argument, the related parameter should be checked in the `Callback()` method. | | [PosInfoMoq1004: The `Callback()` parameter should not be ignored if it has been setup as an `It.IsAny()` argument](docs/Design/PosInfoMoq1004.md) | When a mocked method contains a `It.IsAny()` argument, the related parameter should not be ignored in the `Callback()` method. | +| [PosInfoMoq1005: Defines the generic argument of the `SetupSet()` method with the type of the mocked property](docs/Design/PosInfoMoq1005.md) | When mocking the setter of a property, use the `SetupSet()` method version. | ### Compilation diff --git a/docs/Design/PosInfoMoq1005.md b/docs/Design/PosInfoMoq1005.md new file mode 100644 index 0000000..5d9c9b8 --- /dev/null +++ b/docs/Design/PosInfoMoq1005.md @@ -0,0 +1,86 @@ +# PosInfoMoq1005: Defines the generic argument of the `SetupSet()` method with the type of the mocked property. + +| Property | Value | +|-------------------------------------|------------------------------------------------------------------------------------------------------| +| **Rule ID** | PosInfoMoq1005 | +| **Title** | Defines the generic argument of the `SetupSet()` method with the type of the mocked property. | +| **Category** | Design | +| **Default severity** | Warning | + +## Cause + +A property setter has been set up using `SetupSet()` without a generic argument that represents the type of the mocked property. + +## Rule description + +Moq provides two methods to mock a property setter: +- `Mock.SetupSet(Action)` +- `Mock.SetupSet(Action)` + +When setting up a property setter, use `Mock.SetupSet(Action)` by explicitly defining the type of the property to mock. +This overload of the `SetupSet()` method allows you to define a typed `Callback()` and avoid exceptions if the delegate argument in the `Callback()` +does not match the property type. + +For example, consider the following code to test: + +```csharp +[Fact] +public interface Customer +{ + string Name { get; set; } +} +``` + +If you mock the setter of the `Customer.Name` property, you should set up the property with the `SetupSet()` method: + +```csharp +[Fact] +public void SetNameOfCustomer() +{ + var customer = new Mock(); + customer.SetupSet(c => c.Name = "Gilles") // The SetupSet() version is used. + .Callback((string value) => + { + // Called when the setter of the property is set. + }); +} +``` + +The following code violates the rule because the `SetupSet()` method has no generic argument: + +```csharp +[Fact] +public void SetNameOfCustomer() +{ + var customer = new Mock(); + customer.SetupSet(c => c.Name = "Gilles") // The SetupSet() has been used without set the generic argument. + .Callback((string value) => + { + // Called when the setter of the property is set. + }); +} +``` + +If the non-generic version of the `SetupSet()` method is used, the delegate in the `Callback()` method cannot be checked at compile time, +an exception will occur during the execution of the unit test: + +```csharp +[Fact] +public void SetNameOfCustomer() +{ + var customer = new Mock(); + customer.SetupSet(c => c.Name = "Gilles") + .Callback((int value) => // The code compiles, but during the execution of the unit test + { // an ArgumentException will be thrown. + }); +} +``` + +## How to fix violations + +To fix a violation of this rule, use the `SetupSet()` method with the type of the mocked property as the generic argument. + +## When to suppress warnings + +Do not suppress a warning from this rule. Using the `SetupSet()` method ensures that the delegate argument in the `Callback()` +method matches the type of the property. \ No newline at end of file diff --git a/src/Moq.Analyzers/AnalyzerReleases.Shipped.md b/src/Moq.Analyzers/AnalyzerReleases.Shipped.md index 04270ce..f4bda3e 100644 --- a/src/Moq.Analyzers/AnalyzerReleases.Shipped.md +++ b/src/Moq.Analyzers/AnalyzerReleases.Shipped.md @@ -3,9 +3,11 @@ ### New Rules Rule ID | Category | Severity | Notes --------|----------|----------|------- +PosInfoMoq1005 | Compilation | Error | Defines the generic argument of the `SetupSet()` method with the type of the mocked property. PosInfoMoq2014 | Compilation | Error | The `Callback()` delegate expression must not return a value. PosInfoMoq2015 | Compilation | Error | The `Protected().Setup()` method must match the return type of the mocked method. PosInfoMoq2016 | Compilation | Error | `Mock` constructor with factory lambda expression can be used only with classes. + ## Release 1.10.0 ### New Rules diff --git a/src/Moq.Analyzers/Analyzers/SetupSetAnalyzer.cs b/src/Moq.Analyzers/Analyzers/SetupSetAnalyzer.cs new file mode 100644 index 0000000..4eab583 --- /dev/null +++ b/src/Moq.Analyzers/Analyzers/SetupSetAnalyzer.cs @@ -0,0 +1,68 @@ +//----------------------------------------------------------------------- +// +// 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 SetupSetAnalyzer : DiagnosticAnalyzer + { + private static readonly DiagnosticDescriptor UseSetupSetWithGenericArgumentRule = new DiagnosticDescriptor( + "PosInfoMoq1005", + "Defines the generic argument of the SetupSet() method with the type of the mocked property", + "Defines the generic argument of the SetupSet() method with the type of the mocked property", + "Compilation", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "The SetupSet() method must be used when setting up a mocked property.", + helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq1005.html"); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(UseSetupSetWithGenericArgumentRule); + + 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 methodSymbol = context.SemanticModel.GetSymbolInfo(invocationExpression, context.CancellationToken); + + // Check if not obsolete extension + if (moqSymbols.IsObsoleteMockExtension(methodSymbol.Symbol)) + { + return; + } + + // Check is Setup() method. + if (!moqSymbols.IsSetupSetMethodWithoutGenericArgument(methodSymbol.Symbol)) + { + var nameSyntax = ((MemberAccessExpressionSyntax)invocationExpression.Expression).Name; + + context.ReportDiagnostic(UseSetupSetWithGenericArgumentRule, nameSyntax.GetLocation()); + + return; + } + } + } +} diff --git a/src/Moq.Analyzers/Moq.Analyzers.csproj b/src/Moq.Analyzers/Moq.Analyzers.csproj index c9a70cd..73b5f61 100644 --- a/src/Moq.Analyzers/Moq.Analyzers.csproj +++ b/src/Moq.Analyzers/Moq.Analyzers.csproj @@ -19,16 +19,18 @@ 1.11.0 - Add new rules: + - PosInfoMoq1005: Defines the generic argument of the SetupSet() method with the type of the mocked property. - PosInfoMoq2014: The delegate in the argument of the Returns() method must return a value with same type of the mocked method. - PosInfoMoq2015: The Protected().Setup() method must match the return type of the mocked method. - PosInfoMoq2016: Mock<T> constructor with factory lambda expression can be used only with classes. - Fix the PosInfoMoq1001 rule for the Mock<T> instantiation with the lambda expression to create mock instances. - Fix the PosInfoMoq2004 rule for the Mock<T> instantiation with the lambda expression to create mock instances. + - Fix the PosInfoMoq1001 fixer when using the Mock.Of<T> as argument in the constructor call. 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. + - 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. diff --git a/src/Moq.Analyzers/MoqSymbols.cs b/src/Moq.Analyzers/MoqSymbols.cs index 471c093..f0e90fa 100644 --- a/src/Moq.Analyzers/MoqSymbols.cs +++ b/src/Moq.Analyzers/MoqSymbols.cs @@ -43,6 +43,10 @@ internal sealed class MoqSymbols private readonly Lazy mockConstructorWithFactory; + private readonly Lazy setupSetMethodWithoutGenericArgument; + + private readonly Lazy obsoleteExtensionsClass; + private MoqSymbols(INamedTypeSymbol mockGenericClass, Compilation compilation) { this.mockGenericClass = mockGenericClass; @@ -53,6 +57,7 @@ private MoqSymbols(INamedTypeSymbol mockGenericClass, Compilation compilation) this.isAnyTypeClass = new Lazy(() => compilation.GetTypeByMetadataName("Moq.It+IsAnyType")!); this.isAnyMethod = new Lazy(() => compilation.GetTypeByMetadataName("Moq.It")!.GetMembers("IsAny").Single()); this.verifiesInterface = new Lazy(() => compilation.GetTypeByMetadataName("Moq.Language.IVerifies")!); + this.obsoleteExtensionsClass = new Lazy(() => compilation.GetTypeByMetadataName("Moq.ObsoleteMockExtensions")!); this.setupMethods = new Lazy>(() => mockGenericClass.GetMembers("Setup").Concat(setupConditionResultInterface.Value.GetMembers("Setup")).OfType().ToArray()); this.mockBehaviorStrictField = new Lazy(() => this.mockBehaviorEnum.Value.GetMembers("Strict").First()); @@ -68,6 +73,7 @@ private MoqSymbols(INamedTypeSymbol mockGenericClass, Compilation compilation) this.mockOfMethods = new Lazy>(() => mockGenericClass.BaseType!.GetMembers("Of").Where(m => m.IsStatic).OfType().ToArray()); this.mockConstructorWithFactory = new Lazy(() => mockGenericClass.Constructors.Single(c => c.Parameters.Length == 2 && c.Parameters[0].Type.Name == "Expression")); + this.setupSetMethodWithoutGenericArgument = new Lazy(() => mockGenericClass.GetMembers("SetupSet").OfType().Single(c => c.TypeArguments.Length == 1)); } public static MoqSymbols? FromCompilation(Compilation compilation) @@ -122,6 +128,21 @@ public bool IsMock(ISymbol? symbol) return true; } + public bool IsObsoleteMockExtension(ISymbol? symbol) + { + if (symbol is null) + { + return false; + } + + if (!SymbolEqualityComparer.Default.Equals(symbol.ContainingType, this.obsoleteExtensionsClass.Value)) + { + return false; + } + + return true; + } + public bool IsSetupMethod(ISymbol? symbol) { if (symbol is null) @@ -162,6 +183,21 @@ public bool IsSetupProtectedMethod(ISymbol? symbol) return false; } + public bool IsSetupSetMethodWithoutGenericArgument(ISymbol? symbol) + { + if (symbol is null) + { + return false; + } + + if (!SymbolEqualityComparer.Default.Equals(symbol.OriginalDefinition, this.setupSetMethodWithoutGenericArgument.Value)) + { + return false; + } + + return true; + } + public bool IsVerifiableMethod(ISymbol? symbol) { if (symbol is null) diff --git a/tests/Moq.Analyzers.Tests/Analyzers/SetupSetAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/SetupSetAnalyzerTest.cs new file mode 100644 index 0000000..75f2267 --- /dev/null +++ b/tests/Moq.Analyzers.Tests/Analyzers/SetupSetAnalyzerTest.cs @@ -0,0 +1,102 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Moq.Analyzers.Tests +{ + using Verifier = MoqCSharpAnalyzerVerifier; + + public class SetupSetAnalyzerTest + { + [Fact] + public async Task SetupSetWithGenericArgument_NoDiagnosticReported() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + using System; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(); + mock1.SetupSet(i => i.TestProperty = 1234); + mock1.SetupSet(i => i.TestProperty); // Ignored because Obsolete by Moq + } + } + + public interface I + { + int TestProperty { get; set; } + } + }"; + + await Verifier.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task SetupSetWithNoGenericArgument_DiagnosticReported() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + using System; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(); + mock1.[|SetupSet|](i => i.TestProperty = 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.SetupSet(i => i.Property = 1234); + } + } + + public interface I + { + int Property { get; set; } + } + } + + namespace OtherNamespace + { + public class Mock + { + public void SetupSet(System.Action _) { } + } + }"; + + await Verifier.VerifyAnalyzerWithNoMoqLibraryAsync(source); + } + } +} \ No newline at end of file From 74df2990bf50466326154dd36c4ab3e51436c406 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 24 Oct 2024 15:52:21 +0200 Subject: [PATCH 12/15] Fix the SetupSetAnalyzer. --- .../Analyzers/SetupSetAnalyzer.cs | 6 ++-- src/Moq.Analyzers/MoqSymbols.cs | 30 +++++++++++-------- .../Analyzers/SetupSetAnalyzerTest.cs | 6 ++++ 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/Moq.Analyzers/Analyzers/SetupSetAnalyzer.cs b/src/Moq.Analyzers/Analyzers/SetupSetAnalyzer.cs index 4eab583..a51eb8c 100644 --- a/src/Moq.Analyzers/Analyzers/SetupSetAnalyzer.cs +++ b/src/Moq.Analyzers/Analyzers/SetupSetAnalyzer.cs @@ -48,13 +48,13 @@ private static void Analyze(SyntaxNodeAnalysisContext context) var methodSymbol = context.SemanticModel.GetSymbolInfo(invocationExpression, context.CancellationToken); - // Check if not obsolete extension - if (moqSymbols.IsObsoleteMockExtension(methodSymbol.Symbol)) + // Check if SetupSet() method. + if (!moqSymbols.IsSetupSetMethod(methodSymbol.Symbol)) { return; } - // Check is Setup() method. + // Check is SetupSet() method. if (!moqSymbols.IsSetupSetMethodWithoutGenericArgument(methodSymbol.Symbol)) { var nameSyntax = ((MemberAccessExpressionSyntax)invocationExpression.Expression).Name; diff --git a/src/Moq.Analyzers/MoqSymbols.cs b/src/Moq.Analyzers/MoqSymbols.cs index f0e90fa..818d846 100644 --- a/src/Moq.Analyzers/MoqSymbols.cs +++ b/src/Moq.Analyzers/MoqSymbols.cs @@ -45,7 +45,7 @@ internal sealed class MoqSymbols private readonly Lazy setupSetMethodWithoutGenericArgument; - private readonly Lazy obsoleteExtensionsClass; + private readonly Lazy> setupSetMethods; private MoqSymbols(INamedTypeSymbol mockGenericClass, Compilation compilation) { @@ -57,7 +57,6 @@ private MoqSymbols(INamedTypeSymbol mockGenericClass, Compilation compilation) this.isAnyTypeClass = new Lazy(() => compilation.GetTypeByMetadataName("Moq.It+IsAnyType")!); this.isAnyMethod = new Lazy(() => compilation.GetTypeByMetadataName("Moq.It")!.GetMembers("IsAny").Single()); this.verifiesInterface = new Lazy(() => compilation.GetTypeByMetadataName("Moq.Language.IVerifies")!); - this.obsoleteExtensionsClass = new Lazy(() => compilation.GetTypeByMetadataName("Moq.ObsoleteMockExtensions")!); this.setupMethods = new Lazy>(() => mockGenericClass.GetMembers("Setup").Concat(setupConditionResultInterface.Value.GetMembers("Setup")).OfType().ToArray()); this.mockBehaviorStrictField = new Lazy(() => this.mockBehaviorEnum.Value.GetMembers("Strict").First()); @@ -73,7 +72,9 @@ private MoqSymbols(INamedTypeSymbol mockGenericClass, Compilation compilation) this.mockOfMethods = new Lazy>(() => mockGenericClass.BaseType!.GetMembers("Of").Where(m => m.IsStatic).OfType().ToArray()); this.mockConstructorWithFactory = new Lazy(() => mockGenericClass.Constructors.Single(c => c.Parameters.Length == 2 && c.Parameters[0].Type.Name == "Expression")); + this.setupSetMethodWithoutGenericArgument = new Lazy(() => mockGenericClass.GetMembers("SetupSet").OfType().Single(c => c.TypeArguments.Length == 1)); + this.setupSetMethods = new Lazy>(() => mockGenericClass.GetMembers("SetupSet").OfType().ToArray()); } public static MoqSymbols? FromCompilation(Compilation compilation) @@ -128,22 +129,27 @@ public bool IsMock(ISymbol? symbol) return true; } - public bool IsObsoleteMockExtension(ISymbol? symbol) + public bool IsSetupMethod(ISymbol? symbol) { if (symbol is null) { return false; } - if (!SymbolEqualityComparer.Default.Equals(symbol.ContainingType, this.obsoleteExtensionsClass.Value)) + var originalDefinition = symbol.OriginalDefinition; + + foreach (var setupMethod in this.setupMethods.Value) { - return false; + if (SymbolEqualityComparer.Default.Equals(originalDefinition, setupMethod)) + { + return true; + } } - return true; + return false; } - public bool IsSetupMethod(ISymbol? symbol) + public bool IsSetupProtectedMethod(ISymbol? symbol) { if (symbol is null) { @@ -152,9 +158,9 @@ public bool IsSetupMethod(ISymbol? symbol) var originalDefinition = symbol.OriginalDefinition; - foreach (var setupMethod in this.setupMethods.Value) + foreach (var setupProtectedMethod in this.setupProtectedMethods.Value) { - if (SymbolEqualityComparer.Default.Equals(originalDefinition, setupMethod)) + if (SymbolEqualityComparer.Default.Equals(originalDefinition, setupProtectedMethod)) { return true; } @@ -163,7 +169,7 @@ public bool IsSetupMethod(ISymbol? symbol) return false; } - public bool IsSetupProtectedMethod(ISymbol? symbol) + public bool IsSetupSetMethod(ISymbol? symbol) { if (symbol is null) { @@ -172,9 +178,9 @@ public bool IsSetupProtectedMethod(ISymbol? symbol) var originalDefinition = symbol.OriginalDefinition; - foreach (var setupProtectedMethod in this.setupProtectedMethods.Value) + foreach (var setupSetMethod in this.setupSetMethods.Value) { - if (SymbolEqualityComparer.Default.Equals(originalDefinition, setupProtectedMethod)) + if (SymbolEqualityComparer.Default.Equals(originalDefinition, setupSetMethod)) { return true; } diff --git a/tests/Moq.Analyzers.Tests/Analyzers/SetupSetAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/SetupSetAnalyzerTest.cs index 75f2267..f8868cb 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/SetupSetAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/SetupSetAnalyzerTest.cs @@ -26,6 +26,9 @@ public void TestMethod() var mock1 = new Mock(); mock1.SetupSet(i => i.TestProperty = 1234); mock1.SetupSet(i => i.TestProperty); // Ignored because Obsolete by Moq + + var s = ""The string""; + s.ToString(); // Invocation should be ignored } } @@ -53,6 +56,9 @@ public void TestMethod() { var mock1 = new Mock(); mock1.[|SetupSet|](i => i.TestProperty = 1234); + + var s = ""The string""; + s.ToString(); // Invocation should be ignored } } From ded1fb133896a1df512f611d5b481493eb141117 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 24 Oct 2024 16:26:30 +0200 Subject: [PATCH 13/15] Fix the PosInfoMoq1005 rule to Design and Warning level. --- src/Moq.Analyzers/AnalyzerReleases.Shipped.md | 2 +- src/Moq.Analyzers/Analyzers/SetupSetAnalyzer.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Moq.Analyzers/AnalyzerReleases.Shipped.md b/src/Moq.Analyzers/AnalyzerReleases.Shipped.md index f4bda3e..35d43d6 100644 --- a/src/Moq.Analyzers/AnalyzerReleases.Shipped.md +++ b/src/Moq.Analyzers/AnalyzerReleases.Shipped.md @@ -3,7 +3,7 @@ ### New Rules Rule ID | Category | Severity | Notes --------|----------|----------|------- -PosInfoMoq1005 | Compilation | Error | Defines the generic argument of the `SetupSet()` method with the type of the mocked property. +PosInfoMoq1005 | Design | Warning | Defines the generic argument of the `SetupSet()` method with the type of the mocked property. PosInfoMoq2014 | Compilation | Error | The `Callback()` delegate expression must not return a value. PosInfoMoq2015 | Compilation | Error | The `Protected().Setup()` method must match the return type of the mocked method. PosInfoMoq2016 | Compilation | Error | `Mock` constructor with factory lambda expression can be used only with classes. diff --git a/src/Moq.Analyzers/Analyzers/SetupSetAnalyzer.cs b/src/Moq.Analyzers/Analyzers/SetupSetAnalyzer.cs index a51eb8c..762b787 100644 --- a/src/Moq.Analyzers/Analyzers/SetupSetAnalyzer.cs +++ b/src/Moq.Analyzers/Analyzers/SetupSetAnalyzer.cs @@ -19,8 +19,8 @@ public class SetupSetAnalyzer : DiagnosticAnalyzer "PosInfoMoq1005", "Defines the generic argument of the SetupSet() method with the type of the mocked property", "Defines the generic argument of the SetupSet() method with the type of the mocked property", - "Compilation", - DiagnosticSeverity.Error, + "Design", + DiagnosticSeverity.Warning, isEnabledByDefault: true, description: "The SetupSet() method must be used when setting up a mocked property.", helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq1005.html"); From b71c04f027efa543268b0e7ad716b0d96704d564 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 24 Oct 2024 17:17:17 +0200 Subject: [PATCH 14/15] Updates the PosInfoMoq2001 to check property in the SetupSet(). --- README.md | 2 +- docs/Compilation/PosInfoMoq2001.md | 27 ++++--- ...BeUsedOnlyForOverridableMembersAnalyzer.cs | 6 +- src/Moq.Analyzers/Moq.Analyzers.csproj | 2 + src/Moq.Analyzers/MoqExpressionAnalyzer.cs | 7 ++ ...edOnlyForOverridableMembersAnalyzerTest.cs | 78 +++++++++++++++---- 6 files changed, 91 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 322835c..eb45187 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ All the rules of this category should not be disabled (or changed their severity | Rule | Description | | - | - | | [PosInfoMoq2000: The `Returns()` or `ReturnsAsync()` methods must be call for Strict mocks](docs/Compilation/PosInfoMoq2000.md) | When a `Mock` has been defined with the `Strict` behavior, the `Returns()` or `ReturnsAsync()` method must be called when setup a method to mock which returns a value. | -| [PosInfoMoq2001: The `Setup()` method must be used only on overridable members](docs/Compilation/PosInfoMoq2001.md)) | The `Setup()` method must be applied only for overridable members. | +| [PosInfoMoq2001: The `Setup()`/`SetupSet()` method must be used only on overridable members](docs/Compilation/PosInfoMoq2001.md)) | The `Setup()` method must be applied only for overridable members. | | [PosInfoMoq2002: `Mock` class can be used only to mock non-sealed class](docs/Compilation/PosInfoMoq2002.md) | The `Mock` class can mock only interfaces or non-`sealed` classes. | | [PosInfoMoq2003: The `Callback()` delegate expression must match the signature of the mocked method](docs/Compilation/PosInfoMoq2003.md) | The delegate in the argument of the `Callback()` method must match the signature of the mocked method. | | [PosInfoMoq2004: Constructor arguments cannot be passed for interface mocks](docs/Compilation/PosInfoMoq2004.md) | No arguments can be passed to a mocked interface. | diff --git a/docs/Compilation/PosInfoMoq2001.md b/docs/Compilation/PosInfoMoq2001.md index f851c66..9f98a05 100644 --- a/docs/Compilation/PosInfoMoq2001.md +++ b/docs/Compilation/PosInfoMoq2001.md @@ -1,15 +1,15 @@ -# PosInfoMoq2001: The `Setup()` method must be used only on overridable members +# PosInfoMoq2001: The `Setup()`/`SetupSet()` method must be used only on overridable members | Property | Value | |-------------------------------------|---------------------------------------------------------------| | **Rule ID** | PosInfoMoq2001 | -| **Title** | The `Setup()` method must be used only on overridable members | +| **Title** | The `Setup()`/`SetupSet()` method must be used only on overridable members | | **Category** | Compilation | | **Default severity** | Error | ## Cause -The `Setup()` method must be applied only for overridable members. +The `Setup()` or `SetupSet()` methods must be applied only for overridable members. An overridable member is a **method** or **property** which is in: - An `interface`. - A non-`sealed` `class`. In this case, the member must be: @@ -20,13 +20,18 @@ An overridable member is a **method** or **property** which is in: The `Setup()` method must be applied only for overridable members. -For example, the following methods and properties can be mock and used in the `Setup()` method: -- `IService.MethodCanBeMocked()` -- `IService.PropertyCanBeMocked` -- `Service.VirtualMethodCanBeMocked` -- `Service.VirtualPropertyCanBeMocked` -- `Service.AbstractMethodCanBeMocked` -- `Service.AbstractPropertyCanBeMocked` +For example: +- The following methods and properties can be mock and used in the `Setup()` method: + - `IService.MethodCanBeMocked()` + - `IService.PropertyCanBeMocked` + - `Service.VirtualMethodCanBeMocked` + - `Service.VirtualPropertyCanBeMocked` + - `Service.AbstractMethodCanBeMocked` + - `Service.AbstractPropertyCanBeMocked` +- The following properties can be mock and used in the `SetupSet()` method: + - `IService.PropertyCanBeMocked` + - `Service.VirtualPropertyCanBeMocked` + - `Service.AbstractPropertyCanBeMocked` ```csharp public interface IService @@ -53,7 +58,7 @@ static methods which can not be overriden. ## How to fix violations -To fix a violation of this rule, be sure to mock a member in the `Setup()` method which can be overriden. +To fix a violation of this rule, be sure to mock a member in the `Setup()` or `SetupSet()` method which can be overriden. ## When to suppress warnings diff --git a/src/Moq.Analyzers/Analyzers/SetupMustBeUsedOnlyForOverridableMembersAnalyzer.cs b/src/Moq.Analyzers/Analyzers/SetupMustBeUsedOnlyForOverridableMembersAnalyzer.cs index d565f8a..2d6927c 100644 --- a/src/Moq.Analyzers/Analyzers/SetupMustBeUsedOnlyForOverridableMembersAnalyzer.cs +++ b/src/Moq.Analyzers/Analyzers/SetupMustBeUsedOnlyForOverridableMembersAnalyzer.cs @@ -17,8 +17,8 @@ public class SetupMustBeUsedOnlyForOverridableMembersAnalyzer : DiagnosticAnalyz { private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( "PosInfoMoq2001", - "The Setup() method must be used only on overridable members", - "The Setup() method must be used only on overridable members", + "The Setup()/SetupSet() method must be used only on overridable members", + "The Setup()/SetupSet() method must be used only on overridable members", "Compilation", DiagnosticSeverity.Error, isEnabledByDefault: true, @@ -51,7 +51,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context) // Check is Setup() method. var methodSymbol = context.SemanticModel.GetSymbolInfo(invocationExpression, context.CancellationToken); - if (!moqSymbols.IsSetupMethod(methodSymbol.Symbol)) + if (!moqSymbols.IsSetupMethod(methodSymbol.Symbol) && !moqSymbols.IsSetupSetMethod(methodSymbol.Symbol)) { return; } diff --git a/src/Moq.Analyzers/Moq.Analyzers.csproj b/src/Moq.Analyzers/Moq.Analyzers.csproj index 73b5f61..99b7406 100644 --- a/src/Moq.Analyzers/Moq.Analyzers.csproj +++ b/src/Moq.Analyzers/Moq.Analyzers.csproj @@ -23,6 +23,8 @@ - PosInfoMoq2014: The delegate in the argument of the Returns() method must return a value with same type of the mocked method. - PosInfoMoq2015: The Protected().Setup() method must match the return type of the mocked method. - PosInfoMoq2016: Mock<T> constructor with factory lambda expression can be used only with classes. + - Improvements of the rules: + - PosInfoMoq2001: Check that property setup with the SetupSet() method are overridable. - Fix the PosInfoMoq1001 rule for the Mock<T> instantiation with the lambda expression to create mock instances. - Fix the PosInfoMoq2004 rule for the Mock<T> instantiation with the lambda expression to create mock instances. - Fix the PosInfoMoq1001 fixer when using the Mock.Of<T> as argument in the constructor call. diff --git a/src/Moq.Analyzers/MoqExpressionAnalyzer.cs b/src/Moq.Analyzers/MoqExpressionAnalyzer.cs index c588345..7183f3f 100644 --- a/src/Moq.Analyzers/MoqExpressionAnalyzer.cs +++ b/src/Moq.Analyzers/MoqExpressionAnalyzer.cs @@ -367,6 +367,13 @@ public bool IsStrictBehavior(IdentifierNameSyntax localVariableExpression, Cance } bodyExpression = lambdaExpression.ExpressionBody; + + // Special case for the SetupSet(), if the body expression is an assignment syntax + // "mock => mock.Property = xxx", so we get the left part of the assignment. + if (bodyExpression is AssignmentExpressionSyntax assignmentExpressionSyntax) + { + bodyExpression = assignmentExpressionSyntax.Left; + } } var members = new List(); diff --git a/tests/Moq.Analyzers.Tests/Analyzers/SetupMustBeUsedOnlyForOverridableMembersAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/SetupMustBeUsedOnlyForOverridableMembersAnalyzerTest.cs index fe6bfe0..5216328 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/SetupMustBeUsedOnlyForOverridableMembersAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/SetupMustBeUsedOnlyForOverridableMembersAnalyzerTest.cs @@ -26,48 +26,84 @@ public void TestMethod() var mock1 = new Mock(); mock1.Setup(i => i.TestMethod()); mock1.Setup(i => i.TestProperty); + mock1.SetupSet(i => i.TestProperty = ""Foobard""); + mock1.SetupSet(i => i.TestProperty = ""Foobard""); mock1.Setup(i => i.InnerObject.VirtualMethod()); mock1.Setup(i => i.InnerObject.VirtualProperty); + mock1.SetupSet(i => i.InnerObject.VirtualProperty = ""Foobard""); + mock1.SetupSet(i => i.InnerObject.VirtualProperty = ""Foobard""); mock1.Setup(i => i.InnerObject.AbstractMethod()); mock1.Setup(i => i.InnerObject.AbstractProperty); + mock1.SetupSet(i => i.InnerObject.AbstractProperty = ""Foobard""); + mock1.SetupSet(i => i.InnerObject.AbstractProperty = ""Foobard""); var mock2 = new Mock(); mock2.Setup(i => i.VirtualMethod()); mock2.Setup(i => i.VirtualProperty); + mock2.SetupSet(i => i.VirtualProperty = ""Foobard""); + mock2.SetupSet(i => i.VirtualProperty = ""Foobard""); mock2.Setup(i => i.InnerObject.VirtualMethod()); mock2.Setup(i => i.InnerObject.VirtualProperty); + mock2.SetupSet(i => i.InnerObject.VirtualProperty = ""Foobard""); + mock2.SetupSet(i => i.InnerObject.VirtualProperty = ""Foobard""); mock2.Setup(i => i.InnerObject.AbstractMethod()); mock2.Setup(i => i.InnerObject.AbstractProperty); + mock2.SetupSet(i => i.InnerObject.AbstractProperty = ""Foobard""); + mock2.SetupSet(i => i.InnerObject.AbstractProperty = ""Foobard""); var mock3 = new Mock(); mock3.Setup(i => i.VirtualMethod()); mock3.Setup(i => i.VirtualProperty); + mock3.SetupSet(i => i.VirtualProperty = ""Foobard""); + mock3.SetupSet(i => i.VirtualProperty = ""Foobard""); mock3.Setup(i => i.AbstractMethod()); mock3.Setup(i => i.AbstractProperty); + mock3.SetupSet(i => i.InnerObject.AbstractProperty = ""Foobard""); + mock3.SetupSet(i => i.InnerObject.AbstractProperty = ""Foobard""); mock3.Setup(i => i.InnerObject.VirtualMethod()); mock3.Setup(i => i.InnerObject.VirtualProperty); + mock3.SetupSet(i => i.InnerObject.VirtualProperty = ""Foobard""); + mock3.SetupSet(i => i.InnerObject.VirtualProperty = ""Foobard""); mock3.Setup(i => i.InnerObject.AbstractMethod()); mock3.Setup(i => i.InnerObject.AbstractProperty); + mock3.SetupSet(i => i.InnerObject.AbstractProperty = ""Foobard""); + mock3.SetupSet(i => i.InnerObject.AbstractProperty = ""Foobard""); var mock4 = new Mock(); mock4.Setup(i => i.VirtualMethod()); mock4.Setup(i => i.VirtualProperty); + mock4.SetupSet(i => i.VirtualProperty = ""Foobard""); + mock4.SetupSet(i => i.VirtualProperty = ""Foobard""); mock4.Setup(i => i.AbstractMethod()); mock4.Setup(i => i.AbstractProperty); + mock4.SetupSet(i => i.InnerObject.AbstractProperty = ""Foobard""); + mock4.SetupSet(i => i.InnerObject.AbstractProperty = ""Foobard""); mock4.Setup(i => i.InnerObject.VirtualMethod()); mock4.Setup(i => i.InnerObject.VirtualProperty); + mock4.SetupSet(i => i.InnerObject.VirtualProperty = ""Foobard""); + mock4.SetupSet(i => i.InnerObject.VirtualProperty = ""Foobard""); mock4.Setup(i => i.InnerObject.AbstractMethod()); mock4.Setup(i => i.InnerObject.AbstractProperty); + mock4.SetupSet(i => i.InnerObject.AbstractProperty = ""Foobard""); + mock4.SetupSet(i => i.InnerObject.AbstractProperty = ""Foobard""); var mock5 = new Mock(); mock5.Setup(i => i.VirtualMethod()); mock5.Setup(i => i.VirtualProperty); + mock5.SetupSet(i => i.VirtualProperty = ""Foobard""); + mock5.SetupSet(i => i.VirtualProperty = ""Foobard""); mock5.Setup(i => i.AbstractMethod()); mock5.Setup(i => i.AbstractProperty); + mock5.SetupSet(i => i.InnerObject.AbstractProperty = ""Foobard""); + mock5.SetupSet(i => i.InnerObject.AbstractProperty = ""Foobard""); mock5.Setup(i => i.InnerObject.VirtualMethod()); mock5.Setup(i => i.InnerObject.VirtualProperty); + mock5.SetupSet(i => i.InnerObject.VirtualProperty = ""Foobard""); + mock5.SetupSet(i => i.InnerObject.VirtualProperty = ""Foobard""); mock5.Setup(i => i.InnerObject.AbstractMethod()); mock5.Setup(i => i.InnerObject.AbstractProperty); + mock5.SetupSet(i => i.InnerObject.AbstractProperty = ""Foobard""); + mock5.SetupSet(i => i.InnerObject.AbstractProperty = ""Foobard""); } } @@ -75,7 +111,7 @@ public interface I { int TestMethod(); - string TestProperty { get; } + string TestProperty { get; set; } InnerObject InnerObject { get; } } @@ -84,20 +120,20 @@ public class StandardClass { public virtual void VirtualMethod() { } - public virtual string VirtualProperty => null; + public virtual string VirtualProperty { get => null; set { } } - public virtual InnerObject InnerObject { get => null; } + public virtual InnerObject InnerObject { get => null; set { } } } public abstract class AbstractClass { public virtual void VirtualMethod() { } - public virtual string VirtualProperty => null; + public virtual string VirtualProperty { get => null; set { } } public abstract void AbstractMethod(); - public abstract string AbstractProperty { get; } + public abstract string AbstractProperty { get; set; } public abstract InnerObject InnerObject { get; } } @@ -106,11 +142,11 @@ public class InheritedFromAbstractClass : AbstractClass { public override void AbstractMethod() { } - public override string AbstractProperty => null; + public override string AbstractProperty { get => null; set { } } public override void VirtualMethod() { } - public override string VirtualProperty => null; + public override string VirtualProperty { get => null; set { } } public override InnerObject InnerObject => null; } @@ -119,7 +155,7 @@ public class InheritedFromAbstractClassDontOverrideVirtual : AbstractClass { public override void AbstractMethod() { } - public override string AbstractProperty => null; + public override string AbstractProperty { get => null; set { } } public override InnerObject InnerObject => null; } @@ -128,11 +164,11 @@ public abstract class InnerObject { public virtual void VirtualMethod() { } - public virtual string VirtualProperty => null; + public virtual string VirtualProperty { get => null; set { } } public abstract void AbstractMethod(); - public abstract string AbstractProperty { get; } + public abstract string AbstractProperty { get; set; } } }"; @@ -155,12 +191,20 @@ public void TestMethod() var mock1 = new Mock(); mock1.Setup(i => i.[|Method|]()); mock1.Setup(i => i.[|Property|]); + mock1.SetupSet(i => i.[|Property|] = ""1234""); + mock1.SetupSet(i => i.[|Property|] = ""1234""); mock1.Setup(i => i.[|InnerObject|].VirtualMethod()); mock1.Setup(i => i.[|InnerObject|].VirtualProperty); + mock1.SetupSet(i => i.[|InnerObject|].VirtualProperty = ""1234""); + mock1.SetupSet(i => i.[|InnerObject|].VirtualProperty = ""1234""); mock1.Setup(i => i.AbstractInnerObject.[|Method|]()); mock1.Setup(i => i.AbstractInnerObject.[|Property|]); + mock1.SetupSet(i => i.AbstractInnerObject.[|Property|] = ""1234""); + mock1.SetupSet(i => i.AbstractInnerObject.[|Property|] = ""1234""); mock1.Setup(i => i.[|SealedInnerObject|].VirtualMethod()); mock1.Setup(i => i.[|SealedInnerObject|].VirtualProperty); + mock1.SetupSet(i => i.[|SealedInnerObject|].VirtualProperty = ""1234""); + mock1.SetupSet(i => i.[|SealedInnerObject|].VirtualProperty = ""1234""); var mock2 = new Mock(); mock2.Setup(i => StandardClass.[|StaticMethod|]()); @@ -168,6 +212,8 @@ public void TestMethod() var mock3 = new Mock(); mock3.Setup(i => i.[|Method|]()); mock3.Setup(i => i.[|Property|]); + mock3.SetupSet(i => i.[|Property|] = ""1234""); + mock3.SetupSet(i => i.[|Property|] = ""1234""); var mock4 = new Mock(); mock4.Setup(i => i.[|ExtensionMethod|]()); @@ -178,7 +224,7 @@ public class StandardClass { public void Method() { } - public string Property => null; + public string Property { get => null; set { } } public static void StaticMethod() { } @@ -193,7 +239,7 @@ public abstract class AbstractClass { public void Method() { } - public string Property => null; + public string Property { get => null; set { } } } public interface I @@ -210,25 +256,25 @@ public class InnerObject { public virtual void VirtualMethod() { } - public virtual string VirtualProperty => null; + public virtual string VirtualProperty { get => null; set { } } } public abstract class AbstractInnerObject { public void Method() { } - public string Property { get => null; } + public string Property { get => null; set { } } public abstract void VirtualMethod(); - public abstract string VirtualProperty { get; } + public abstract string VirtualProperty { get; set; } } public sealed class SealedInnerObject : AbstractInnerObject { public override void VirtualMethod() { } - public override string VirtualProperty => null; + public override string VirtualProperty { get => null; set { } } } }"; From bd18baaee61247ec80002b15922f7dc0355a4853 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Fri, 25 Oct 2024 11:33:24 +0200 Subject: [PATCH 15/15] Add a fixer for the PosInfoMoq1005 rule. --- docs/Design/PosInfoMoq1005-Fixer.png | Bin 0 -> 47527 bytes docs/Design/PosInfoMoq1005.md | 6 + .../Analyzers/SetupSetAnalyzer.cs | 2 +- ...tGenericArgumentSetupSetCodeFixProvider.cs | 119 ++++++++++++++++++ ...ericArgumentSetupSetCodeFixProviderTest.cs | 76 +++++++++++ 5 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 docs/Design/PosInfoMoq1005-Fixer.png create mode 100644 src/Moq.Analyzers/CodeFixes/SetGenericArgumentSetupSetCodeFixProvider.cs create mode 100644 tests/Moq.Analyzers.Tests/CodeFixes/SetGenericArgumentSetupSetCodeFixProviderTest.cs diff --git a/docs/Design/PosInfoMoq1005-Fixer.png b/docs/Design/PosInfoMoq1005-Fixer.png new file mode 100644 index 0000000000000000000000000000000000000000..146458c47275905daa1f8194cd0215db043fa003 GIT binary patch literal 47527 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 z zZ04iZuZ?Q?<3d_=v80OQ(1m<#n;HF%J{uQcao(Rjksl#2{25A+pKD6uqVfCi8-hpP zlx6uI!4nm1HbgQ^?cQQJ%?FvZAk@fgnAr?#dmh|}yPq9tf`5c(Kg!cpF>wu}{lu6o zBB=QYz9);%*ls$2GbzneM-G2$t|2$U7e911?uUE!;Lg@7o5o@D{$py-1|ASdEB81C z@_G8A0~Wksu1gsnt4}a5jf-=_SutADx>}$ZJ>MpCw^V>_X7El08u(AnF!9pF)9-$0 zL&`6hwdmo7KWL#AbQ63%4;s9yegtvaJrC!C3=elqUCwe{{7*HkmO)&G%bD^DWj#x93{A_;j{l7 zh~36~Mix>|UaG++)PWabZUqt>xtC|z=j#DVBX^({C60TuI^o7%sa%zYI%^hNJgUPp z)g5Kqd5#$?zoN6`FTBurQm91^GgBZ%!@%CojMnZ@WKB0;IwTNvV4#$jS4C5Kt7q6T ztMW?+b>zB{^Fo`SF^KeV+2oVYt0s&iV(JdMV{&B_Kf*iNDcn|pLh_M_>hEr!_Y8aA zA*9H0C1>fC#(n(403l|Ls&vQ39I~%;$gY^&HddT!z%X9{9Iv>UYt9>O#5)Zud~FKPKl-VU(b_!I0O#PZQ2s zkbm)cU4*P*i)`WiwHz|JA}4x>7J7G-%A_<}3FN1&G@jGeF3IRf{}C4Y14wIK*YJ|1 zLIG8$2HvG7y8bu=WLGF-?4>&;0OH%;ceg6qa?PcP(qWR@WZRVa%(r=C!dH&QYt~09 z%}yz?psAXsfvaU?u4w?4H6#ZQQuzz$16devOX$CFR1}9g6AH(DYUKNUq=euiChwy- zC*$mQP-OW7nRhG}oqU^PKa!2x`8gfjk8PQdp;S6!UZx7qk{RY$vbe1Y=F>Ot_emUi zaf$4lH+5H51bcO#=1IvaXmo7M*4)vfCp%VAU){~Mk|GM;Iu<|9Dy^Zq(d$x;e6 zaM`RX(_%}wAxA+4l;aeC{S6`4LFdfrW?;rhuVdaNu+!b z>l05oV@r#5r>j8WG5mRs(xmRsbQ8wg_H@^Cho@FXKR*0W9LC=~*o^dT@yw2CUAi&X zALY)y{9TB?A=dU)(;hWZqSI}SEdMUnhBl-%Dtl?uliR!lz$~SfB`kv$c5giF=$y8h z;jI2K`0wTf#4)aN?X@cCgi8f+HGbOy`Ct@~=V}koSD|x1xk&&}{ve7Fil_9UNO3!{ z7kuEm9Aqn+O3Zmc?0>DVC)huU3x?w8sCb@8AJD>@)e4NuR0-M0XhR7=;D*uLYK7sa!Op&rZ_ zocGBWC@_D|?8np37{ym|H@eAxSsr=rv+~gs5b0E(noy+QZrP(WSu~kcc|Neop8|o6 z@+FDxpUs^{d0;aZHgd!KK`Ln$o9RXFFxOP~Rkm?YJdJ~~Y2b}5rQ2&il8n}+y(@s3 zr76bR$#-2F9W+t9ZlKFh?Fe!(b?i|c^|biO?C0{))A_L>e@Uv<@k)|NCBoGL(B}Lh ztpV^*GmUn4^QwG#4A;fsg7@zW(=XV8K0y_uy}6fft~07CwHMG1_6Mupa&EZS>k{R? zdrq#@>98~b$b-UGsh51Z`~NRfM{#V#urn-dEc$D5CM~513#b)xGd+Z!ymw6=3(wcB zGbFxnw2kM}wbJJTI##|^(1I1Nim|VBi_j2L(@1exVOvQEfBM-Ofdk+V06oni*OZMtk?r{+ zkg5c1cV^H^k(PxQ+`hw}RYMB2Kthhhdq27y2_Ad+YGKH}`K;V>i^-{KL+Y=L20p#| z;2z)~+5ZyY1gj+9flXzX`Z%(7yDEm=%H4daB@uT|#brv^se%Q+-htEA9?#4EA~jdN z`aFRzH_3w_t0^T^3W(y-{+`5xDa*K^a`XS*tKGj%NkJw|0l zfj*tHocA0_X{IuqqP6|q{e^T0wsNwYeSRv!dB0eYuY@W1lzz-|Xz!)vwdM%&cPurj zOrBjCfjxqkEvcms#Po%jc!f&8J`hN8?${|M_l;6Pf%V;p!)*)`iwW!zQadkO$6GIl zM4ozW=HziU_>y$3`t|wQhL3~=?jMbnI3-49@nKmf3?3bKguQVbL!Z<;s1oCbB)@p#I*(Mx;TwJSgVD|;c5!#ef>IB z0fpH`4b8nmkTa!{ESNG-w_IP7rrwq^T;{k^2xWy40g{xFke*{Ie z`Qh(y=PJ@3t4F11urhV0jC59YB3rXRSK)m^zW_tu(A+x!Sy95}Dd3Ko5()^Q%3?tz zv?jNG3n|O=3?G7P)7z8$06E~3k9ArMmSEaq4-qjZyG)47yPyD^90DCFP0!B@jJ{^^ zO*D0d#igOQU+#Y}CqVk_3alM4Sxa0Z6TIW>)+e9=vb$?K{0^oc&MJd`GI3WXpKE{_ z<=oOJIZDM_@UApe>U`XvL)T{_NK#~UL0R0+DXx zRFQmKxQp2Ji)e;UALI7tbZfDwi?Q0;U^lU6B8$yOiwV|rVrH3?*yV98bxmH#h@IiO zQ5iEV){s}ptFw@H)Zd*^qXppF+RYa_B~Vu%hcj3Aa=h1ajn4W}QnwQW9Zev4{w+;! zzMQ)>r`zC|u;aw>a1C!X^UPl5pnhM}8lpdf<_KAb)D4dnnfq)FeBt-5FbK^J)D)~J zqAd*Yq|AOuhnEc~@g9Wz@mBG`HHE+o>L@+O@NcwJH6ZBSq_UOWalaiO_xj-;Se|r7 z%y7g^IhHx2V5cJWzDeo8nyl%x&5InIMw4^&(rX0`%ZQ5oKJJ~8FD9zf7 zw@vkXNhH@ux>}VfGo<1{=PNKPQdY~zQuR+3y!FzVe_UJx81%eBF8Xbgr^e!&zOP@~ z+Pd}Qb6!u-?^Fm)<7H2v)e`Dy0BVT!hnM42J_Gg-tN8W)gwWj2+pg?UNS?jB2cGJr zaf#oC?bwk}F7FTQN81WyKCi^Yo4V_R$lY7rAUb9rg6x@Z%0$+FfzOjuQGPj7pL>$= zXE0NoPB&*TllzNPJkH7##viZ2WV^DWz!|4D?MGiekq&q&WX6zyd5o?kv`~{})Y@mU zk&)~+nPOKLtw>?cknvuS?B#RHllsG-^{pTuj8yy2g=CgKLkp$1)g$Xg=Y{COYA_C! z`ll6t>k=Z#3?6*Oq|swpN~SBKyP4KU8h9X^7-1FO4QbUQI<#lsmD8Qn_EaAHdo0}# zK!vLK5j>t1w?2*__L@o$Qp0Hzxf3Gum=??i5jhoZ^X#KFa2aXgvz=jiB@OXKK)-m? zh%Ckd@U>1=>4N;*o(pOhVCAL`>fzF3K4ki5@Jr#S*|Ssl#lKi+yOXUX+x6r*`VdGC zn(OX$Inks*6`mTyRqJ<*mXgvnT&NKZe!6T*EoE$g)={t`Lp3`nE8TlL&ck-7CZ_RF zA2wPOGCepkTs2zYf_s$n{P?vS`tY!jfyt}e2S5GDPJnZ#hm&1Do!fZ-Xi>c4flsy- zH14FzXHZfec4(@HDJrq@l^E(t*1Eqw8CC&5p+X;~Y7ZnHuT0No%A7FTi$I8SQhwdj zcU(a9qy=kC=UreSJyyKOYcV#ltiDjU0LC93nR?q_uh2^6-s@>`*2t5NLp@ST7m#`y zTfkOraAUg%UFuJPKqV-}TAGMHG=X}Qduz?`EUPv&0z_!2vN*vD0#~Sqcn|Sz>Q?#A z(k$!ZW%xMqVR0ePxBUB0Q~8*@T*&bp-Kft;U+Yf!_O1CagSoUT-#r%y`aj0t=5_{8 zV(XQ(Jr@>ZCxXq8pCg-xNN_STwtLBhEC(Y206t+xEnB5>Y-;;T@oyR~ z=nX4O50|n=L66^Ar45IqH!|rYG^+DEn5-nFWBs@EDwX;64yMuC#2|!)9yZl7GU?P$?yiSFxI}w(hONYIlTp z6GcH{)owGhv{v;*#Y9U0wbU25UX6jB$^(n8%<|XV=IZf%Vs$B1%jzPGV)0;}*F#jjqN(wqXN@|> z5{y$(9L;kr-Ks9=p)JkI_}%A|Kr{h{RNxw+^O}QNT<%L1F@B&=Ty}EYf}0D7VuQuw z)o-?Qw=4%Gq{X=Q9p&w;VAYZ=-qu&gCK&rbr{$h41ePa+PWdiYe-Y!cAJ&DP>V>=F zwKEQ9`ATP5DA+D?KH?DZcofA|{A}ulzOd3Y8zzSaJ$?>F4dgg~AMl_gCIi}zQQc+u zm}z`l2_EXHvBBXQvQ}~IzNiO} z7)>#8i({UCku^Z?Q&Qdd-Nve}*o}6X-@2+B>0(a%SLl<^e|61Dx-+}AF8lU##JkMm)>Zh`7QMNJ-(Y3&D8fyvQtm7=2!S zz7klf*-_k$ zqv^dCl)sin@%D8o8-Up8X7#bUd$-E8$RpFj=Nzr*)M6sssm5a>HukHjgP3S|<8x4c z;YX}-(2@Lok-fPr_5BSW9(Wh#%Li6JfVkZ`(x~1W@+i{UGY(iRIwUuNt1AmH-z(}< z8t@pwSG;Kdakc#v=xMaHQjSPgcnI5et2+=npljYv^V)_@9#=Sr_?)awv}jYgKY49O zXZ>#55!U$1jan31NW@7dR+8^tDw7GZ)+LTRNxy!qB4gC7y(I&*h$S_TbjRGEl|VLm zo|d|IxPzj&9P^g)j6*Hz%IvR;lXKf5t53GZ?NH`rwQ($zlqB|^*JeK^=Jq|kmeU$2 zvRce*vD>k?f;)L?_+XdHH;Zxn=7rJs@=DdueYzw1(-!TLqtJ1j9z?@S0ov~Bv z9_vZ6U0M9HYe&#FN7LFZ3#wV>ZF<`j_QeQ^S_@MtYxYO-XpZDoa%PRNuI$%%kc&Z1zxpNzHh=_6WaQ& z9&|q%d=i-hD_Kz(cw5UB(2v&rYcCVE1hFUWwbL`Xe5Xa)CvATePJ+^3^sr_PIQFOu zBc=95MOJvZytmex{GKX1-Sts{8L z^6i~)NbOEFW)WbYZSHTakD!}=tptUJV{pe#WoBW~&g#(ni5PP__>=0A1CuZ1N#`Ju z4NeV!L>l5{#S3n_1*^q&_9woO)rL*9C-!$N$phh)uf9oxe^V#@7`vWvcz62xRg#e( z9yBL?iN8APyLp7jsAf;2F2zAV)z+&4r;yNLmD{q|i(($q@f}5V7}p^kyri0^{~4oV zlGr1&YdaM~I822#reyeidZ8WFsh1y$-o3j9RAKg56T_B}J?( zB%`^YK<};z>lGo^vA-9YPSNSKHuRO&;=9t5{V7)Zy<0J4Yy)$vzaaRUOkm%THj@s#J&RFr!$sTjvnM^vBHp}PSN4;+k}U= zw{f<1_hADxL(s7=!AuaFm*TL7?dt&UN595#3aZ%hA3iShHqi%Fu=n0W2JQAX=0;uT z{5-I6Dz>|E5%+O9*!S1e3tuo%Li_l|scKYS7j2XQ`2isoT9jU|*{A?puLf5!h+{J|JrzPzLQakCBo$i72uY>r)g z=kR3Fe9}|e;@Qpn7TtvI2$kkhns9DL;>-6lFF5uHf6Die?C z`MkW1;f+GckQ?1yt>w_(;|e_9JoGENqCpEA&2+yQmr(762va@$QRxAA76&&f?$yIL zYtk0-v>TmU9|t5|)!;F2eO|Y*|9ZRcfJ`fmpC;XNn}m-;%w){4eRo*lC?=|g+rw*A zyhk7v`}d5P*0EfAbJlmhPpBxaghY=-hmlRy^h#fb*S-?3rK>dClC$}tFWpB?=xcMnMoT@9N;&Dk(eJsQvy#wKamIVQscqRu`zdHG=>Fq9 z{`Hi6=LRCA?rfXTa;E}CyOoX20x4L$L^`4^%-GdsXN1LJjIyZ3bw~PB@WcGnL6WK#nCMG)jaO1(ndRVUG?IibXsgzsNE^h^n>jHmj%a-hu=+-949$D z$E%(f@~FBSRYlg+=wff}p@Q9XT)B#r`Qp+?b7KgbSN^a_wV!T$63MrB%=6M`wGtsM zV`M1smjCY-a+xHn%2ljtzxR?<)b+*4Mo%nbnmQd^x0k%fQY@Hue65Ap$zcUrK8B|1 zXNzq??e#JE4UCr+&JR$a_l0ILhs*b8I^|%o&eQ3q^H`>_&q9wj295XM`uPUVP|U$E z^cp~9l!fL=chimknNfBA1g)fZZKQ}UPl}l6`K=kG1D=BL3bBntfNBatwLp#v*8H5n z{yES{jlOYPiaLsGCmNA2;QLT6lcX&fVDolGu&=Lu6AeQ(n5_ zT4VTpYG*&&q~I+6Ta#hc+4^$aHLK0Kw6&Y%Pbi}cG^~QU71P9|;G~=Yui|7{9uiF(ImivddTa~mBw|4oe5ub+tH9ht}c)MdYk~-gc?y_ER-PF zG4PyM{ic0;et>*<$#2hI*lLjZ|7D99oJ>pDq3Ge+rwL}p6XmonVQ@u}V)l1d`K$Kw zm^j@r4DA7=FMkrwp&4wZt?o$H8~m`&MhmKHNy<1r zF^kt2Sg+0ZvP|YL_@dH+W9sFP#h&`!Rh3^G{DZ`8Usq0WrQULG!e6;Jt0S~#mqyY1 zNJidg;Z>-g)Z~JF@e|QU5v@iYK7F0^dbw3=Q?;xc)uGITP8iutmdWPn*DEaa&9tVZ zxlJdsf!z5hK!!2K{vJ~r`T(bvXeIjzuvhOvDDdP8@51TIY#LJp^_B#$Yv4Qs+F-pK)Culpv zGdOGfnHxY^taCx2H>d!Zlct>9VmsSm+ifo-F2l>B3zaQ{wEc^bX1Z+3pzG}oixLA? z0qB_Sek5Uy_m>g?!kDsuomoMMIBG3(HQlURbU;30PiWqKl{?@QiXYf$Wzm@1525b< zs`c`c@ykn6NQnR4rXGDc!TLza!5;y)PT{GsssWH{r0G3(!lPPXX54+|@1wDbdJN}k zQ&)1vtu}0XtdlXW$GtRVtZW5`x=LI6e5to|iRy+PCZCY^g0@mxj+H zL+PY1S<1di{;M}z7ev+>t~S_Gk#%7bHwO?o`;^NJE6IzQ7HO`cydWvx|2DXM$r^Lo zt-jBu0>oK$qDl(Rvi?0l|MA53p`CUb?wsO!NQH=>sj>&0Of-{lPFoQF%!(>HHn&UC z>qR3PQu%4Bn{9;eVuJav^*)iT1%>bVA!jJ*%}J7Q{V{gP;QQ=DobZefhtf|NO@vbf z0B^M&Wg&#pz|6kDt@6e6dW#!7n7~{6y1cuJoDple_6tWt0Sza(VB0(D)Cp~emZI1( z-sSKzHkpaCFA+h5+1q)mTAya$so^A-ktnAKvY8(f*DXq}#J!m>cfogcUNh3v{~|`S zT20B}AwJGnWtGJuO$hy=&bC)y-qgd{5LTPLpXz#?k=aw|y&J=>@35J2M>^EgsM1Sea7EtS8 zY>R=P8GvM~z99!aDHnyqh{yZYKl^R6@p4SGaP1Wrzzb7{Em6DvDMlL-L0Pu_?KUiT zW+X?>4z^t&XR*ow*W;(z-o7B!7Vu&mTAEYmPw`fb*e&se*-f|1g3GcDzm7!4jvVEcv^#l7F5 z6c)FY=OBhk_lSRw?LWsOw_X?802Pkr6xq{~;YG;A)yn)iNr(dHfc09Fx0z<;``@ zTdm`g<-}q@Xe zv>441pkHoy=&_`{z{mayw zM32ck-$Cu**UfQ=>a5G+Y-6~IS9#-cM|j5mzKqa+g8(+4%owl=ldrdCcf9;|A8Kz9B`-M8ro|Xub=6ntDZovze3f;*>I?pFhL?VkuYGaQS#o!io^^ z*qaZ3OUr;6^7)4H(72D^Bs6Puv+lm)7}RZV7@fOtGu>HvB>i@}60i2+Di2d?ZGO>` z{3_n1(azLZZfzzsVF!5Y0n@NkV;>gGThd z=L_-`5TlM!(;;31#uFwS%}AhrUm#Y2;T8Dxw}F%_Au&D+eDnf4jYu93~GL-QU+ek7Coqi zQz3G{$u4Usi`j~OH1Nwd5Izxak$Y6ok&=aimMeVgu#Rmt$g$!`&LxyIB6g9JV0_x& zgZ1yCKs&j$UiP2Omwv)L2&IAUcjMF=!Hc5bl=HOfWie1lMJer}qAHZ!p z#=FlrUv?>-A&GkTy&V8b7IVdQ;OM1&qMqk9RI<`_#sSkGu|8`O7LQGBcodnMCUmKx zo|P5T1_^54C%YZVON?{8x>?6}`_)NpBbz$Vvi zT4G9l-D1sj;~Tx8S2MQ`nX&hSuaPs+*bj{x7w~7ka7!X-GfTb-d=Xpz4uX=N457YR zyghW%#EiDeiw7)E#z!<%TgoYu4VSA`=^{k!IZW4nyx5SSP&vf}Eu@9;y3w_o%w~d; zD+Gd3Vx-BOi2)*!Cr*G2Nn}d1z}FAzFm{p9Bi(nkx>=E)phv#Cu%uLJgoS*&etoA zui;&;4ydN=`gcY6=sh)srbz%Laj8G^Je=puV-2{3vYy0Ae@+KdUY?cMq&P`?I@z>5}21)<07QDj fQtQ6u(cc)y6F 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 literal 0 HcmV?d00001 diff --git a/docs/Design/PosInfoMoq1005.md b/docs/Design/PosInfoMoq1005.md index 5d9c9b8..8a0d286 100644 --- a/docs/Design/PosInfoMoq1005.md +++ b/docs/Design/PosInfoMoq1005.md @@ -80,6 +80,12 @@ public void SetNameOfCustomer() To fix a violation of this rule, use the `SetupSet()` method with the type of the mocked property as the generic argument. +### Visual Studio fixer +A Visual Studio fixer exists to set explicitly the generic argument of the `SetupSet()` method with the property type +in the current document, project or solution. + +![Visual Studio rule fixer](PosInfoMoq1005-Fixer.png) + ## When to suppress warnings Do not suppress a warning from this rule. Using the `SetupSet()` method ensures that the delegate argument in the `Callback()` diff --git a/src/Moq.Analyzers/Analyzers/SetupSetAnalyzer.cs b/src/Moq.Analyzers/Analyzers/SetupSetAnalyzer.cs index 762b787..9aa6ac1 100644 --- a/src/Moq.Analyzers/Analyzers/SetupSetAnalyzer.cs +++ b/src/Moq.Analyzers/Analyzers/SetupSetAnalyzer.cs @@ -15,7 +15,7 @@ namespace PosInformatique.Moq.Analyzers [DiagnosticAnalyzer(LanguageNames.CSharp)] public class SetupSetAnalyzer : DiagnosticAnalyzer { - private static readonly DiagnosticDescriptor UseSetupSetWithGenericArgumentRule = new DiagnosticDescriptor( + internal static readonly DiagnosticDescriptor UseSetupSetWithGenericArgumentRule = new DiagnosticDescriptor( "PosInfoMoq1005", "Defines the generic argument of the SetupSet() method with the type of the mocked property", "Defines the generic argument of the SetupSet() method with the type of the mocked property", diff --git a/src/Moq.Analyzers/CodeFixes/SetGenericArgumentSetupSetCodeFixProvider.cs b/src/Moq.Analyzers/CodeFixes/SetGenericArgumentSetupSetCodeFixProvider.cs new file mode 100644 index 0000000..d911756 --- /dev/null +++ b/src/Moq.Analyzers/CodeFixes/SetGenericArgumentSetupSetCodeFixProvider.cs @@ -0,0 +1,119 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Moq.Analyzers +{ + 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; + + [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SetGenericArgumentSetupSetCodeFixProvider))] + [Shared] + public class SetGenericArgumentSetupSetCodeFixProvider : CodeFixProvider + { + public sealed override ImmutableArray FixableDiagnosticIds + { + get { return ImmutableArray.Create(SetupSetAnalyzer.UseSetupSetWithGenericArgumentRule.Id); } + } + + 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); + + if (root is null) + { + return; + } + + // Gets the location where is the issue in the code. + var diagnostic = context.Diagnostics.First(); + var diagnosticSpan = diagnostic.Location.SourceSpan; + + // Gets the syntax node where is located the issue in the code. + var node = root.FindNode(diagnosticSpan, getInnermostNodeForTie: true); + + if (node is not IdentifierNameSyntax identifierNameSyntax) + { + return; + } + + context.RegisterCodeFix( + CodeAction.Create( + title: "Set the generic argument with the mocked property type to the SetupSet() method.", + createChangedDocument: cancellationToken => AddMockBehiavorStrictArgumentAsync(context.Document, identifierNameSyntax, cancellationToken), + equivalenceKey: "Set the generic argument with the mocked property type to the SetupSet() method."), + diagnostic); + + return; + } + + private static async Task AddMockBehiavorStrictArgumentAsync(Document document, IdentifierNameSyntax oldIdentifierNameSyntax, CancellationToken cancellationToken) + { + var semanticModel = await document.GetSemanticModelAsync(); + + if (semanticModel is null) + { + return document; + } + + var moqSymbols = MoqSymbols.FromCompilation(semanticModel.Compilation); + + if (moqSymbols is null) + { + return document; + } + + // Retrieve the invocation expression + if (oldIdentifierNameSyntax.Parent is not MemberAccessExpressionSyntax memberAccessExpressionSyntax) + { + return document; + } + + if (memberAccessExpressionSyntax.Parent is not InvocationExpressionSyntax invocationExpressionSyntax) + { + return document; + } + + // Gets the chained members from the lambda expression (to determine the type of the last property in the members chain). + var expressionAnalyzer = new MoqExpressionAnalyzer(moqSymbols, semanticModel); + + var chainedMembers = expressionAnalyzer.ExtractChainedMembersInvocationFromLambdaExpression(invocationExpressionSyntax, cancellationToken); + + if (chainedMembers is null) + { + return document; + } + + // Update the IdentifierNameSyntax with a GenericName (which contains the type of the property). + var propertyType = SyntaxFactory.ParseTypeName(chainedMembers.ReturnType.ToDisplayString()); + + var newIdentifierNameSyntax = SyntaxFactory.GenericName( + oldIdentifierNameSyntax.Identifier, + SyntaxFactory.TypeArgumentList( + SyntaxFactory.SingletonSeparatedList(propertyType))); + + var oldRoot = await document.GetSyntaxRootAsync(cancellationToken); + + if (oldRoot is null) + { + return document; + } + + var newRoot = oldRoot.ReplaceNode(oldIdentifierNameSyntax, newIdentifierNameSyntax); + + return document.WithSyntaxRoot(newRoot); + } + } +} diff --git a/tests/Moq.Analyzers.Tests/CodeFixes/SetGenericArgumentSetupSetCodeFixProviderTest.cs b/tests/Moq.Analyzers.Tests/CodeFixes/SetGenericArgumentSetupSetCodeFixProviderTest.cs new file mode 100644 index 0000000..4c30477 --- /dev/null +++ b/tests/Moq.Analyzers.Tests/CodeFixes/SetGenericArgumentSetupSetCodeFixProviderTest.cs @@ -0,0 +1,76 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Moq.Analyzers.Tests +{ + using Verifier = MoqCSharpCodeFixVerifier; + + public class SetGenericArgumentSetupSetCodeFixProviderTest + { + [Fact] + public async Task SetupSet_Fix() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + using System; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(); + mock1.[|SetupSet|](i => i.TestPropertyInt32 = 1234); + mock1.[|SetupSet|](i => i.TestPropertyString = ""Foobard""); + + // No changes + mock1.SetupSet(i => i.TestPropertyInt32 = 1234); + mock1.SetupSet(i => i.TestPropertyString = ""Foobard""); + } + } + + public interface I + { + int TestPropertyInt32 { get; set; } + + string TestPropertyString { get; set; } + } + }"; + + var expectedFixedSource = + @" + namespace ConsoleApplication1 + { + using Moq; + using System; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(); + mock1.SetupSet(i => i.TestPropertyInt32 = 1234); + mock1.SetupSet(i => i.TestPropertyString = ""Foobard""); + + // No changes + mock1.SetupSet(i => i.TestPropertyInt32 = 1234); + mock1.SetupSet(i => i.TestPropertyString = ""Foobard""); + } + } + + public interface I + { + int TestPropertyInt32 { get; set; } + + string TestPropertyString { get; set; } + } + }"; + + await Verifier.VerifyCodeFixAsync(source, expectedFixedSource); + } + } +}