Skip to content

Commit

Permalink
v1.5.0 (#18)
Browse files Browse the repository at this point in the history
New rules:
- **PosInfoMoq2004**: Check that no arguments is passed to mocked interface (fixes #14).
- **PosInfoMoq2005**: Check the arguments passed to mocked abstract classes (fixes #15).
- **PosInfoMoq2006**: Check setups with Protected() mocks (fixes #10 and fixes #11).

Improvements/fixes:
- The **PosInfoMoq2000** rule does not raise an error when the `Returns()`/`ReturnsAsync()` methods is invalided by the compiler (fixes #16).
- The **PosInfoMoq2001** rule check that each chained member access in a `Setup()` method is overridable (fixes #17). 

Internal:
- Propagate the cancellation token to allow to cancel analysis if the host (the compiler for example) cancel the analysis process.
- Migrates the Azure Pipelines to Github actions (fixes #9).
  • Loading branch information
GillesTourreau authored Jun 10, 2024
1 parent 6880a50 commit f19dd54
Show file tree
Hide file tree
Showing 34 changed files with 2,001 additions and 230 deletions.
24 changes: 24 additions & 0 deletions .github/workflows/github-actions-ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Continuous Integration

on:
pull_request:
branches: [ "main" ]
push:
branches: [ "releases/**" ]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup .NET 8.x
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.x'

- name: Build
run: dotnet build --property:Configuration=Debug "PosInformatique.Moq.Analyzers.sln"

- name: Test with the dotnet CLI
run: dotnet test --property:Configuration=Debug "PosInformatique.Moq.Analyzers.sln"
36 changes: 36 additions & 0 deletions .github/workflows/github-actions-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Release

on:
workflow_dispatch:
inputs:
VersionPrefix:
type: string
description: The version of the library
required: true
default: 1.5.0
VersionSuffix:
type: string
description: The version suffix of the library (for example rc.1)

run-name: ${{ inputs.VersionPrefix }}-${{ inputs.VersionSuffix }}

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup .NET 8.x
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.x'

- name: Build
run: dotnet pack
--property:Configuration=Release
--property:VersionPrefix=${{ github.event.inputs.VersionPrefix }}
--property:VersionSuffix=${{ github.event.inputs.VersionSuffix }}
"src/Moq.Analyzers/Moq.Analyzers.csproj"

- name: Publish the package to nuget.org
run: dotnet nuget push "src/Moq.Analyzers/bin/Release/*.nupkg" --api-key "${{ secrets.NUGET_APIKEY }}" --source https://api.nuget.org/v3/index.json
16 changes: 9 additions & 7 deletions PosInformatique.Moq.Analyzers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
stylecop.json = stylecop.json
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{4C3E1C72-8977-4ABD-8360-A8707DBBA5AE}"
ProjectSection(SolutionItems) = preProject
build\azure-pipelines-ci.yaml = build\azure-pipelines-ci.yaml
build\azure-pipelines-release.yaml = build\azure-pipelines-release.yaml
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{DC2CDF77-88A9-490D-84ED-34832943104A}"
ProjectSection(SolutionItems) = preProject
tests\.editorconfig = tests\.editorconfig
Expand All @@ -43,8 +37,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Compilation", "Compilation"
docs\Compilation\PosInfoMoq2001.md = docs\Compilation\PosInfoMoq2001.md
docs\Compilation\PosInfoMoq2002.md = docs\Compilation\PosInfoMoq2002.md
docs\Compilation\PosInfoMoq2003.md = docs\Compilation\PosInfoMoq2003.md
docs\Compilation\PosInfoMoq2004.md = docs\Compilation\PosInfoMoq2004.md
docs\Compilation\PosInfoMoq2005.md = docs\Compilation\PosInfoMoq2005.md
docs\Compilation\PosInfoMoq2006.md = docs\Compilation\PosInfoMoq2006.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}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -59,12 +58,15 @@ Global
{1962BEF9-E6DF-4485-A113-E255C84177D4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1962BEF9-E6DF-4485-A113-E255C84177D4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1962BEF9-E6DF-4485-A113-E255C84177D4}.Release|Any CPU.Build.0 = Release|Any CPU
{07F970A1-1477-4D4C-B233-C9B4DA6E3AD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{07F970A1-1477-4D4C-B233-C9B4DA6E3AD6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{07F970A1-1477-4D4C-B233-C9B4DA6E3AD6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{07F970A1-1477-4D4C-B233-C9B4DA6E3AD6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{4C3E1C72-8977-4ABD-8360-A8707DBBA5AE} = {1D59B801-B4D3-44FC-A2BE-F2F53AC54061}
{DC2CDF77-88A9-490D-84ED-34832943104A} = {1D59B801-B4D3-44FC-A2BE-F2F53AC54061}
{3C20D95F-AB5F-44EC-8FB6-CB9827B7FD63} = {1D59B801-B4D3-44FC-A2BE-F2F53AC54061}
{815BE8D0-C7D5-4B4E-82E0-DE29C11F258E} = {3C20D95F-AB5F-44EC-8FB6-CB9827B7FD63}
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,7 @@ All the rules of this category should not be disabled (or changed their severity
| [PosInfoMoq2001: The `Setup()` method must be used only on overridable members](docs/Compilation/PosInfoMoq2001.md)) | The `Setup()` method must be applied only for overridable members. |
| [PosInfoMoq2002: `Mock<T>` class can be used only to mock non-sealed class](docs/Compilation/PosInfoMoq2002.md) | The `Mock<T>` 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. |
| [PosInfoMoq2005: Constructor arguments must match the constructors of the mocked class](docs/Compilation/PosInfoMoq2005.md) | When instantiating a `Mock<T>`, the parameters must match one of the constructors of the mocked type. |
| [PosInfoMoq2006: The Protected().Setup() method must be use with overridable protected or internal methods](docs/Compilation/PosInfoMoq2006.md) | When using the `Protected().Setup()` configuration, the method mocked must be overridable and protected or internal. |

24 changes: 0 additions & 24 deletions build/azure-pipelines-ci.yaml

This file was deleted.

54 changes: 0 additions & 54 deletions build/azure-pipelines-release.yaml

This file was deleted.

39 changes: 39 additions & 0 deletions docs/Compilation/PosInfoMoq2004.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# PosInfoMoq2004: The `Callback()` delegate expression must match the signature of the mocked method

| Property | Value |
|-------------------------------------|---------------------------------------------------------------------|
| **Rule ID** | PosInfoMoq2004 |
| **Title** | Constructor arguments cannot be passed for interface mocks. |
| **Category** | Compilation |
| **Default severity** | Error |

## Cause

Constructor arguments has been passed to a mocked interface.

## Rule description

It is not possible to pass contructor arguments to mocked interface. Is only possible with non-sealed class
or abstract class.

```csharp
[Fact]
public void Test()
{
var service1 = new Mock<IService>("Argument 1", 2); // No constructor arguments can be passed to mocked interface.
var service2 = new Mock<IService>(MockBehavior.Strict, "Argument 1", 2); // Same if we use the MockBehavior.
}

public interface IService
{
}
```

## How to fix violations

To fix a violation of this rule, be sure to pass parameters to mocked abstract class.

## When to suppress warnings

Do not suppress an error from this rule. If bypassed, the execution of the unit test will be failed with a `MoqException`
thrown with the *"Constructor arguments cannot be passed for interface mocks."* message.
46 changes: 46 additions & 0 deletions docs/Compilation/PosInfoMoq2005.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# PosInfoMoq2005: The `Callback()` delegate expression must match the signature of the mocked method

| Property | Value |
|-------------------------------------|-------------------------------------------------------------------------|
| **Rule ID** | PosInfoMoq2005 |
| **Title** | Constructor arguments must match the constructors of the mocked class. |
| **Category** | Compilation |
| **Default severity** | Error |

## Cause

Constructor arguments must match the constructors of the mocked class.

## Rule description

When configurate mock, all the arguments must match one of the constructor of the mocked type.

```csharp
[Fact]
public void Test()
{
var service1 = new Mock<Service>(1, 2, 3); // The arguments does not match one of the Service type constructors.
var service2 = new Mock<Service>("Argument 1", 2); // OK
var service3 = new Mock<Service>(MockBehavior.Strict, "Argument 1", 2); // OK
}

public abstract class Service
{
public Service(string a)
{
}

public Service(string a, int b)
{
}
}
```

## How to fix violations

To fix a violation of this rule, be sure to pass right arguments of one of the constructor of the mocked instance.

## When to suppress warnings

Do not suppress an error from this rule. If bypassed, the execution of the unit test will be failed with a `MoqException`
thrown with the *"Constructor arguments must match the constructors of the mocked class."* message.
56 changes: 56 additions & 0 deletions docs/Compilation/PosInfoMoq2006.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# PosInfoMoq2006: The Protected().Setup() method must be use with overridable protected or internal methods

| Property | Value |
|-------------------------------------|----------------------------------------------------------------------------------------------|
| **Rule ID** | PosInfoMoq2006 |
| **Title** | The `Protected().Setup()` method must be use with overridable protected or internal methods. |
| **Category** | Compilation |
| **Default severity** | Error |

## Cause

A `Protected().Setup()` reference a method in the mocked type which is:
- Not existing in the mocked type.
- Not overridable (`abstract`, `virtual` or `override`, but not `sealed`).
- Not `protected` or `internal`.

## Rule description

When using the `Protected().Setup()`, the method mocked must be `protected`, `internal` or `protected internal`,
and must be overridable (`virtual`, `abstract` and `override`, but not `sealed`).

```csharp
[Fact]
public void Test()
{
var service = new Mock<Service>(1, 2, 3);
service.Protected().Setup("GetData") // The GetData() is public and can be mocked with Protected() feature.
.Returns(10);
service.Protected().Setup("NotExists") // The NotExists() method does not exist.
.Returns(10);
service.Protected().Setup("YouCantOverrideMe") // The YouCantOverrideMe() is not virtual or abstract.
.Returns(10);
}

public abstract class Service
{
public abstract int GetData();

protected void YouCantOverrideMe() { };
}
```

## How to fix violations

To fix a violation of this rule, use the `Protected().Setup()` to mock method which are:
- `protected`
- `internal`
- `protected internal`
- Overridable (`virtual`, `abstract` or `override`, but not `sealed`).

Else use the standard mocking feature without the `Protected()` method.

## When to suppress warnings

Do not suppress an error from this rule. If bypassed, the execution of the unit test will be failed with a `MoqException`
thrown with the *"Method X.xxxx is public. Use strong-typed."* message.
12 changes: 11 additions & 1 deletion src/Moq.Analyzers/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
## Release 1.3.0
## Release 1.5.0

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|--------------------
PosInfoMoq2004 | Compilation | Error | Constructor arguments cannot be passed for interface mocks.
PosInfoMoq2005 | Compilation | Error | Constructor arguments must match the constructors of the mocked class.
PosInfoMoq2006 | Compilation | Error | The `Protected().Setup()` method must be use with overridable protected or internal methods.

## Release 1.3.0

### New Rules

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
var moqExpressionAnalyzer = new MoqExpressionAnalyzer(context.SemanticModel);

// Try to determine if the invocation expression is a Callback() expression.
var methodSymbol = context.SemanticModel.GetSymbolInfo(invocationExpression);
var methodSymbol = context.SemanticModel.GetSymbolInfo(invocationExpression, context.CancellationToken);

if (!moqSymbols.IsCallback(methodSymbol.Symbol))
{
return;
}

// If yes, we extract the lambda expression of it.
var callBackLambdaExpressionSymbol = moqExpressionAnalyzer.ExtractCallBackLambdaExpressionMethod(invocationExpression, out var lambdaExpression);
var callBackLambdaExpressionSymbol = moqExpressionAnalyzer.ExtractCallBackLambdaExpressionMethod(invocationExpression, out var lambdaExpression, context.CancellationToken);

if (callBackLambdaExpressionSymbol is null)
{
Expand All @@ -70,7 +70,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
{
// Find the symbol of the mocked method (if not symbol found, it is mean we Setup() method that not currently compile)
// so we skip the analysis.
var mockedMethod = moqExpressionAnalyzer.ExtractSetupMethod(followingMethod, out var _);
var mockedMethod = moqExpressionAnalyzer.ExtractSetupMethod(followingMethod, out var _, context.CancellationToken);

if (mockedMethod is null)
{
Expand Down
Loading

0 comments on commit f19dd54

Please sign in to comment.