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) diff --git a/PosInformatique.Moq.Analyzers.sln b/PosInformatique.Moq.Analyzers.sln index c4ca56b..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}" @@ -51,6 +52,9 @@ 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 + 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 f84a756..eb45187 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 @@ -40,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. | @@ -53,6 +54,9 @@ 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. | +| [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/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/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/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/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/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/docs/Design/PosInfoMoq1005-Fixer.png b/docs/Design/PosInfoMoq1005-Fixer.png new file mode 100644 index 0000000..146458c Binary files /dev/null and b/docs/Design/PosInfoMoq1005-Fixer.png differ diff --git a/docs/Design/PosInfoMoq1005.md b/docs/Design/PosInfoMoq1005.md new file mode 100644 index 0000000..8a0d286 --- /dev/null +++ b/docs/Design/PosInfoMoq1005.md @@ -0,0 +1,92 @@ +# 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. + +### 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()` +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 2111b71..35d43d6 100644 --- a/src/Moq.Analyzers/AnalyzerReleases.Shipped.md +++ b/src/Moq.Analyzers/AnalyzerReleases.Shipped.md @@ -1,4 +1,14 @@ -## Release 1.10.0 +## Release 1.11.0 + +### New Rules +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +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. + +## 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/Analyzers/ConstructorArgumentCannotBePassedForInterfaceAnalyzer.cs b/src/Moq.Analyzers/Analyzers/ConstructorArgumentCannotBePassedForInterfaceAnalyzer.cs deleted file mode 100644 index 8a31e27..0000000 --- a/src/Moq.Analyzers/Analyzers/ConstructorArgumentCannotBePassedForInterfaceAnalyzer.cs +++ /dev/null @@ -1,100 +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; - } - - 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(); - - var diagnostic = Diagnostic.Create(Rule, locations[0], locations.Skip(1)); - context.ReportDiagnostic(diagnostic); - - return; - } - } - } -} diff --git a/src/Moq.Analyzers/Analyzers/ConstructorArgumentsMustMatchAnalyzer.cs b/src/Moq.Analyzers/Analyzers/ConstructorArgumentsAnalyzer.cs similarity index 72% rename from src/Moq.Analyzers/Analyzers/ConstructorArgumentsMustMatchAnalyzer.cs rename to src/Moq.Analyzers/Analyzers/ConstructorArgumentsAnalyzer.cs index 45d4bb7..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,8 +14,18 @@ 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", + "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", @@ -36,7 +46,21 @@ 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( + ConstructorArgumentsCanBePassedToInterfaceRule, + ConstructorArgumentsMustMatchMockedClassRule, + ConstructorMockedClassMustBeAccessibleRule, + ConstructorWithLambdaExpressionCanBeUseWithClassesOnlyRule); public override void Initialize(AnalysisContext context) { @@ -60,20 +84,29 @@ 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 the type is mockable - if (!moqSymbols.IsMockable(mockedType)) + // 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 a class (other type are ignored) - if (mockedType.TypeKind != TypeKind.Class) + // Check the type is mockable + if (!moqSymbols.IsMockable(mockedType)) { return; } @@ -103,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/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/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); 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/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/Analyzers/SetupSetAnalyzer.cs b/src/Moq.Analyzers/Analyzers/SetupSetAnalyzer.cs new file mode 100644 index 0000000..9aa6ac1 --- /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 + { + 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", + "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"); + + 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 SetupSet() method. + if (!moqSymbols.IsSetupSetMethod(methodSymbol.Symbol)) + { + return; + } + + // Check is SetupSet() method. + if (!moqSymbols.IsSetupSetMethodWithoutGenericArgument(methodSymbol.Symbol)) + { + var nameSyntax = ((MemberAccessExpressionSyntax)invocationExpression.Expression).Name; + + context.ReportDiagnostic(UseSetupSetWithGenericArgumentRule, nameSyntax.GetLocation()); + + return; + } + } + } +} diff --git a/src/Moq.Analyzers/CodeFixes/SetBehaviorToStrictCodeFixProvider.cs b/src/Moq.Analyzers/CodeFixes/SetBehaviorToStrictCodeFixProvider.cs index a54aabd..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; } } @@ -86,16 +87,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 +188,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/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/src/Moq.Analyzers/Moq.Analyzers.csproj b/src/Moq.Analyzers/Moq.Analyzers.csproj index a2d0d4d..99b7406 100644 --- a/src/Moq.Analyzers/Moq.Analyzers.csproj +++ b/src/Moq.Analyzers/Moq.Analyzers.csproj @@ -17,10 +17,22 @@ https://github.com/PosInformatique/PosInformatique.Moq.Analyzers README.md + 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. + - 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. + 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/MoqExpressionAnalyzer.cs b/src/Moq.Analyzers/MoqExpressionAnalyzer.cs index f179432..7183f3f 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) - { - 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)) + // Check that the "Mock.Of()" statement have at least one argument (else Strict is missing...). + if (argumentList is null) { return false; } - // Check that the memberAccessExpression.Name reference the Strict field - var firstArgumentField = this.semanticModel.GetSymbolInfo(memberAccessExpression!.Name, cancellationToken); + var lastArgument = argumentList.Arguments.LastOrDefault(); - 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; } @@ -391,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(); @@ -560,5 +543,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..818d846 100644 --- a/src/Moq.Analyzers/MoqSymbols.cs +++ b/src/Moq.Analyzers/MoqSymbols.cs @@ -41,6 +41,12 @@ internal sealed class MoqSymbols private readonly Lazy verifiesInterface; + private readonly Lazy mockConstructorWithFactory; + + private readonly Lazy setupSetMethodWithoutGenericArgument; + + private readonly Lazy> setupSetMethods; + private MoqSymbols(INamedTypeSymbol mockGenericClass, Compilation compilation) { this.mockGenericClass = mockGenericClass; @@ -64,6 +70,11 @@ 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")); + + 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) @@ -158,6 +169,41 @@ public bool IsSetupProtectedMethod(ISymbol? symbol) return false; } + public bool IsSetupSetMethod(ISymbol? symbol) + { + if (symbol is null) + { + return false; + } + + var originalDefinition = symbol.OriginalDefinition; + + foreach (var setupSetMethod in this.setupSetMethods.Value) + { + if (SymbolEqualityComparer.Default.Equals(originalDefinition, setupSetMethod)) + { + return true; + } + } + + 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) @@ -396,6 +442,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/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] diff --git a/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentCannotBePassedForInterfaceAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentCannotBePassedForInterfaceAnalyzerTest.cs deleted file mode 100644 index 40dc526..0000000 --- a/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentCannotBePassedForInterfaceAnalyzerTest.cs +++ /dev/null @@ -1,174 +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")); - } - - [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/ConstructorArgumentsAnalyzerTest.cs similarity index 75% rename from tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentsMustMatchAnalyzerTest.cs rename to tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentsAnalyzerTest.cs index b053a46..447bdfc 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentsMustMatchAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentsAnalyzerTest.cs @@ -1,14 +1,15 @@ //----------------------------------------------------------------------- -// +// // Copyright (c) P.O.S Informatique. All rights reserved. // //----------------------------------------------------------------------- namespace PosInformatique.Moq.Analyzers.Tests { - using Verifier = MoqCSharpAnalyzerVerifier; + using Microsoft.CodeAnalysis.Testing; + using Verifier = MoqCSharpAnalyzerVerifier; - public class ConstructorArgumentsMustMatchAnalyzerTest + public class ConstructorArgumentsAnalyzerTest { [Fact] public async Task NoMock() @@ -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(ConstructorArgumentsAnalyzer.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(ConstructorArgumentsAnalyzer.ConstructorArgumentsCanBePassedToInterfaceRule) + .WithLocation(0).WithArguments("1") + .WithLocation(1).WithArguments("2")); + } + [Fact] public async Task Arguments_Empty() { @@ -523,6 +626,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() { 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/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 { } } } }"; 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() { diff --git a/tests/Moq.Analyzers.Tests/Analyzers/SetupSetAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/SetupSetAnalyzerTest.cs new file mode 100644 index 0000000..f8868cb --- /dev/null +++ b/tests/Moq.Analyzers.Tests/Analyzers/SetupSetAnalyzerTest.cs @@ -0,0 +1,108 @@ +//----------------------------------------------------------------------- +// +// 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 + + var s = ""The string""; + s.ToString(); // Invocation should be ignored + } + } + + 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); + + var s = ""The string""; + s.ToString(); // Invocation should be ignored + } + } + + 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 diff --git a/tests/Moq.Analyzers.Tests/CodeFixes/SetBehaviorToStrictCodeFixProviderTest.cs b/tests/Moq.Analyzers.Tests/CodeFixes/SetBehaviorToStrictCodeFixProviderTest.cs index 38bedaa..70ec544 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,8 +200,13 @@ public interface I await Verifier.VerifyCodeFixAsync(source, expectedFixedSource); } - [Fact] - public async Task MockOf_Fix_MissingBehavior() + [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_WithArguments(string arguments, string expectedArguments) { var source = @" namespace ConsoleApplication1 @@ -205,7 +217,7 @@ public class TestClass { public void TestMethod() { - var mock1 = [|Mock.Of()|]; + var mock = [|Mock.Of(" + arguments + @")|]; } } @@ -224,7 +236,7 @@ public class TestClass { public void TestMethod() { - var mock1 = Mock.Of(MockBehavior.Strict); + var mock = Mock.Of(" + expectedArguments + @"); } } @@ -237,12 +249,10 @@ public interface I } [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) + [InlineData("")] + [InlineData("MockBehavior.Loose")] + [InlineData("MockBehavior.Default")] + public async Task MockOf_InConstructor_Fix(string behavior) { var source = @" namespace ConsoleApplication1 @@ -253,13 +263,20 @@ public class TestClass { public void TestMethod() { - var mock = [|Mock.Of(" + arguments + @")|]; + var mock = new C([|Mock.Of(" + behavior + @")|], [|Mock.Of(" + behavior + @")|]); } } public interface I { } + + public class C + { + public C(I r1, I r2) + { + } + } }"; var expectedFixedSource = @@ -272,13 +289,20 @@ public class TestClass { public void TestMethod() { - var mock = Mock.Of(" + expectedArguments + @"); + 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); 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); + } + } +}