diff --git a/.github/workflows/github-actions-release.yml b/.github/workflows/github-actions-release.yml index c05370a..1c0a449 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.13.0 + default: 2.0.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 9aed5d6..68f7953 100644 --- a/PosInformatique.Moq.Analyzers.sln +++ b/PosInformatique.Moq.Analyzers.sln @@ -38,6 +38,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Design", "Design", "{815BE8 docs\Design\PosInfoMoq1007.md = docs\Design\PosInfoMoq1007.md docs\Design\PosInfoMoq1008.md = docs\Design\PosInfoMoq1008.md docs\Design\PosInfoMoq1009.md = docs\Design\PosInfoMoq1009.md + docs\Design\PosInfoMoq1010.md = docs\Design\PosInfoMoq1010.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Compilation", "Compilation", "{D9C84D36-7F9C-4EFB-BE6F-9F7A05FE957D}" @@ -59,6 +60,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Compilation", "Compilation" docs\Compilation\PosInfoMoq2014.md = docs\Compilation\PosInfoMoq2014.md docs\Compilation\PosInfoMoq2015.md = docs\Compilation\PosInfoMoq2015.md docs\Compilation\PosInfoMoq2016.md = docs\Compilation\PosInfoMoq2016.md + docs\Compilation\PosInfoMoq2017.md = docs\Compilation\PosInfoMoq2017.md + docs\Compilation\PosInfoMoq2018.md = docs\Compilation\PosInfoMoq2018.md + docs\Compilation\PosInfoMoq2019.md = docs\Compilation\PosInfoMoq2019.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}" @@ -71,6 +75,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ .github\workflows\github-actions-release.yml = .github\workflows\github-actions-release.yml EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" + ProjectSection(SolutionItems) = preProject + src\.editorconfig = src\.editorconfig + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/README.md b/README.md index f93ff02..caeb6d7 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,8 @@ Design rules used to make your unit tests more strongly strict. | [PosInfoMoq1006: The `It.IsAny()` or `It.Is()` arguments must match the parameters of the mocked method.](docs/Design/PosInfoMoq1006.md) | When setting up a method using `It.IsAny()` or `It.Is()` as arguments, the type `T` must exactly match the parameters of the configured method. | | [PosInfoMoq1007: The `Verify()` method must specify the `Times` argument.](docs/Design/PosInfoMoq1007.md) | When calling the `Verify()` method, if the `Times` argument is not specified, Moq will assume `Times.AtLeastOnce()` by default. | | [PosInfoMoq1008: The `Mock.Verify()` and `Mock.VerifyAll()` methods must specify at least one mock.](docs/Design/PosInfoMoq1008.md) | When calling the static methods `Mock.Verify()` or `Mock.VerifyAll()` without providing any `Mock` instances, no verification is performed. | -| [PosInfoMoq1009: Avoid using `Verifiable()` method](docs/Design/PosInfoMoq1008.md) | A `Verify()` of an `Mock` instance has not been called in the *Assert* phase of an unit test for `Verifiable()` setups. | +| [PosInfoMoq1009: Avoid using `Verifiable()` method](docs/Design/PosInfoMoq1009.md) | A `Verify()` of an `Mock` instance has not been called in the *Assert* phase of an unit test for `Verifiable()` setups. | +| [PosInfoMoq1010: Use `+= null` syntax when raising events with `Raise()`/`RaiseAsync()`](docs/Design/PosInfoMoq1010.md) | When using `Mock.Raise()` or `Mock.RaiseAsync()`, the lambda expression used to identify the event should consistently use the `+= null` syntax. | ### Compilation @@ -58,7 +59,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()`/`SetupSet()` 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()`/`SetupSequence()`/`SetupSet()` method must be used only on overridable members](docs/Compilation/PosInfoMoq2001.md)) | The `Setup()`/`SetupSequence()`/`SetupSet()` 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. | @@ -74,6 +75,7 @@ All the rules of this category should not be disabled (or changed their severity | [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. | - - +| [PosInfoMoq2017: `Mock.Raise()`/`RaiseAsync()` must use parameters matching the event signature.](docs/Compilation/PosInfoMoq2017.md) | The parameters passed to `Raise()` or `RaiseAsync()` must exactly match the parameters of the corresponding event delegate. | +| [PosInfoMoq2018: The first parameter of `Raise()`/`RaiseAsync()` must be an event.](docs/Compilation/PosInfoMoq2018.md) | The first parameter passed to `Raise()` or `RaiseAsync()` must reference an **event** member of the mocked type. | +| [PosInfoMoq2019: `RaiseAsync()` must be used only for events with async handlers (returning `Task`).](docs/Compilation/PosInfoMoq2019.md) | `Mock.RaiseAsync()` must only be used with events whose delegate type returns `Task` (i.e., async events). | diff --git a/docs/Compilation/PosInfoMoq2001.md b/docs/Compilation/PosInfoMoq2001.md index 9f98a05..448e202 100644 --- a/docs/Compilation/PosInfoMoq2001.md +++ b/docs/Compilation/PosInfoMoq2001.md @@ -1,15 +1,15 @@ -# PosInfoMoq2001: The `Setup()`/`SetupSet()` method must be used only on overridable members +# PosInfoMoq2001: The `Setup()`/`SetupSequence()`/`SetupSet()` method must be used only on overridable members | Property | Value | |-------------------------------------|---------------------------------------------------------------| | **Rule ID** | PosInfoMoq2001 | -| **Title** | The `Setup()`/`SetupSet()` method must be used only on overridable members | +| **Title** | The `Setup()`/`SetupSequence()`/`SetupSet()` method must be used only on overridable members | | **Category** | Compilation | | **Default severity** | Error | ## Cause -The `Setup()` or `SetupSet()` methods must be applied only for overridable members. +The `Setup()`, `SetupSequence()` 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: @@ -18,10 +18,10 @@ An overridable member is a **method** or **property** which is in: ## Rule description -The `Setup()` method must be applied only for overridable members. +The `Setup()`, `SetupSequence()` and `SetupSet()` methods must be applied only for overridable members. For example: -- The following methods and properties can be mock and used in the `Setup()` method: +- The following methods and properties can be mock and used in the `Setup()` and `SetupSequence()` method: - `IService.MethodCanBeMocked()` - `IService.PropertyCanBeMocked` - `Service.VirtualMethodCanBeMocked` @@ -58,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()` or `SetupSet()` method which can be overriden. +To fix a violation of this rule, be sure to mock a member in the `Setup()`, `SetupSequence()` or `SetupSet()` method which can be overriden. ## When to suppress warnings diff --git a/docs/Compilation/PosInfoMoq2017.md b/docs/Compilation/PosInfoMoq2017.md new file mode 100644 index 0000000..20b47f7 --- /dev/null +++ b/docs/Compilation/PosInfoMoq2017.md @@ -0,0 +1,91 @@ +# PosInfoMoq2017: `Mock.Raise()`/`RaiseAsync()` must use parameters matching the event signature + +| Property | Value | +|-----------------------|----------------------------------------------------------------------| +| **Rule ID** | PosInfoMoq2017 | +| **Title** | `Mock.Raise()`/`RaiseAsync()` must use parameters matching the event signature | +| **Category** | Compilation | +| **Default severity** | Error | + +## Cause + +The parameters passed to `Raise()` or `RaiseAsync()` must exactly match the parameters of the corresponding event delegate. + +## Rule description + +When mocking events with **Moq**, calling `Mock.Raise()` or `Mock.RaiseAsync()` requires that the provided arguments match the event signature. + +- With the **`Raise(event, EventArgs)`** overload: + - Used for `EventHandler` or `EventHandler`. + - Moq automatically supplies the `sender` (the mocked object). + - You only pass the `EventArgs` (or derived class) instance. + +- With the **`Raise(event, params object[])`** overload: + - You provide all the arguments of the event delegate yourself. + - This includes the `sender` and all additional event arguments. + +### Example + +```csharp +public class Service +{ + public event EventHandler Changed; + public event EventHandler DataChanged; + + public void DoSomething() + { + this.Changed?.Invoke(this, EventArgs.Empty); + this.DataChanged?.Invoke(this, new DataEventArgs(42)); + } +} + +public class DataEventArgs : EventArgs +{ + public int Value { get; } + public DataEventArgs(int value) => Value = value; +} +``` + +#### Correct usage (matching parameters) + +```csharp +var serviceMock = new Mock(); + +// Raise with EventHandler (sender is mocked object, EventArgs required) +serviceMock.Raise(s => s.Changed += null, EventArgs.Empty); + +// Raise with EventHandler (sender is mocked object, T required) +serviceMock.Raise(s => s.DataChanged += null, new DataEventArgs(42)); + +// Raise with params object[] (sender + event args explicitly) +serviceMock.Raise(s => s.DataChanged += null, "CustomSender", new DataEventArgs(42)); +``` + +#### Incorrect usage (parameters not matching) + +```csharp +var serviceMock = new Mock(); + +// Missing EventArgs +serviceMock.Raise(s => s.Changed += null); // ❌ + +// Wrong argument type +serviceMock.Raise(s => s.DataChanged += null, EventArgs.Empty); // ❌ +``` + +## How to fix violations + +To fix a violation, ensure that the arguments passed to `Raise()` or `RaiseAsync()` match **exactly** the event delegate signature: + +- `EventHandler`: `(object sender, EventArgs e)` +- `EventHandler`: `(object sender, T e)` + +Depending on the overload: +- With `Raise(event, EventArgs)` → provide only `EventArgs` or `T`. +- With `Raise(event, params object[])` → provide both `sender` and `EventArgs` (or `T`). + +## When to suppress warnings + +Do not suppress errors from this rule. If skipped, Moq will throw a runtime exception such as `TargetParameterCountException` +(with the following message *"Parameter count mismatch"*) or an `ArgumentException` to explain that a value can not be converted +to an other type. diff --git a/docs/Compilation/PosInfoMoq2018.md b/docs/Compilation/PosInfoMoq2018.md new file mode 100644 index 0000000..9b903b7 --- /dev/null +++ b/docs/Compilation/PosInfoMoq2018.md @@ -0,0 +1,83 @@ +# PosInfoMoq2018: The first parameter of `Raise()`/`RaiseAsync()` must be an event + +| Property | Value | +|-----------------------|----------------------------------------------------------------------| +| **Rule ID** | PosInfoMoq2018 | +| **Title** | The first parameter of `Raise()`/`RaiseAsync()` must be an event | +| **Category** | Compilation | +| **Default severity** | Error | + +## Cause + +The first parameter passed to `Raise()` or `RaiseAsync()` must reference an **event** member of the mocked type. +Using any other kind of member (property, method, field, etc.) is invalid. + +## Rule description + +When using Moq, `Mock.Raise()` and `Mock.RaiseAsync()` are designed to trigger **events** defined in the mocked type. +If the provided lambda expression instead references something that is not an event (like a property assignment), the code is invalid. + +### Example + +```csharp +public class Service +{ + public event EventHandler Changed; + public event EventHandler DataChanged; + public int Property { get; set; } +} + +public class DataEventArgs : EventArgs +{ + public int Value { get; } + public DataEventArgs(int value) => Value = value; +} +``` + +#### Correct usage (event targeted) + +```csharp +var serviceMock = new Mock(); + +// Raise() with EventHandler +serviceMock.Raise(s => s.Changed += null, EventArgs.Empty); + +// Raise() with EventHandler +serviceMock.Raise(s => s.DataChanged += null, new DataEventArgs(42)); + +// RaiseAsync() with EventHandler +await serviceMock.RaiseAsync(s => s.DataChanged += null, new DataEventArgs(42)); +``` + +#### Incorrect usage (invalid target) + +```csharp +var serviceMock = new Mock(); + +// Refers to a property, not an event +serviceMock.Raise(s => s.Property = 10, EventArgs.Empty); // ❌ + +// Same problem with RaiseAsync +await serviceMock.RaiseAsync(s => s.Property = 10, EventArgs.Empty); // ❌ +``` + +## How to fix violations + +Ensure that the first parameter of `Raise()` and `RaiseAsync()` always references an **event** of the mocked type, never a property or field. + +Correct: + +```csharp +serviceMock.Raise(s => s.Changed += null, EventArgs.Empty); +``` + +Incorrect: + +```csharp +serviceMock.Raise(s => s.Property = 10, EventArgs.Empty); +``` + +## When to suppress warnings + +Do not suppress this warning. If bypassed, Moq itself will throw an `ArgumentException` exception at runtime with a message such as +`Unsupported expression: m => (m.Property = 10)`. \ No newline at end of file diff --git a/docs/Compilation/PosInfoMoq2019.md b/docs/Compilation/PosInfoMoq2019.md new file mode 100644 index 0000000..7d61bc5 --- /dev/null +++ b/docs/Compilation/PosInfoMoq2019.md @@ -0,0 +1,85 @@ +# PosInfoMoq2019: `RaiseAsync()` must be used only for events with async handlers (returning `Task`) + +| Property | Value | +|-----------------------|----------------------------------------------------------------------| +| **Rule ID** | PosInfoMoq2019 | +| **Title** | `RaiseAsync()` must be used only for events with async handlers (returning `Task`) | +| **Category** | Compilation | +| **Default severity** | Error | + +## Cause + +`Mock.RaiseAsync()` must only be used with events whose delegate type returns `Task` (i.e., async events). +Using `RaiseAsync()` on events whose handlers return `void` (or any non-`Task` type) is invalid. + +## Rule description + +Moq provides `RaiseAsync()` to trigger asynchronous events. This requires that the event delegate returns `Task` (or `Task`). +If the event delegate returns `void` (like `EventHandler` or `EventHandler`), `RaiseAsync()` is not appropriate and will cause runtime issues. + +### Delegate examples + +```csharp +// Async event delegate +public delegate Task AsyncEventHandler(object sender, EventArgs e); + +// Classic sync delegates (return void) +public delegate void VoidEventHandler(object sender, EventArgs e); +public class DataEventArgs : EventArgs { public int Value { get; } public DataEventArgs(int v) => Value = v; } +``` + +### Mocked type with both kinds of events + +```csharp +public class Service +{ + public event AsyncEventHandler AsyncChanged; // returns Task + public event EventHandler SyncChanged; // returns void + public event EventHandler SyncDataChanged; // returns void +} +``` + +### Correct usage (async delegate returning Task) + +```csharp +var serviceMock = new Mock(); + +// Use RaiseAsync with async event (Task-returning delegate) +await serviceMock.RaiseAsync(s => s.AsyncChanged += null, EventArgs.Empty); +``` + +### Incorrect usage (void-returning delegates) + +```csharp +var serviceMock = new Mock(); + +// EventHandler returns void — using RaiseAsync is invalid +await serviceMock.RaiseAsync(s => s.SyncChanged += null, EventArgs.Empty); // ❌ + +// EventHandler returns void — using RaiseAsync is invalid +await serviceMock.RaiseAsync(s => s.SyncDataChanged += null, new DataEventArgs(42)); // ❌ +``` + +## How to fix violations + +- If the event delegate returns `void`, use `Raise()` instead of `RaiseAsync()`. +- If you need async semantics, change the event to use a `Task`-returning delegate, for example `AsyncEventHandler`. + +### Fix examples + +```csharp +// Using Raise() for void-returning events +serviceMock.Raise(s => s.SyncChanged += null, EventArgs.Empty); +serviceMock.Raise(s => s.SyncDataChanged += null, new DataEventArgs(42)); + +// Using RaiseAsync() for Task-returning events +await serviceMock.RaiseAsync(s => s.AsyncChanged += null, EventArgs.Empty); +``` + +## When to suppress warnings + +Do not suppress this rule. +If disabled, Moq may throw a runtime exception (`NullReferenceException`) due to a known bug when `RaiseAsync()` is used on non-async events, +instead of providing a clear error message. + +Reference: https://github.com/devlooped/moq/issues/1568 diff --git a/docs/Design/PosInfoMoq1000-Fixer.png b/docs/Design/PosInfoMoq1000-Fixer.png new file mode 100644 index 0000000..519051f Binary files /dev/null and b/docs/Design/PosInfoMoq1000-Fixer.png differ diff --git a/docs/Design/PosInfoMoq1000.md b/docs/Design/PosInfoMoq1000.md index d82f28d..383279e 100644 --- a/docs/Design/PosInfoMoq1000.md +++ b/docs/Design/PosInfoMoq1000.md @@ -40,6 +40,10 @@ public void SendMail_ShouldCallSmtpService() To fix a violation of this rule, call the `VerifyAll()` in the *Assert* phase on the `Mock` instances created during the *Arrange* phase. +### Visual Studio fixer +A Visual Studio fixer exists to add the call to the `VerifyAll()` method at the end of the unit test in the current document, project or solution. +![Visual Studio rule fixer](PosInfoMoq1000-Fixer.png) + ## When to suppress warnings Do not suppress a warning from this rule. Normally all setup methods must be call. diff --git a/docs/Design/PosInfoMoq1010.md b/docs/Design/PosInfoMoq1010.md new file mode 100644 index 0000000..d0cbcd4 --- /dev/null +++ b/docs/Design/PosInfoMoq1010.md @@ -0,0 +1,77 @@ +# PosInfoMoq1010: Use `+= null` syntax when raising events with `Raise()`/`RaiseAsync()` + +| Property | Value | +|-----------------------|----------------------------------------------------------------------| +| **Rule ID** | PosInfoMoq1010 | +| **Title** | Use `+= null` syntax when raising events with `Raise()`/`RaiseAsync()` | +| **Category** | Design | +| **Default severity** | Warning | + +## Cause + +When using `Mock.Raise()` or `Mock.RaiseAsync()`, the lambda expression used to identify the event should consistently use the `+= null` syntax. + +## Rule description + +While Moq is flexible and can identify the event even with `+= SomeMethod`, using `+= null` is the conventional and clearest way to indicate that you are merely referencing the event for Moq's internal use, not actually subscribing a handler. This improves code readability and intent. + +### Example + +```csharp +public class Service +{ + public event EventHandler Changed; + + public void DoSomething() + { + this.Changed?.Invoke(this, EventArgs.Empty); + } +} +``` + +#### Correct usage (`+= null`) + +```csharp +var serviceMock = new Mock(); + +// Clear intent: referencing the event for Moq +serviceMock.Raise(s => s.Changed += null, EventArgs.Empty); +``` + +#### Incorrect usage (`+= SomeMethod`) + +```csharp +public class MyTests +{ + private void MyEventHandler(object sender, EventArgs e) { /* ... */ } + + [Fact] + public void Test() + { + var serviceMock = new Mock(); + + // While functional, less clear intent + serviceMock.Raise(s => s.Changed += MyEventHandler, EventArgs.Empty); // ⚠️ + } +} +``` + +## How to fix violations + +Change the lambda expression to use `+= null` when identifying the event for `Raise()` or `RaiseAsync()`. + +Correct: + +```csharp +serviceMock.Raise(s => s.Changed += null, EventArgs.Empty); +``` + +Incorrect: + +```csharp +serviceMock.Raise(s => s.Changed += MyEventHandler, EventArgs.Empty); +``` + +## When to suppress warnings + +This is a design guideline for code readability. While suppressing it won't cause runtime errors, it's generally recommended to follow for consistent and clear code. diff --git a/src/.editorconfig b/src/.editorconfig new file mode 100644 index 0000000..ff4340d --- /dev/null +++ b/src/.editorconfig @@ -0,0 +1,9 @@ + +[*.{cs,vb}] +#### Naming styles #### + +#### Code Analysis #### +dotnet_diagnostic.CA2016.severity = warning + +#### Visual Studio Threading Analyzer #### +dotnet_diagnostic.VSTHRD111.severity = warning \ No newline at end of file diff --git a/src/Moq.Analyzers/AnalyzerReleases.Shipped.md b/src/Moq.Analyzers/AnalyzerReleases.Shipped.md index b41611f..fcb8544 100644 --- a/src/Moq.Analyzers/AnalyzerReleases.Shipped.md +++ b/src/Moq.Analyzers/AnalyzerReleases.Shipped.md @@ -1,4 +1,14 @@ -## Release 1.13.0 +## Release 2.0.0 + +### New Rules +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +PosInfoMoq1010 | Design | Warning | RaiseMethodsAnalyzer, [Documentation](https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Design/PosInfoMoq1010.html) +PosInfoMoq2017 | Compilation | Error | RaiseMethodsAnalyzer, [Documentation](https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2017.html) +PosInfoMoq2018 | Compilation | Error | RaiseMethodsAnalyzer, [Documentation](https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2018.html) +PosInfoMoq2019 | Compilation | Error | RaiseMethodsAnalyzer, [Documentation](https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2019.html) + +## Release 1.13.0 ### New Rules Rule ID | Category | Severity | Notes diff --git a/src/Moq.Analyzers/Analyzers/RaiseMethodsAnalyzer.cs b/src/Moq.Analyzers/Analyzers/RaiseMethodsAnalyzer.cs new file mode 100644 index 0000000..f98f727 --- /dev/null +++ b/src/Moq.Analyzers/Analyzers/RaiseMethodsAnalyzer.cs @@ -0,0 +1,178 @@ +//----------------------------------------------------------------------- +// +// 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 RaiseMethodsAnalyzer : DiagnosticAnalyzer + { + internal static readonly DiagnosticDescriptor EventExpressionMustBeEventAddWithNull = new DiagnosticDescriptor( + "PosInfoMoq1010", + "Use '+= null' syntax when raising events with Raise()/RaiseAsync()", + "Use '+= null' syntax when raising events with Raise()/RaiseAsync()", + "Design", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Use '+= null' syntax when raising events with Raise()/RaiseAsync().", + helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Design/PosInfoMoq1010.html"); + + internal static readonly DiagnosticDescriptor ParametersMustMatchSignature = new DiagnosticDescriptor( + "PosInfoMoq2017", + "The Raise() parameters must match the signature of the mocked event", + "The Raise() parameters must match the signature of the mocked event. {0}.", + "Compilation", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "The Raise() parameters must match the signature of the mocked event.", + helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2017.html"); + + internal static readonly DiagnosticDescriptor FirstParameterMustBeEvent = new DiagnosticDescriptor( + "PosInfoMoq2018", + "The first parameter of Raise()/RaiseAsync() must be an event", + "The first parameter of Raise()/RaiseAsync() must be an event", + "Compilation", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "The first parameter of Raise()/RaiseAsync() must be an event.", + helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2018.html"); + + internal static readonly DiagnosticDescriptor RaiseAsyncMustBeUsedOnlyForAsyncEvents = new DiagnosticDescriptor( + "PosInfoMoq2019", + "RaiseAsync() must be used only for events with async handlers (returning Task)", + "RaiseAsync() must be used only for events with async handlers (returning Task)", + "Compilation", + DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "RaiseAsync() must be used only for events with async handlers (returning Task).", + helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2019.html"); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( + EventExpressionMustBeEventAddWithNull, + ParametersMustMatchSignature, + FirstParameterMustBeEvent, + RaiseAsyncMustBeUsedOnlyForAsyncEvents); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression); + } + + private static void Analyze(SyntaxNodeAnalysisContext context) + { + var moqSymbols = MoqSymbols.FromCompilation(context.Compilation); + + if (moqSymbols is null) + { + return; + } + + // Extract the Raise() method call info (event + parameters) + var invocationExpression = (InvocationExpressionSyntax)context.Node; + + var moqExpressionAnalyzer = new MoqExpressionAnalyzer(moqSymbols, context.SemanticModel); + + var raiseMethod = moqExpressionAnalyzer.ExtractRaiseMethodCall(invocationExpression, out var invalidEventExpression, context.CancellationToken); + + if (raiseMethod is null) + { + if (invalidEventExpression is not null) + { + // Raise the rule that the first argument of Raise()/RaiseAsync() method must be an event. + context.ReportDiagnostic(FirstParameterMustBeEvent, invalidEventExpression.GetLocation()); + } + + return; + } + + var raiseMethodSyntax = GetRaiseMethodSyntax(invocationExpression); + + if (raiseMethodSyntax is null) + { + return; + } + + if (invalidEventExpression is not null) + { + // Raise a warning to indicate that the expression after the "+=" is not "null". + context.ReportDiagnostic(EventExpressionMustBeEventAddWithNull, invalidEventExpression.GetLocation()); + } + + // -- Check if RaiseAsync() is called for asynchronous event -- + if (moqSymbols.IsRaiseAsyncMethod(raiseMethod.Method)) + { + var eventReturnType = raiseMethod.EventReturnType; + + if (!moqSymbols.IsTask(eventReturnType) && !moqSymbols.IsTaskGeneric(eventReturnType)) + { + context.ReportDiagnostic(RaiseAsyncMustBeUsedOnlyForAsyncEvents, raiseMethodSyntax.GetLocation()); + } + } + + // -- Check the signature of the event called -- + var eventParameters = raiseMethod.EventParameters.ToList(); + + // Check the overload of the Raise() method called. + if (moqSymbols.IsEventArgs(raiseMethod.Method.Parameters[1].Type)) + { + // Case calling the overload : Raise(x => x.Event, EventArgs) + // We don't check in this case the "object sender" parameter + // of the event, because Raise(x => x.Event, EventArgs) + // don't propose to pass this parameter. + eventParameters.RemoveAt(0); + } + + // Check the parameters count match + if (raiseMethod.MethodParameters.Count != eventParameters.Count) + { + context.ReportDiagnostic(ParametersMustMatchSignature, raiseMethodSyntax.GetLocation(), $"The event '{raiseMethod.Event.Name}' expects {raiseMethod.EventParameters.Count} argument(s) but {raiseMethod.MethodParameters.Count} were provided."); + return; + } + + // Check the arguments of the Raise() method match the parameters of the event. + for (var i = 0; i < raiseMethod.MethodParameters.Count; i++) + { + var methodParameter = raiseMethod.MethodParameters[i]; + var eventParameter = eventParameters[i]; + + if (methodParameter is null) + { + // Check the event parameter is nullable. + if (!eventParameter.Type.IsReferenceType) + { + context.ReportDiagnostic(ParametersMustMatchSignature, raiseMethod.MethodArguments[i].GetLocation(), $"The parameter '{eventParameter.Name}' of the event '{raiseMethod.Event.Name}' expects a value of type '{eventParameter.Type.Name}' but a value 'null' was provided."); + } + + continue; + } + + if (!methodParameter.IsOrInheritFrom(eventParameter.Type)) + { + context.ReportDiagnostic(ParametersMustMatchSignature, raiseMethod.MethodArguments[i].GetLocation(), $"The parameter '{eventParameter.Name}' of the event '{raiseMethod.Event.Name}' expects a value of type '{eventParameter.Type.Name}' but a value of type '{methodParameter.Name}' was provided."); + continue; + } + } + } + + private static SyntaxNode? GetRaiseMethodSyntax(InvocationExpressionSyntax expressionSyntax) + { + if (expressionSyntax.Expression is not MemberAccessExpressionSyntax memberAccessExpressionSyntax) + { + return null; + } + + return memberAccessExpressionSyntax.Name; + } + } +} diff --git a/src/Moq.Analyzers/Analyzers/SetupMustBeUsedOnlyForOverridableMembersAnalyzer.cs b/src/Moq.Analyzers/Analyzers/SetupMustBeUsedOnlyForOverridableMembersAnalyzer.cs index 2d6927c..fc3c6ee 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()/SetupSet() method must be used only on overridable members", - "The Setup()/SetupSet() method must be used only on overridable members", + "The Setup()/SetupSequence()/SetupSet() method must be used only on overridable members", + "The Setup()/SetupSequence()/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) && !moqSymbols.IsSetupSetMethod(methodSymbol.Symbol)) + if (!moqSymbols.IsSetupMethod(methodSymbol.Symbol) && !moqSymbols.IsSetupSetMethod(methodSymbol.Symbol) && !moqSymbols.IsSetupSequenceMethod(methodSymbol.Symbol)) { return; } diff --git a/src/Moq.Analyzers/Analyzers/VerifyAllShouldBeCalledAnalyzer.cs b/src/Moq.Analyzers/Analyzers/VerifyAllShouldBeCalledAnalyzer.cs index 72c945f..d2ad9eb 100644 --- a/src/Moq.Analyzers/Analyzers/VerifyAllShouldBeCalledAnalyzer.cs +++ b/src/Moq.Analyzers/Analyzers/VerifyAllShouldBeCalledAnalyzer.cs @@ -15,7 +15,7 @@ namespace PosInformatique.Moq.Analyzers [DiagnosticAnalyzer(LanguageNames.CSharp)] public class VerifyAllShouldBeCalledAnalyzer : DiagnosticAnalyzer { - private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( + internal static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( "PosInfoMoq1000", "VerifyAll() method should be called when instantiate a Mock instances", "The VerifyAll() method should be called at the end of the unit test", @@ -54,7 +54,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context) } // Retrieve the variable name - var variableName = objectCreation.Ancestors().OfType().FirstOrDefault(); + var variableName = SyntaxNodeHelper.GetVariableNameSyntax(objectCreation); if (variableName is null) { diff --git a/src/Moq.Analyzers/CodeFixes/AddVerifyAllCodeFixProvider.cs b/src/Moq.Analyzers/CodeFixes/AddVerifyAllCodeFixProvider.cs new file mode 100644 index 0000000..b52f22d --- /dev/null +++ b/src/Moq.Analyzers/CodeFixes/AddVerifyAllCodeFixProvider.cs @@ -0,0 +1,205 @@ +//----------------------------------------------------------------------- +// +// 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(AddVerifyAllCodeFixProvider))] + [Shared] + public class AddVerifyAllCodeFixProvider : CodeFixProvider + { + public sealed override ImmutableArray FixableDiagnosticIds + { + get { return ImmutableArray.Create(VerifyAllShouldBeCalledAnalyzer.Rule.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); + + var variableName = SyntaxNodeHelper.GetVariableNameSyntax(node); + + if (variableName is null) + { + return; + } + + var parentMethod = node.Ancestors().OfType().FirstOrDefault(); + + if (parentMethod is null) + { + // Parent method not found, skip it. + return; + } + + context.RegisterCodeFix( + CodeAction.Create( + title: "Add VerifyAll() call at the end of the unit test method.", + createChangedDocument: cancellationToken => AddVerifyAllAsync(context.Document, parentMethod, variableName.Identifier.ValueText, cancellationToken), + equivalenceKey: "Add VerifyAll() call at the end of the unit test method."), + diagnostic); + + return; + } + + private static async Task AddVerifyAllAsync(Document document, MethodDeclarationSyntax oldUnitTestMethod, string variableName, CancellationToken cancellationToken) + { + if (oldUnitTestMethod.Body is null) + { + return document; + } + + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + if (semanticModel is null) + { + return document; + } + + var moqSymbols = MoqSymbols.FromCompilation(semanticModel.Compilation); + + if (moqSymbols is null) + { + return document; + } + + // Find the location where we can insert the mock. + var index = oldUnitTestMethod.Body.Statements.Count; + bool insertNewLine = true; + + foreach (var statement in oldUnitTestMethod.Body.Statements.Reverse()) + { + if (statement is not ExpressionStatementSyntax expressionStatement) + { + continue; + } + + if (!IsVerifyAll(semanticModel, moqSymbols, expressionStatement, out var otherVariableName, cancellationToken)) + { + break; + } + + insertNewLine = false; + + if (variableName.CompareTo(otherVariableName) >= 0) + { + // The variable name to add is in alphabetic order after the "variable.VerifyAll()", + // so will insert the statement here. + break; + } + + // We continue to iterate + index--; + } + + var verifyAllCallStatement = SyntaxFactory.ExpressionStatement( + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName(variableName), + SyntaxFactory.IdentifierName("VerifyAll")))); + + var newUnitTestMethodBody = InsertStatement(oldUnitTestMethod.Body, index, verifyAllCallStatement, insertNewLine); + + var oldRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + if (oldRoot is null) + { + return document; + } + + var newRoot = oldRoot.ReplaceNode(oldUnitTestMethod, oldUnitTestMethod.WithBody(newUnitTestMethodBody)); + + return document.WithSyntaxRoot(newRoot); + } + + private static BlockSyntax InsertStatement(BlockSyntax unitTestMethodBody, int index, StatementSyntax statement, bool insertNewLine) + { + var newStatements = new List(unitTestMethodBody.Statements); + + if (index < newStatements.Count) + { + var leadingTrivia = newStatements[index].GetLeadingTrivia(); + + // Remove the leading trivia of the current node and transfert it to the new statement to insert. + newStatements[index] = newStatements[index].ReplaceNode(newStatements[index], newStatements[index].WithoutLeadingTrivia()); + + statement = statement.WithLeadingTrivia(leadingTrivia); + } + + if (insertNewLine) + { + // Special case, we insert a new line at the end of the unit test (we also retrieve the comment at the end if need). + // Because we want that the VerifyAll() blocks are detached with a new line. + // It is useful if developers want to call the fixer for all the solution. + statement = statement.WithLeadingTrivia( + unitTestMethodBody.CloseBraceToken.LeadingTrivia.Add(SyntaxFactory.CarriageReturnLineFeed)); + + unitTestMethodBody = unitTestMethodBody.WithCloseBraceToken(unitTestMethodBody.CloseBraceToken.WithLeadingTrivia()); + } + + newStatements.Insert(index, statement); + + return unitTestMethodBody.WithStatements(SyntaxFactory.List(newStatements)); + } + + private static bool IsVerifyAll(SemanticModel semanticModel, MoqSymbols moqSymbols, ExpressionStatementSyntax statement, out string? variableName, CancellationToken cancellationToken) + { + var expressionSymbol = semanticModel.GetSymbolInfo(statement.Expression, cancellationToken); + + if (!moqSymbols.IsVerifyAllMethod(expressionSymbol.Symbol)) + { + variableName = null; + return false; + } + + if (statement.Expression is not InvocationExpressionSyntax invocationExpressionSyntax) + { + variableName = null; + return false; + } + + if (invocationExpressionSyntax.Expression is not MemberAccessExpressionSyntax memberAccessExpressionSyntax) + { + variableName = null; + return false; + } + + if (memberAccessExpressionSyntax.Expression is not IdentifierNameSyntax identifierNameSyntax) + { + variableName = null; + return false; + } + + variableName = identifierNameSyntax.Identifier.ValueText; + return true; + } + } +} diff --git a/src/Moq.Analyzers/CodeFixes/SetBehaviorToStrictCodeFixProvider.cs b/src/Moq.Analyzers/CodeFixes/SetBehaviorToStrictCodeFixProvider.cs index 0e4c900..c85f4dc 100644 --- a/src/Moq.Analyzers/CodeFixes/SetBehaviorToStrictCodeFixProvider.cs +++ b/src/Moq.Analyzers/CodeFixes/SetBehaviorToStrictCodeFixProvider.cs @@ -111,7 +111,7 @@ private static async Task AddMockBehiavorStrictArgumentAsync(Document var newMockCreationExpression = oldMockCreationExpression.WithArgumentList( SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(arguments))); - var oldRoot = await document.GetSyntaxRootAsync(cancellationToken); + var oldRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); if (oldRoot is null) { @@ -148,7 +148,7 @@ private static async Task AddMockBehiavorStrictArgumentAsync(Document var newInvocationExpression = oldInvocationExpression.WithArgumentList( SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(arguments))); - var oldRoot = await document.GetSyntaxRootAsync(cancellationToken); + var oldRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); if (oldRoot is null) { diff --git a/src/Moq.Analyzers/CodeFixes/SetGenericArgumentSetupSetCodeFixProvider.cs b/src/Moq.Analyzers/CodeFixes/SetGenericArgumentSetupSetCodeFixProvider.cs index d911756..eb1c068 100644 --- a/src/Moq.Analyzers/CodeFixes/SetGenericArgumentSetupSetCodeFixProvider.cs +++ b/src/Moq.Analyzers/CodeFixes/SetGenericArgumentSetupSetCodeFixProvider.cs @@ -61,7 +61,7 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) private static async Task AddMockBehiavorStrictArgumentAsync(Document document, IdentifierNameSyntax oldIdentifierNameSyntax, CancellationToken cancellationToken) { - var semanticModel = await document.GetSemanticModelAsync(); + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); if (semanticModel is null) { @@ -104,7 +104,7 @@ private static async Task AddMockBehiavorStrictArgumentAsync(Document SyntaxFactory.TypeArgumentList( SyntaxFactory.SingletonSeparatedList(propertyType))); - var oldRoot = await document.GetSyntaxRootAsync(cancellationToken); + var oldRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); if (oldRoot is null) { diff --git a/src/Moq.Analyzers/Moq.Analyzers.csproj b/src/Moq.Analyzers/Moq.Analyzers.csproj index 99b7406..82d371c 100644 --- a/src/Moq.Analyzers/Moq.Analyzers.csproj +++ b/src/Moq.Analyzers/Moq.Analyzers.csproj @@ -17,6 +17,28 @@ https://github.com/PosInformatique/PosInformatique.Moq.Analyzers README.md + 2.0.0 + - Add new rules: + - PosInfoMoq1010: Use '+= null' syntax when raising events with Raise()/RaiseAsync() + - PosInfoMoq2001: Add the support of the SetupSequence() methods. + - PosInfoMoq2017: Mock<T>.Raise()/RaiseAsync() must use parameters matching the event signature. + - PosInfoMoq2018: The first parameter of Raise()/RaiseAsync() must be an event. + - PosInfoMoq2019: RaiseAsync() must be used only for events with async handlers (returning Task). + - Add new fixers: + - PosInfoMoq1000: Add a call of missing VerifyAll() at the end of the unit tests. + + 1.13.0 + - Add new rules: + - PosInfoMoq1007: The Verify() method must specify the Times argument. + - PosInfoMoq1008: The Mock.Verify() and Mock.VerifyAll() methods must specify at least one mock. + - PosInfoMoq1009: Avoid using Verifiable() method. + - Improvements/Fixes + - When using It.IsAny<T>() or It.Is<T>() with inherited class, the PosInfoMoq1006 was raised. + + 1.12.0 + - Add new rules: + - PosInfoMoq1006: The It.IsAny<T>() or It.Is<T>() arguments must match the parameters of the mocked method. + 1.11.0 - Add new rules: - PosInfoMoq1005: Defines the generic argument of the SetupSet() method with the type of the mocked property. @@ -31,8 +53,8 @@ 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. @@ -107,6 +129,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Moq.Analyzers/MoqExpressionAnalyzer.cs b/src/Moq.Analyzers/MoqExpressionAnalyzer.cs index 7183f3f..3ce9631 100644 --- a/src/Moq.Analyzers/MoqExpressionAnalyzer.cs +++ b/src/Moq.Analyzers/MoqExpressionAnalyzer.cs @@ -7,6 +7,7 @@ namespace PosInformatique.Moq.Analyzers { using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; internal class MoqExpressionAnalyzer @@ -488,6 +489,82 @@ public bool IsStrictBehavior(IdentifierNameSyntax localVariableExpression, Cance return methodSymbol.TypeArguments[0]; } + public RaiseMethodCall? ExtractRaiseMethodCall(InvocationExpressionSyntax invocationExpression, out ExpressionSyntax? invalidEventExpression, CancellationToken cancellationToken) + { + // Check if the method analyzed is a Raise() method. + var methodSymbol = this.semanticModel.GetSymbolInfo(invocationExpression, cancellationToken); + + if (!this.moqSymbols.IsRaiseMethod(methodSymbol.Symbol) && !this.moqSymbols.IsRaiseAsyncMethod(methodSymbol.Symbol)) + { + invalidEventExpression = null; + return null; + } + + // Gets the event + // 1 - Check the first argument is a lambda expression (Raise(x => ...)) + var eventExpression = invocationExpression.ArgumentList.Arguments[0].Expression; + + if (eventExpression is not LambdaExpressionSyntax lambdaExpressionSyntax) + { + invalidEventExpression = eventExpression; + return null; + } + + // 2 - If the body of the lambda expression is an assignment to an event (Raise(x => x.Event += null)) + if (lambdaExpressionSyntax.Body is not AssignmentExpressionSyntax assignmentExpressionSyntax) + { + invalidEventExpression = eventExpression; + return null; + } + + // 3 - Check the left of assignment is a member access. + if (assignmentExpressionSyntax.Left is not MemberAccessExpressionSyntax memberAccessExpressionSyntax) + { + invalidEventExpression = eventExpression; + return null; + } + + // 4 - Gets the event symbol + var memberSymbol = this.semanticModel.GetSymbolInfo(memberAccessExpressionSyntax.Name, cancellationToken); + + if (memberSymbol.Symbol is not IEventSymbol eventSymbol) + { + invalidEventExpression = eventExpression; + return null; + } + + // 5 - Gets the parameters of the Raise() method after the "x => x.Event += null". + var arguments = invocationExpression.ArgumentList.Arguments.Skip(1).ToArray(); + + var parameterSymbols = new List(arguments.Length); + + foreach (var argument in arguments) + { + var parameterSymbol = this.semanticModel.GetTypeInfo(argument.Expression, cancellationToken); + + parameterSymbols.Add(parameterSymbol.Type); + } + + // Check the right assignement expression is "null". + if (assignmentExpressionSyntax.Right is not LiteralExpressionSyntax literalExpressionSyntax) + { + invalidEventExpression = assignmentExpressionSyntax.Right; + } + else + { + if (literalExpressionSyntax.Kind() != SyntaxKind.NullLiteralExpression) + { + invalidEventExpression = assignmentExpressionSyntax.Right; + } + else + { + invalidEventExpression = null; + } + } + + return new RaiseMethodCall((IMethodSymbol)methodSymbol.Symbol, parameterSymbols, arguments, eventSymbol); + } + private static ObjectCreationExpressionSyntax? FindMockCreation(BlockSyntax block, string variableName) { foreach (var statement in block.Statements.OfType()) diff --git a/src/Moq.Analyzers/MoqSymbols.cs b/src/Moq.Analyzers/MoqSymbols.cs index d306d8d..43afa38 100644 --- a/src/Moq.Analyzers/MoqSymbols.cs +++ b/src/Moq.Analyzers/MoqSymbols.cs @@ -16,6 +16,10 @@ internal sealed class MoqSymbols private readonly INamedTypeSymbol mockGenericClass; + private readonly Lazy> raiseMethods; + + private readonly Lazy> raiseAsyncMethods; + private readonly Lazy> setupMethods; private readonly Lazy> setupProtectedMethods; @@ -48,12 +52,20 @@ internal sealed class MoqSymbols private readonly Lazy setupSetMethodWithoutGenericArgument; + private readonly Lazy> setupSequenceMethods; + private readonly Lazy> setupSetMethods; private readonly Lazy timesClass; + private readonly Lazy eventArgsClass; + private readonly Lazy funcClass; + private readonly Lazy taskClass; + + private readonly Lazy taskGenericClass; + private MoqSymbols(INamedTypeSymbol mockGenericClass, Compilation compilation) { this.mockGenericClass = mockGenericClass; @@ -66,6 +78,8 @@ private MoqSymbols(INamedTypeSymbol mockGenericClass, Compilation compilation) this.itIsAnyMethod = new Lazy(() => compilation.GetTypeByMetadataName("Moq.It")!.GetMembers("IsAny").Single()); this.verifiesInterface = new Lazy(() => compilation.GetTypeByMetadataName("Moq.Language.IVerifies")!); + this.raiseMethods = new Lazy>(() => mockGenericClass.GetMembers("Raise").OfType().ToArray()); + this.raiseAsyncMethods = new Lazy>(() => mockGenericClass.GetMembers("RaiseAsync").OfType().ToArray()); this.setupMethods = new Lazy>(() => mockGenericClass.GetMembers("Setup").Concat(setupConditionResultInterface.Value.GetMembers("Setup")).OfType().ToArray()); this.mockBehaviorStrictField = new Lazy(() => this.mockBehaviorEnum.Value.GetMembers("Strict").First()); this.setupProtectedMethods = new Lazy>(() => compilation.GetTypeByMetadataName("Moq.Protected.IProtectedMock`1")!.GetMembers("Setup").OfType().ToArray()); @@ -81,11 +95,16 @@ private MoqSymbols(INamedTypeSymbol mockGenericClass, Compilation compilation) this.mockConstructorWithFactory = new Lazy(() => mockGenericClass.Constructors.Single(c => c.Parameters.Length == 2 && c.Parameters[0].Type.Name == "Expression")); + this.setupSequenceMethods = new Lazy>(() => mockGenericClass.GetMembers("SetupSequence").OfType().ToArray()); this.setupSetMethodWithoutGenericArgument = new Lazy(() => mockGenericClass.GetMembers("SetupSet").OfType().Single(c => c.TypeArguments.Length == 1)); this.setupSetMethods = new Lazy>(() => mockGenericClass.GetMembers("SetupSet").OfType().ToArray()); this.timesClass = new Lazy(() => compilation.GetTypeByMetadataName("Moq.Times")!); + + this.eventArgsClass = new Lazy(() => compilation.GetTypeByMetadataName("System.EventArgs")!); this.funcClass = new Lazy(() => compilation.GetTypeByMetadataName("System.Func`1")!); + this.taskClass = new Lazy(() => compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")!); + this.taskGenericClass = new Lazy(() => compilation.GetTypeByMetadataName("System.Threading.Tasks.Task`1")!); } public static MoqSymbols? FromCompilation(Compilation compilation) @@ -122,14 +141,7 @@ public bool ContainsTimesParameters(IMethodSymbol method) } public bool IsAnyType(ITypeSymbol symbol) - { - if (!SymbolEqualityComparer.Default.Equals(symbol, this.isAnyTypeClass.Value)) - { - return false; - } - - return true; - } + => AreEqual(symbol, this.isAnyTypeClass); public ITypeSymbol? GetItIsType(ISymbol? symbol) { @@ -149,20 +161,11 @@ public bool IsAnyType(ITypeSymbol symbol) return null; } - public bool IsItIsAny(ISymbol? symbol) - { - if (symbol is null) - { - return false; - } - - if (!SymbolEqualityComparer.Default.Equals(symbol.OriginalDefinition, this.itIsAnyMethod.Value)) - { - return false; - } + public bool IsEventArgs(ISymbol? symbol) + => AreEqual(symbol, this.eventArgsClass); - return true; - } + public bool IsItIsAny(ISymbol? symbol) + => AreEqual(symbol, this.itIsAnyMethod); public ITypeSymbol? GetItIsAnyType(ISymbol? symbol) { @@ -194,165 +197,47 @@ public bool IsMock(ISymbol? symbol) return true; } - public bool IsSetupMethod(ISymbol? symbol) - { - if (symbol is null) - { - return false; - } - - var originalDefinition = symbol.OriginalDefinition; + public bool IsRaiseMethod([NotNullWhen(true)] ISymbol? symbol) + => AreEqual(symbol, this.raiseMethods); - foreach (var setupMethod in this.setupMethods.Value) - { - if (SymbolEqualityComparer.Default.Equals(originalDefinition, setupMethod)) - { - return true; - } - } + public bool IsRaiseAsyncMethod([NotNullWhen(true)] ISymbol? symbol) + => AreEqual(symbol, this.raiseAsyncMethods); - return false; - } + public bool IsSetupMethod(ISymbol? symbol) + => AreEqual(symbol, this.setupMethods); public bool IsSetupProtectedMethod(ISymbol? symbol) - { - if (symbol is null) - { - return false; - } - - var originalDefinition = symbol.OriginalDefinition; + => AreEqual(symbol, this.setupProtectedMethods); - foreach (var setupProtectedMethod in this.setupProtectedMethods.Value) - { - if (SymbolEqualityComparer.Default.Equals(originalDefinition, setupProtectedMethod)) - { - return true; - } - } - - return false; - } + public bool IsSetupSequenceMethod(ISymbol? symbol) + => AreEqual(symbol, this.setupSequenceMethods); 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; - } + => AreEqual(symbol, this.setupSetMethods); public bool IsSetupSetMethodWithoutGenericArgument(ISymbol? symbol) - { - if (symbol is null) - { - return false; - } + => AreEqual(symbol, this.setupSetMethodWithoutGenericArgument); - if (!SymbolEqualityComparer.Default.Equals(symbol.OriginalDefinition, this.setupSetMethodWithoutGenericArgument.Value)) - { - return false; - } + public bool IsTask(ISymbol? symbol) + => AreEqual(symbol, this.taskClass); - return true; - } + public bool IsTaskGeneric(ISymbol? symbol) + => AreEqual(symbol, this.taskGenericClass); public bool IsVerifiableMethod([NotNullWhen(true)] ISymbol? symbol) - { - if (symbol is null) - { - return false; - } - - var originalDefinition = symbol.OriginalDefinition; - - foreach (var verifiableMethod in this.verifiableMethods.Value) - { - if (SymbolEqualityComparer.Default.Equals(originalDefinition, verifiableMethod)) - { - return true; - } - } - - return false; - } + => AreEqual(symbol, this.verifiableMethods); public bool IsVerifyMethod([NotNullWhen(true)] ISymbol? symbol) - { - if (symbol is null) - { - return false; - } - - var originalDefinition = symbol.OriginalDefinition; - - foreach (var verifyMethod in this.verifyMethods.Value) - { - if (SymbolEqualityComparer.Default.Equals(originalDefinition, verifyMethod)) - { - return true; - } - } - - return false; - } + => AreEqual(symbol, this.verifyMethods); public bool IsVerifyStaticMethod([NotNullWhen(true)] ISymbol? symbol) - { - if (symbol is null) - { - return false; - } - - if (!SymbolEqualityComparer.Default.Equals(symbol, this.staticVerifyMethod.Value)) - { - return false; - } - - return true; - } + => AreEqual(symbol, this.staticVerifyMethod); public bool IsVerifyAllMethod(ISymbol? symbol) - { - if (symbol is null) - { - return false; - } - - if (!SymbolEqualityComparer.Default.Equals(symbol.OriginalDefinition, this.verifyAllMethod.Value)) - { - return false; - } - - return true; - } + => AreEqual(symbol, this.verifyAllMethod); public bool IsVerifyAllStaticMethod([NotNullWhen(true)] ISymbol? symbol) - { - if (symbol is null) - { - return false; - } - - if (!SymbolEqualityComparer.Default.Equals(symbol, this.staticVerifyAllMethod.Value)) - { - return false; - } - - return true; - } + => AreEqual(symbol, this.staticVerifyAllMethod); public bool IsCallback(ISymbol? symbol) { @@ -425,34 +310,10 @@ public bool IsThrowsAsyncMethod(ISymbol symbol) } public bool IsMockBehaviorEnum(ISymbol? symbol) - { - if (symbol is null) - { - return false; - } - - if (!SymbolEqualityComparer.Default.Equals(symbol, this.mockBehaviorEnum.Value)) - { - return false; - } - - return true; - } + => AreEqual(symbol, this.mockBehaviorEnum); public bool IsMockBehaviorStrictField(ISymbol? symbol) - { - if (symbol is null) - { - return false; - } - - if (!SymbolEqualityComparer.Default.Equals(symbol, this.mockBehaviorStrictField.Value)) - { - return false; - } - - return true; - } + => AreEqual(symbol, this.mockBehaviorStrictField); public bool IsOverridable(ISymbol method) { @@ -500,31 +361,23 @@ public bool IsMockable(ITypeSymbol type) } public bool IsMockOfMethod(ISymbol? symbol) - { - if (symbol is null) - { - return false; - } + => AreEqual(symbol, this.mockOfMethods); - foreach (var mockOfMethod in this.mockOfMethods.Value) - { - if (SymbolEqualityComparer.Default.Equals(symbol.OriginalDefinition, mockOfMethod)) - { - return true; - } - } + public bool IsMockConstructorWithFactory(ISymbol? symbol) + => AreEqual(symbol, this.mockConstructorWithFactory); - return false; - } + public bool IsAsMethod(IMethodSymbol symbol) + => AreEqual(symbol, this.asMethod); - public bool IsMockConstructorWithFactory(ISymbol? symbol) + private static bool AreEqual([NotNullWhen(true)] ISymbol? symbol1, Lazy symbol2) + where TSymbol : ISymbol { - if (symbol is null) + if (symbol1 is null) { return false; } - if (!SymbolEqualityComparer.Default.Equals(symbol.OriginalDefinition, this.mockConstructorWithFactory.Value)) + if (!SymbolEqualityComparer.Default.Equals(symbol1.OriginalDefinition, symbol2.Value)) { return false; } @@ -532,14 +385,25 @@ public bool IsMockConstructorWithFactory(ISymbol? symbol) return true; } - public bool IsAsMethod(IMethodSymbol method) + private static bool AreEqual([NotNullWhen(true)] ISymbol? symbol1, Lazy> symbols2) + where TSymbol : ISymbol { - if (!SymbolEqualityComparer.Default.Equals(method.OriginalDefinition, this.asMethod.Value)) + if (symbol1 is null) { return false; } - return true; + var originalDefinition = symbol1.OriginalDefinition; + + foreach (var symbol in symbols2.Value) + { + if (SymbolEqualityComparer.Default.Equals(originalDefinition, symbol)) + { + return true; + } + } + + return false; } } } diff --git a/src/Moq.Analyzers/RaiseMethodCall.cs b/src/Moq.Analyzers/RaiseMethodCall.cs new file mode 100644 index 0000000..2b3bb89 --- /dev/null +++ b/src/Moq.Analyzers/RaiseMethodCall.cs @@ -0,0 +1,38 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Moq.Analyzers +{ + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp.Syntax; + + internal sealed class RaiseMethodCall + { + private readonly IMethodSymbol eventMethod; + + public RaiseMethodCall(IMethodSymbol method, IReadOnlyList methodParameters, IReadOnlyList methodArguments, IEventSymbol @event) + { + this.Method = method; + this.Event = @event; + this.MethodArguments = methodArguments; + this.MethodParameters = methodParameters; + + this.eventMethod = ((INamedTypeSymbol)@event.Type).DelegateInvokeMethod!; + } + + public IMethodSymbol Method { get; } + + public IReadOnlyList MethodArguments { get; } + + public IReadOnlyList MethodParameters { get; } + + public IEventSymbol Event { get; } + + public IReadOnlyList EventParameters => this.eventMethod.Parameters; + + public ITypeSymbol EventReturnType => this.eventMethod.ReturnType; + } +} diff --git a/src/Moq.Analyzers/SyntaxNodeHelper.cs b/src/Moq.Analyzers/SyntaxNodeHelper.cs new file mode 100644 index 0000000..77001f2 --- /dev/null +++ b/src/Moq.Analyzers/SyntaxNodeHelper.cs @@ -0,0 +1,27 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Moq.Analyzers +{ + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp.Syntax; + + internal static class SyntaxNodeHelper + { + public static VariableDeclaratorSyntax? GetVariableNameSyntax(SyntaxNode node) + { + var variableName = node.Ancestors().OfType().FirstOrDefault(); + + if (variableName is null) + { + // No variable set on the left. + return null; + } + + return variableName; + } + } +} diff --git a/tests/Moq.Analyzers.Sandbox/Moq.Analyzers.Sandbox.csproj b/tests/Moq.Analyzers.Sandbox/Moq.Analyzers.Sandbox.csproj index e530db7..57aa733 100644 --- a/tests/Moq.Analyzers.Sandbox/Moq.Analyzers.Sandbox.csproj +++ b/tests/Moq.Analyzers.Sandbox/Moq.Analyzers.Sandbox.csproj @@ -6,17 +6,18 @@ + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - diff --git a/tests/Moq.Analyzers.Tests/Analyzers/CallBackDelegateMustMatchMockedMethodAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/CallBackDelegateMustMatchMockedMethodAnalyzerTest.cs index 7316795..b649dd2 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/CallBackDelegateMustMatchMockedMethodAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/CallBackDelegateMustMatchMockedMethodAnalyzerTest.cs @@ -182,7 +182,9 @@ await Verifier.VerifyAnalyzerAsync( source, [ new DiagnosticResult(CallBackDelegateMustMatchMockedMethodAnalyzer.CallbackMustNotReturnValue) - .WithSpan(59, 57, 59, 70).WithSpan(62, 41, 62, 54).WithSpan(65, 37, 65, 49), + .WithLocation(0) + .WithLocation(1) + .WithLocation(2), ]); } diff --git a/tests/Moq.Analyzers.Tests/Analyzers/CallBackDelegateParametersShouldNotBeIgnoredAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/CallBackDelegateParametersShouldNotBeIgnoredAnalyzerTest.cs index de6f880..b3de61d 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/CallBackDelegateParametersShouldNotBeIgnoredAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/CallBackDelegateParametersShouldNotBeIgnoredAnalyzerTest.cs @@ -46,13 +46,17 @@ await Verifier.VerifyAnalyzerAsync( source, [ new DiagnosticResult(CallBackDelegateParametersShouldNotBeIgnoredAnalyzer.Rule) - .WithSpan(13, 44, 13, 52).WithArguments("a"), + .WithLocation(0) + .WithArguments("a"), new DiagnosticResult(CallBackDelegateParametersShouldNotBeIgnoredAnalyzer.Rule) - .WithSpan(13, 54, 13, 59).WithArguments("b"), + .WithLocation(1) + .WithArguments("b"), new DiagnosticResult(CallBackDelegateParametersShouldNotBeIgnoredAnalyzer.Rule) - .WithSpan(15, 54, 15, 59).WithArguments("b"), + .WithLocation(2) + .WithArguments("b"), new DiagnosticResult(CallBackDelegateParametersShouldNotBeIgnoredAnalyzer.Rule) - .WithSpan(17, 44, 17, 52).WithArguments("a"), + .WithLocation(3) + .WithArguments("a"), ]); } diff --git a/tests/Moq.Analyzers.Tests/Analyzers/CallBackDelegateShouldBeUsedWithItIsAnyParametersAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/CallBackDelegateShouldBeUsedWithItIsAnyParametersAnalyzerTest.cs index 73806a0..8cb90a9 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/CallBackDelegateShouldBeUsedWithItIsAnyParametersAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/CallBackDelegateShouldBeUsedWithItIsAnyParametersAnalyzerTest.cs @@ -44,15 +44,20 @@ await Verifier.VerifyAnalyzerAsync( source, [ new DiagnosticResult(CallBackDelegateShouldBeUsedWithItIsAnyParametersAnalyzer.Rule) - .WithSpan(12, 59, 12, 77).WithArguments("a"), + .WithLocation(0) + .WithArguments("a"), new DiagnosticResult(CallBackDelegateShouldBeUsedWithItIsAnyParametersAnalyzer.Rule) - .WithSpan(12, 79, 12, 94).WithArguments("b"), + .WithLocation(1) + .WithArguments("b"), new DiagnosticResult(CallBackDelegateShouldBeUsedWithItIsAnyParametersAnalyzer.Rule) - .WithSpan(13, 70, 13, 85).WithArguments("b"), + .WithLocation(2) + .WithArguments("b"), new DiagnosticResult(CallBackDelegateShouldBeUsedWithItIsAnyParametersAnalyzer.Rule) - .WithSpan(14, 59, 14, 77).WithArguments("a"), + .WithLocation(3) + .WithArguments("a"), new DiagnosticResult(CallBackDelegateShouldBeUsedWithItIsAnyParametersAnalyzer.Rule) - .WithSpan(15, 59, 15, 77).WithArguments("a"), + .WithLocation(4) + .WithArguments("a"), ]); } @@ -91,15 +96,20 @@ await Verifier.VerifyAnalyzerAsync( source, [ new DiagnosticResult(CallBackDelegateShouldBeUsedWithItIsAnyParametersAnalyzer.Rule) - .WithSpan(14, 80, 14, 98).WithArguments("a"), + .WithLocation(0) + .WithArguments("a"), new DiagnosticResult(CallBackDelegateShouldBeUsedWithItIsAnyParametersAnalyzer.Rule) - .WithSpan(14, 100, 14, 115).WithArguments("b"), + .WithLocation(1) + .WithArguments("b"), new DiagnosticResult(CallBackDelegateShouldBeUsedWithItIsAnyParametersAnalyzer.Rule) - .WithSpan(15, 91, 15, 106).WithArguments("b"), + .WithLocation(2) + .WithArguments("b"), new DiagnosticResult(CallBackDelegateShouldBeUsedWithItIsAnyParametersAnalyzer.Rule) - .WithSpan(16, 80, 16, 98).WithArguments("a"), + .WithLocation(3) + .WithArguments("a"), new DiagnosticResult(CallBackDelegateShouldBeUsedWithItIsAnyParametersAnalyzer.Rule) - .WithSpan(17, 80, 17, 98).WithArguments("a"), + .WithLocation(4) + .WithArguments("a"), ]); } diff --git a/tests/Moq.Analyzers.Tests/Analyzers/ItArgumentsMustMatchMockedMethodArgumentsAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/ItArgumentsMustMatchMockedMethodArgumentsAnalyzerTest.cs index 84cc9b6..411bd40 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/ItArgumentsMustMatchMockedMethodArgumentsAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/ItArgumentsMustMatchMockedMethodArgumentsAnalyzerTest.cs @@ -98,11 +98,11 @@ await Verifier.VerifyAnalyzerAsync( source, [ new DiagnosticResult(ItArgumentsMustMatchMockedMethodArgumentsAnalyzer.Rule) - .WithSpan(13, 59, 13, 74), + .WithLocation(0), new DiagnosticResult(ItArgumentsMustMatchMockedMethodArgumentsAnalyzer.Rule) - .WithSpan(14, 59, 14, 83), + .WithLocation(1), new DiagnosticResult(ItArgumentsMustMatchMockedMethodArgumentsAnalyzer.Rule) - .WithSpan(15, 59, 15, 104), + .WithLocation(2), ]); } diff --git a/tests/Moq.Analyzers.Tests/Analyzers/RaiseMethodsAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/RaiseMethodsAnalyzerTest.cs new file mode 100644 index 0000000..655fc1a --- /dev/null +++ b/tests/Moq.Analyzers.Tests/Analyzers/RaiseMethodsAnalyzerTest.cs @@ -0,0 +1,393 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Moq.Analyzers.Tests +{ + using Microsoft.CodeAnalysis.Testing; + using Verifier = MoqCSharpAnalyzerVerifier; + + public class RaiseMethodsAnalyzerTest + { + [Theory] + [InlineData("Raise", "void")] + [InlineData("RaiseAsync", "Task")] + [InlineData("RaiseAsync", "Task")] + public async Task Raise_WithParams_NoDiagnosticReported(string method, string eventReturnType) + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + using System; + using System.Threading.Tasks; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(MockBehavior.Strict); + + mock1." + method + @"(m => m.TheEvent += null, ""OK"", 1, null); + mock1." + method + @"(m => m.TheEvent += null, ""OK"", 1, 10); + mock1." + method + @"(m => m.TheEvent += null, ""OK"", 1, new object()); + + mock1." + method + @"(m => m.TheEvent -= null, ""OK"", 1, null); + mock1." + method + @"(m => m.TheEvent -= null, ""OK"", 1, 10); + mock1." + method + @"(m => m.TheEvent -= null, ""OK"", 1, new object()); + } + } + + public interface I + { + event CustomEventHandler TheEvent; + } + + public delegate " + eventReturnType + @" CustomEventHandler(string a, int b, object c); + }"; + + await Verifier.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task Raise_WithEventArgs_NoDiagnosticReported() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + using System; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(MockBehavior.Strict); + + mock1.Raise(m => m.TheEvent += null, new CustomEventArgs()); + mock1.Raise(m => m.TheEvent += null, null, new CustomEventArgs()); + mock1.Raise(m => m.TheEvent += null, 10, new CustomEventArgs()); + mock1.Raise(m => m.TheEvent += null, new object(), new CustomEventArgs()); + + mock1.Raise(m => m.TheEvent -= null, new CustomEventArgs()); + mock1.Raise(m => m.TheEvent -= null, null, new CustomEventArgs()); + mock1.Raise(m => m.TheEvent -= null, 10, new CustomEventArgs()); + mock1.Raise(m => m.TheEvent -= null, new object(), new CustomEventArgs()); + } + } + + public interface I + { + event EventHandler TheEvent; + } + + public class CustomEventArgs : EventArgs + { + } + }"; + + await Verifier.VerifyAnalyzerAsync(source); + } + + [Theory] + [InlineData("Raise", "void")] + [InlineData("RaiseAsync", "Task")] + [InlineData("RaiseAsync", "Task")] + public async Task Raise_WithParams_MissingArgument_DiagnosticReported(string method, string eventReturnType) + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + using System; + using System.Threading.Tasks; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(MockBehavior.Strict); + + mock1.{|#0:" + method + @"|#0}(m => m.TheEvent += null); + mock1.{|#1:" + method + @"|#1}(m => m.TheEvent += null, 1); + mock1.{|#2:" + method + @"|#2}(m => m.TheEvent += null, 1, 2, ""3"", 4, 5, 6); + } + } + + public interface I + { + event CustomEventHandler TheEvent; + } + + public delegate " + eventReturnType + @" CustomEventHandler(string a, int b, object c); + }"; + + await Verifier.VerifyAnalyzerAsync( + source, + [ + new DiagnosticResult(RaiseMethodsAnalyzer.ParametersMustMatchSignature) + .WithLocation(0) + .WithArguments("The event 'TheEvent' expects 3 argument(s) but 0 were provided."), + new DiagnosticResult(RaiseMethodsAnalyzer.ParametersMustMatchSignature) + .WithLocation(1) + .WithArguments("The event 'TheEvent' expects 3 argument(s) but 1 were provided."), + new DiagnosticResult(RaiseMethodsAnalyzer.ParametersMustMatchSignature) + .WithLocation(2) + .WithArguments("The event 'TheEvent' expects 3 argument(s) but 6 were provided."), + ]); + } + + [Theory] + [InlineData("Raise", "void")] + [InlineData("RaiseAsync", "Task")] + [InlineData("RaiseAsync", "Task")] + public async Task Raise_WithParams_WrongType_DiagnosticReported(string method, string eventReturnType) + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + using System; + using System.Threading.Tasks; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(MockBehavior.Strict); + + mock1." + method + @"(m => m.TheEvent += null, {|#0:1|#0}, 2, ""OK"", {|#1:""Wrong""|#1}, null, {|#2:null|#2}); + } + } + + public interface I + { + event CustomEventHandler TheEvent; + } + + public delegate " + eventReturnType + @" CustomEventHandler(string a, int b, object c, int d, string e, int f); + }"; + + await Verifier.VerifyAnalyzerAsync( + source, + [ + new DiagnosticResult(RaiseMethodsAnalyzer.ParametersMustMatchSignature) + .WithLocation(0) + .WithArguments("The parameter 'a' of the event 'TheEvent' expects a value of type 'String' but a value of type 'Int32' was provided."), + new DiagnosticResult(RaiseMethodsAnalyzer.ParametersMustMatchSignature) + .WithLocation(1) + .WithArguments("The parameter 'd' of the event 'TheEvent' expects a value of type 'Int32' but a value of type 'String' was provided."), + new DiagnosticResult(RaiseMethodsAnalyzer.ParametersMustMatchSignature) + .WithLocation(2) + .WithArguments("The parameter 'f' of the event 'TheEvent' expects a value of type 'Int32' but a value 'null' was provided."), + ]); + } + + [Fact] + public async Task Raise_WithEventArgs_WrongType_DiagnosticReported() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + using System; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(MockBehavior.Strict); + + mock1.Raise(m => m.TheEvent += null, {|#0:new EventArgs()|#0}); + } + } + + public interface I + { + event EventHandler TheEvent; + } + + public class CustomEventArgs : EventArgs + { + } + }"; + + await Verifier.VerifyAnalyzerAsync( + source, + [ + new DiagnosticResult(RaiseMethodsAnalyzer.ParametersMustMatchSignature) + .WithLocation(0) + .WithArguments("The parameter 'e' of the event 'TheEvent' expects a value of type 'CustomEventArgs' but a value of type 'EventArgs' was provided."), + ]); + } + + [Theory] + [InlineData("Raise", "void", "\"OK\", 1, null")] + [InlineData("Raise", "void", "\"OK\", 1, 10")] + [InlineData("Raise", "void", "\"OK\", 1, new object()")] + [InlineData("RaiseAsync", "Task", "\"OK\", 1, null")] + [InlineData("RaiseAsync", "Task", "\"OK\", 1, 10")] + [InlineData("RaiseAsync", "Task", "\"OK\", 1, new object()")] + [InlineData("RaiseAsync", "Task", "\"OK\", 1, null")] + [InlineData("RaiseAsync", "Task", "\"OK\", 1, 10")] + [InlineData("RaiseAsync", "Task", "\"OK\", 1, new object()")] + public async Task Raise_WrongLambdaExpression_DiagnosticReported(string method, string eventReturnType, string parameters) + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + using System; + using System.Threading.Tasks; + + public class TestClass + { + public void TestMethod() + { + var act = (I i) => i.TheEvent += null; + var tmp = 0; + + var mock1 = new Mock(MockBehavior.Strict); + + mock1." + method + @"({|PosInfoMoq2018:null|}); + mock1." + method + @"({|PosInfoMoq2018:null|}, " + parameters + @"); + mock1." + method + @"({|PosInfoMoq2018:m => m.ToString()|}, "" + parameters + @""); + mock1." + method + @"({|PosInfoMoq2018:act|}, "" + parameters + @""); + mock1." + method + @"({|PosInfoMoq2018:m => new object()|}, "" + parameters + @""); + mock1." + method + @"({|PosInfoMoq2018:m => tmp = 10|}, "" + parameters + @""); + mock1." + method + @"({|PosInfoMoq2018:m => m.Property = 10|}, "" + parameters + @""); + } + } + + public interface I + { + event CustomEventHandler TheEvent; + + int Property { get; set; } + } + + public delegate " + eventReturnType + @" CustomEventHandler(string a, int b, object c); + }"; + + await Verifier.VerifyAnalyzerAsync(source); + } + + [Theory] + [InlineData("Raise", "void")] + [InlineData("RaiseAsync", "Task")] + [InlineData("RaiseAsync", "Task")] + public async Task Raise_WithNotNullDelegate_NoDiagnosticReported(string method, string eventReturnType) + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + using System; + using System.Threading.Tasks; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(MockBehavior.Strict); + + mock1." + method + @"(m => m.TheEvent += {|PosInfoMoq1010:MethodMatchEventSignature|}, ""OK"", 1, null); + mock1." + method + @"(m => m.TheEvent += {|PosInfoMoq1010:MethodMatchEventSignature|}, ""OK"", 1, 10); + mock1." + method + @"(m => m.TheEvent += {|PosInfoMoq1010:MethodMatchEventSignature|}, ""OK"", 1, new object()); + + mock1." + method + @"(m => m.TheEvent += {|PosInfoMoq1010:(a, b, c) => throw new NotSupportedException()|}, ""OK"", 1, null); + mock1." + method + @"(m => m.TheEvent += {|PosInfoMoq1010:(a, b, c) => throw new NotSupportedException()|}, ""OK"", 1, 10); + mock1." + method + @"(m => m.TheEvent += {|PosInfoMoq1010:(a, b, c) => throw new NotSupportedException()|}, ""OK"", 1, new object()); + } + + public static " + eventReturnType + @" MethodMatchEventSignature(string a, int b, object c) { throw new NotSupportedException(); } + } + + public interface I + { + event CustomEventHandler TheEvent; + } + + public delegate " + eventReturnType + @" CustomEventHandler(string a, int b, object c); + }"; + + await Verifier.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task RaiseAsync_WithNoAsynchronousEvent_NoDiagnosticReported() + { + var source = @" + namespace ConsoleApplication1 + { + using Moq; + using System; + using System.Threading.Tasks; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(MockBehavior.Strict); + + mock1.{|PosInfoMoq2019:RaiseAsync|}(m => m.TheEvent += null, ""OK"", 1, new object()); + } + } + + public interface I + { + event CustomEventHandler TheEvent; + } + + public delegate void CustomEventHandler(string a, int b, object c); + }"; + + await Verifier.VerifyAnalyzerAsync(source); + } + + [Fact] + public async Task NoMoqLibrary() + { + var source = @" + namespace ConsoleApplication1 + { + using System; + using OtherNamespace; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(MockBehavior.Strict); + mock1.Raise(m => m.EventWithSenderAndString += null, new EventArgs()); + mock1.Raise(m => m.EventWithSenderAndString += null, 1, 2, 3, 4); + } + } + + public interface I + { + event EventHandler EventWithSenderAndString; + } + } + + namespace OtherNamespace + { + using System; + + public class Mock + { + public Mock(MockBehavior _) { } + + public void Raise(Action act, EventArgs e) { } + public void Raise(Action act, params object[] args) { } + } + + public enum MockBehavior { Strict, Loose } + }"; + + await Verifier.VerifyAnalyzerWithNoMoqLibraryAsync(source); + } + } +} \ No newline at end of file diff --git a/tests/Moq.Analyzers.Tests/Analyzers/ReturnsMethodDelegateMustMatchMockedMethodAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/ReturnsMethodDelegateMustMatchMockedMethodAnalyzerTest.cs index 91607c2..242f738 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/ReturnsMethodDelegateMustMatchMockedMethodAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/ReturnsMethodDelegateMustMatchMockedMethodAnalyzerTest.cs @@ -95,7 +95,7 @@ public void TestMethod() var mock1 = new Mock(); mock1.Setup(i => i.TestMethod()) .Callback(() => { }) - .Returns(() => { return ""Foobar""; }); + .Returns({|#0:() => { return ""Foobar""; }|#0}); } } @@ -108,7 +108,8 @@ public interface I await Verifier.VerifyAnalyzerAsync( source, new DiagnosticResult(ReturnsMethodDelegateMustMatchMockedMethodAnalyzer.ReturnValueMustMatchRule) - .WithSpan(13, 42, 13, 68).WithArguments("Int32")); + .WithLocation(0) + .WithArguments("Int32")); } [Fact] @@ -126,7 +127,7 @@ public void TestMethod() var mock1 = new Mock(); mock1.Setup(i => i.TestProperty) .Callback(() => { }) - .Returns(() => { return ""Foobar""; }); + .Returns({|#0:() => { return ""Foobar""; }|#0}); } } @@ -139,7 +140,8 @@ public interface I await Verifier.VerifyAnalyzerAsync( source, new DiagnosticResult(ReturnsMethodDelegateMustMatchMockedMethodAnalyzer.ReturnValueMustMatchRule) - .WithSpan(13, 42, 13, 68).WithArguments("Int32")); + .WithLocation(0) + .WithArguments("Int32")); } [Theory] diff --git a/tests/Moq.Analyzers.Tests/Analyzers/SetupMethodMustReturnValueWithStrictBehaviorAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/SetupMethodMustReturnValueWithStrictBehaviorAnalyzerTest.cs index e6fdc66..3c7ef17 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/SetupMethodMustReturnValueWithStrictBehaviorAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/SetupMethodMustReturnValueWithStrictBehaviorAnalyzerTest.cs @@ -101,10 +101,10 @@ public void TestMethod() var mock1 = new Mock(MockBehavior.Strict); mock1.Setup(i => i.TestMethod()) .Callback(() => { }) - .Returns(""Foobar""); + .Returns({|#0:""Foobar""|#0}); mock1.Setup(i => i.TestProperty) .Callback(() => { }) - .Returns(""Foobar""); + .Returns({|#1:""Foobar""|#1}); } } @@ -118,8 +118,8 @@ public interface I await Verifier.VerifyAnalyzerAsync( source, - DiagnosticResult.CompilerError("CS1503").WithSpan(13, 42, 13, 50).WithArguments("1", "string", "int"), - DiagnosticResult.CompilerError("CS1503").WithSpan(16, 42, 16, 50).WithArguments("1", "string", "int")); + DiagnosticResult.CompilerError("CS1503").WithLocation(0).WithArguments("1", "string", "int"), + DiagnosticResult.CompilerError("CS1503").WithLocation(1).WithArguments("1", "string", "int")); } [Fact] diff --git a/tests/Moq.Analyzers.Tests/Analyzers/SetupMustBeUsedOnlyForOverridableMembersAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/SetupMustBeUsedOnlyForOverridableMembersAnalyzerTest.cs index 5216328..413aacc 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/SetupMustBeUsedOnlyForOverridableMembersAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/SetupMustBeUsedOnlyForOverridableMembersAnalyzerTest.cs @@ -26,82 +26,118 @@ public void TestMethod() var mock1 = new Mock(); mock1.Setup(i => i.TestMethod()); mock1.Setup(i => i.TestProperty); + mock1.SetupSequence(i => i.TestMethod()); + mock1.SetupSequence(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.SetupSequence(i => i.InnerObject.VirtualMethod()); + mock1.SetupSequence(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.SetupSequence(i => i.InnerObject.AbstractMethod()); + mock1.SetupSequence(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.SetupSequence(i => i.VirtualMethod()); + mock2.SetupSequence(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.SetupSequence(i => i.InnerObject.VirtualMethod()); + mock2.SetupSequence(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.SetupSequence(i => i.InnerObject.AbstractMethod()); + mock2.SetupSequence(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.SetupSequence(i => i.VirtualMethod()); + mock3.SetupSequence(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.SetupSequence(i => i.AbstractMethod()); + mock3.SetupSequence(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.SetupSequence(i => i.InnerObject.VirtualMethod()); + mock3.SetupSequence(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.SetupSequence(i => i.InnerObject.AbstractMethod()); + mock3.SetupSequence(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.SetupSequence(i => i.VirtualMethod()); + mock4.SetupSequence(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.SetupSequence(i => i.AbstractMethod()); + mock4.SetupSequence(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.SetupSequence(i => i.InnerObject.VirtualMethod()); + mock4.SetupSequence(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.SetupSequence(i => i.InnerObject.AbstractMethod()); + mock4.SetupSequence(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.SetupSequence(i => i.VirtualMethod()); + mock5.SetupSequence(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.SetupSequence(i => i.AbstractMethod()); + mock5.SetupSequence(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.SetupSequence(i => i.InnerObject.VirtualMethod()); + mock5.SetupSequence(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.SetupSequence(i => i.InnerObject.AbstractMethod()); + mock5.SetupSequence(i => i.InnerObject.AbstractProperty); mock5.SetupSet(i => i.InnerObject.AbstractProperty = ""Foobard""); mock5.SetupSet(i => i.InnerObject.AbstractProperty = ""Foobard""); } @@ -191,32 +227,44 @@ public void TestMethod() var mock1 = new Mock(); mock1.Setup(i => i.[|Method|]()); mock1.Setup(i => i.[|Property|]); + mock1.SetupSequence(i => i.[|Method|]()); + mock1.SetupSequence(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.SetupSequence(i => i.[|InnerObject|].VirtualMethod()); + mock1.SetupSequence(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.SetupSequence(i => i.AbstractInnerObject.[|Method|]()); + mock1.SetupSequence(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.SetupSequence(i => i.[|SealedInnerObject|].VirtualMethod()); + mock1.SetupSequence(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|]()); + mock2.SetupSequence(i => StandardClass.[|StaticMethod|]()); var mock3 = new Mock(); mock3.Setup(i => i.[|Method|]()); mock3.Setup(i => i.[|Property|]); + mock3.SetupSequence(i => i.[|Method|]()); + mock3.SetupSequence(i => i.[|Property|]); mock3.SetupSet(i => i.[|Property|] = ""1234""); mock3.SetupSet(i => i.[|Property|] = ""1234""); var mock4 = new Mock(); mock4.Setup(i => i.[|ExtensionMethod|]()); + mock4.SetupSequence(i => i.[|ExtensionMethod|]()); } } diff --git a/tests/Moq.Analyzers.Tests/Analyzers/VerifyMustHaveTimesParameterAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/VerifyMustHaveTimesParameterAnalyzerTest.cs index 545ff6f..c924f96 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/VerifyMustHaveTimesParameterAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/VerifyMustHaveTimesParameterAnalyzerTest.cs @@ -123,9 +123,11 @@ await Verifier.VerifyAnalyzerAsync( source, [ new DiagnosticResult(VerifyMustHaveTimesParameterAnalyzer.Rule) - .WithSpan(13, 34, 13, 40).WithArguments("Verify"), + .WithLocation(0) + .WithArguments("Verify"), new DiagnosticResult(VerifyMustHaveTimesParameterAnalyzer.Rule) - .WithSpan(14, 34, 14, 40).WithArguments("Verify"), + .WithLocation(1) + .WithArguments("Verify"), ]); } @@ -159,9 +161,11 @@ await Verifier.VerifyAnalyzerAsync( source, [ new DiagnosticResult(VerifyMustHaveTimesParameterAnalyzer.Rule) - .WithSpan(13, 61, 13, 71).WithArguments("Verifiable"), + .WithLocation(0) + .WithArguments("Verifiable"), new DiagnosticResult(VerifyMustHaveTimesParameterAnalyzer.Rule) - .WithSpan(14, 61, 14, 71).WithArguments("Verifiable"), + .WithLocation(1) + .WithArguments("Verifiable"), ]); } diff --git a/tests/Moq.Analyzers.Tests/Analyzers/VerifyStaticMethodsRequiresMockParametersAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/VerifyStaticMethodsRequiresMockParametersAnalyzerTest.cs index cbb99da..db15b17 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/VerifyStaticMethodsRequiresMockParametersAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/VerifyStaticMethodsRequiresMockParametersAnalyzerTest.cs @@ -71,10 +71,10 @@ await Verifier.VerifyAnalyzerAsync( source, [ new DiagnosticResult(VerifyStaticMethodsRequiresMockParametersAnalyzer.Rule) - .WithSpan(11, 34, 11, 42) + .WithLocation(0) .WithArguments("Verify"), new DiagnosticResult(VerifyStaticMethodsRequiresMockParametersAnalyzer.Rule) - .WithSpan(12, 34, 12, 45) + .WithLocation(1) .WithArguments("VerifyAll"), ]); } diff --git a/tests/Moq.Analyzers.Tests/CodeFixes/AddVerifyAllCodeFixProviderTest.cs b/tests/Moq.Analyzers.Tests/CodeFixes/AddVerifyAllCodeFixProviderTest.cs new file mode 100644 index 0000000..6d61c5b --- /dev/null +++ b/tests/Moq.Analyzers.Tests/CodeFixes/AddVerifyAllCodeFixProviderTest.cs @@ -0,0 +1,329 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) P.O.S Informatique. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace PosInformatique.Moq.Analyzers.Tests +{ + using Verifier = MoqCSharpCodeFixVerifier; + + public class AddVerifyAllCodeFixProviderTest + { + [Fact] + public async Task AddVerifyAllBeforeOtherMocks_Fix() + { + var source = @" +namespace ConsoleApplication1 +{ + using Moq; + using System; + + public class TestClass + { + public void TestMethod() + { + var mock1 = {|PosInfoMoq1000:new Mock()|}; + mock1.Setup(i => i.Method()); + + var mock2 = new Mock(); + mock2.Setup(i => i.Method()); + + var mock3 = new Mock(); + mock3.Setup(i => i.Method()); + + // No changes + mock2.VerifyAll(); + mock3.VerifyAll(); + } + } + + public interface I + { + void Method(); + } +}"; + + var expectedFixedSource = + @" +namespace ConsoleApplication1 +{ + using Moq; + using System; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(); + mock1.Setup(i => i.Method()); + + var mock2 = new Mock(); + mock2.Setup(i => i.Method()); + + var mock3 = new Mock(); + mock3.Setup(i => i.Method()); + + // No changes + mock1.VerifyAll(); + mock2.VerifyAll(); + mock3.VerifyAll(); + } + } + + public interface I + { + void Method(); + } +}"; + + await Verifier.VerifyCodeFixAsync(source, expectedFixedSource); + } + + [Fact] + public async Task AddVerifyAllInTheMiddleOfOtherMocks_Fix() + { + var source = @" +namespace ConsoleApplication1 +{ + using Moq; + using System; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(); + mock1.Setup(i => i.Method()); + + var mock2 = {|PosInfoMoq1000:new Mock()|}; + mock2.Setup(i => i.Method()); + + var mock3 = new Mock(); + mock3.Setup(i => i.Method()); + + // No changes + mock1.VerifyAll(); + mock3.VerifyAll(); + } + } + + public interface I + { + void Method(); + } +}"; + + var expectedFixedSource = + @" +namespace ConsoleApplication1 +{ + using Moq; + using System; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(); + mock1.Setup(i => i.Method()); + + var mock2 = new Mock(); + mock2.Setup(i => i.Method()); + + var mock3 = new Mock(); + mock3.Setup(i => i.Method()); + + // No changes + mock1.VerifyAll(); + mock2.VerifyAll(); + mock3.VerifyAll(); + } + } + + public interface I + { + void Method(); + } +}"; + + await Verifier.VerifyCodeFixAsync(source, expectedFixedSource); + } + + [Fact] + public async Task AddVerifyAllAfterOtherMocks_Fix() + { + var source = @" +namespace ConsoleApplication1 +{ + using Moq; + using System; + + public class TestClass + { + public void TestMethod() + { + var mockzzz = {|PosInfoMoq1000:new Mock()|}; + mockzzz.Setup(i => i.Method()); + + var mock2 = new Mock(); + mock2.Setup(i => i.Method()); + + var mock3 = new Mock(); + mock3.Setup(i => i.Method()); + + // No changes + mock2.VerifyAll(); + mock3.VerifyAll(); + } + } + + public interface I + { + void Method(); + } +}"; + + var expectedFixedSource = + @" +namespace ConsoleApplication1 +{ + using Moq; + using System; + + public class TestClass + { + public void TestMethod() + { + var mockzzz = new Mock(); + mockzzz.Setup(i => i.Method()); + + var mock2 = new Mock(); + mock2.Setup(i => i.Method()); + + var mock3 = new Mock(); + mock3.Setup(i => i.Method()); + + // No changes + mock2.VerifyAll(); + mock3.VerifyAll(); + mockzzz.VerifyAll(); + } + } + + public interface I + { + void Method(); + } +}"; + + await Verifier.VerifyCodeFixAsync(source, expectedFixedSource); + } + + [Fact] + public async Task AddVerifyAllSingleMock() + { + var source = @" +namespace ConsoleApplication1 +{ + using Moq; + using System; + + public class TestClass + { + public void TestMethod() + { + var mock1 = {|PosInfoMoq1000:new Mock()|}; + mock1.Setup(i => i.Method()); + } + } + + public interface I + { + void Method(); + } +}"; + + var expectedFixedSource = + @" +namespace ConsoleApplication1 +{ + using Moq; + using System; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(); + mock1.Setup(i => i.Method()); + + mock1.VerifyAll(); + } + } + + public interface I + { + void Method(); + } +}"; + + await Verifier.VerifyCodeFixAsync(source, expectedFixedSource); + } + + [Fact] + public async Task AddVerifyAllSingleMock_WithCommentAtTheEnd() + { + var source = @" +namespace ConsoleApplication1 +{ + using Moq; + using System; + + public class TestClass + { + public void TestMethod() + { + var mock1 = {|PosInfoMoq1000:new Mock()|}; + mock1.Setup(i => i.Method()); + + // End of unit tests + } + } + + public interface I + { + void Method(); + } +}"; + + var expectedFixedSource = + @" +namespace ConsoleApplication1 +{ + using Moq; + using System; + + public class TestClass + { + public void TestMethod() + { + var mock1 = new Mock(); + mock1.Setup(i => i.Method()); + + // End of unit tests + + mock1.VerifyAll(); + } + } + + public interface I + { + void Method(); + } +}"; + + await Verifier.VerifyCodeFixAsync(source, expectedFixedSource); + } + } +} diff --git a/tests/Moq.Analyzers.Tests/Moq.Analyzers.Tests.csproj b/tests/Moq.Analyzers.Tests/Moq.Analyzers.Tests.csproj index c202d68..84997b4 100644 --- a/tests/Moq.Analyzers.Tests/Moq.Analyzers.Tests.csproj +++ b/tests/Moq.Analyzers.Tests/Moq.Analyzers.Tests.csproj @@ -5,16 +5,16 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all