Skip to content

Commit

Permalink
v1.6.0 (#24)
Browse files Browse the repository at this point in the history
- Add new rules:
  - PosInfoMoq2007: The `As<T>()` method can be used only with interfaces (fixes: #19).
  - PosInfoMoq2008: The `Verify()` method must be used only on overridable members.
- Add the support of static methods `VerifyAll()` and `Verify()` for the PosInfoMoq2000 rule (fixes: #20).
- Various optimizations to increase speed of analysis.
- Various optimizations to reduce memory usage.
- Add hyperlink to the documentation of the rules.
  • Loading branch information
GillesTourreau authored Jun 24, 2024
1 parent f19dd54 commit da2f494
Show file tree
Hide file tree
Showing 29 changed files with 1,007 additions and 135 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/github-actions-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
type: string
description: The version of the library
required: true
default: 1.5.0
default: 1.6.0
VersionSuffix:
type: string
description: The version suffix of the library (for example rc.1)
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@

<!-- Common NuGet packages -->
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
2 changes: 2 additions & 0 deletions PosInformatique.Moq.Analyzers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Compilation", "Compilation"
docs\Compilation\PosInfoMoq2004.md = docs\Compilation\PosInfoMoq2004.md
docs\Compilation\PosInfoMoq2005.md = docs\Compilation\PosInfoMoq2005.md
docs\Compilation\PosInfoMoq2006.md = docs\Compilation\PosInfoMoq2006.md
docs\Compilation\PosInfoMoq2007.md = docs\Compilation\PosInfoMoq2007.md
docs\Compilation\PosInfoMoq2008.md = docs\Compilation\PosInfoMoq2008.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}"
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,8 @@ All the rules of this category should not be disabled (or changed their severity
| [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. |
| [PosInfoMoq2007: The `As<T>()` method can be used only with interfaces.](docs/Compilation/PosInfoMoq2007.md) | The `As<T>()` can only be use with the interfaces. |
| [PosInfoMoq2008: The `Verify()` method must be used only on overridable members](docs/Compilation/PosInfoMoq2008.md)) | The `Verify()` method must be applied only for overridable members. |



4 changes: 2 additions & 2 deletions docs/Compilation/PosInfoMoq2001.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
## Cause

The `Setup()` method must be applied only for overridable members.
An overridable member is a **methode** or **property** which is in:
An overridable member is a **method** or **property** which is in:
- An `interface`.
- A non-`sealed` `class`. In this case, the member must be:
- Defines as `abstract`.
Expand Down Expand Up @@ -58,4 +58,4 @@ To fix a violation of this rule, be sure to mock a member in the `Setup()` metho
## 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 *"Extensions methods may not be used in setup/verification expressions"* message.
thrown with the *"Unsupported expression: m => m.Method(). Non-overridable members (here: Namespace.Class.Method) may not be used in setup / verification expressions."* message.
2 changes: 1 addition & 1 deletion docs/Compilation/PosInfoMoq2006.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ and must be overridable (`virtual`, `abstract` and `override`, but not `sealed`)
[Fact]
public void Test()
{
var service = new Mock<Service>(1, 2, 3);
var service = new Mock<Service>();
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.
Expand Down
45 changes: 45 additions & 0 deletions docs/Compilation/PosInfoMoq2007.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# PosInfoMoq2007: The `As<T>()` method can be used only with interfaces.

| Property | Value |
|-------------------------------------|-----------------------------------------------------------------|
| **Rule ID** | PosInfoMoq2007 |
| **Title** | The `As<T>()` method can be used only with interfaces. |
| **Category** | Compilation |
| **Default severity** | Error |

## Cause

The `As<T>()` method is used with a type which is not an interface.

## Rule description

Moq allows to add additional implementations for mocked class (or interface) by adding additional interfaces
with the `As<T>()` method.

```csharp
[Fact]
public void Test()
{
var service = new Mock<Service>();
service.As<IDisposable>() // Add IDisposable implementation for the mocked Service class.
.Setup(s => s.Dispose());
service.As<OtherService>(); // An error will be raised, because we can't mock additional implementation of a class.
}

public abstract class Service
{
}

public abstract class OtherService
{
}
```

## How to fix violations

To fix a violation of this rule, use an interface when using the `As<T>()` 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 *"Can only add interfaces to the mock."* message.
61 changes: 61 additions & 0 deletions docs/Compilation/PosInfoMoq2008.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# PosInfoMoq2008: The `Verify()` method must be used only on overridable members

| Property | Value |
|-------------------------------------|---------------------------------------------------------------|
| **Rule ID** | PosInfoMoq2008 |
| **Title** | The `Verify()` method must be used only on overridable members |
| **Category** | Compilation |
| **Default severity** | Error |

## Cause

The `Verify()` method 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:
- Defines as `abstract`.
- Or defined as `virtual`

## Rule description

The `Verify()` method must be applied only for overridable members.

For example, the following methods and properties can be mock and used in the `Verify()` method:
- `IService.MethodCanBeMocked()`
- `IService.PropertyCanBeMocked`
- `Service.VirtualMethodCanBeMocked`
- `Service.VirtualPropertyCanBeMocked`
- `Service.AbstractMethodCanBeMocked`
- `Service.AbstractPropertyCanBeMocked`

```csharp
public interface IService
{
void MethodCanBeMocked();

string PropertyCanBeMocked { get; set; }
}

public abstract class Service
{
public virtual void VirtualMethodCanBeMocked() { ... }

public virtual void VirtualPropertyCanBeMocked() { ... }

public abstract void AbstractMethodCanBeMocked();

public abstract void AbstractPropertyCanBeMocked();
}
```

> **NOTE**: The extension methods can not be overriden. The C# syntax looks like a member method an interface or class, but the extension method are just simple
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 `Verify()` method which can be overriden.

## 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 *"Unsupported expression: m => m.Method(). Non-overridable members (here: Namespace.Class.Method) may not be used in setup / verification expressions."* message.
11 changes: 10 additions & 1 deletion src/Moq.Analyzers/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
## Release 1.5.0
## Release 1.6.0

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|--------------------
PosInfoMoq2007 | Compilation | Error | The `As<T>()` method can be used only with interfaces.
PosInfoMoq2008 | Compilation | Error | The `Verify()` method can be used only on overridable members.

## Release 1.5.0

### New Rules

Expand Down
67 changes: 67 additions & 0 deletions src/Moq.Analyzers/Analyzers/AsMustBeUsedWithInterfaceAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//-----------------------------------------------------------------------
// <copyright file="AsMustBeUsedWithInterfaceAnalyzer.cs" company="P.O.S Informatique">
// Copyright (c) P.O.S Informatique. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

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 AsMustBeUsedWithInterfaceAnalyzer : DiagnosticAnalyzer
{
internal static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
"PosInfoMoq2007",
"The As<T>() method can be used only with interfaces",
"The As<T>() method can be used only with interfaces",
"Compilation",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "The As<T>() method can be used only with interfaces.",
helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2007.html");

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();

context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression);
}

private static void Analyze(SyntaxNodeAnalysisContext context)
{
var invocationExpression = (InvocationExpressionSyntax)context.Node;

var moqSymbols = MoqSymbols.FromCompilation(context.Compilation);

if (moqSymbols is null)
{
return;
}

var moqExpressionAnalyzer = new MoqExpressionAnalyzer(moqSymbols, context.SemanticModel);

var asMethodType = moqExpressionAnalyzer.ExtractAsMethodType(invocationExpression, out var typeSyntax, context.CancellationToken);

if (asMethodType is null)
{
return;
}

if (asMethodType.TypeKind == TypeKind.Interface)
{
return;
}

var diagnostic = Diagnostic.Create(Rule, typeSyntax!.GetLocation());
context.ReportDiagnostic(diagnostic);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ public class CallBackDelegateMustMatchMockedMethodAnalyzer : DiagnosticAnalyzer
"Compilation",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "The Callback() delegate expression must match the signature of the mocked method.");
description: "The Callback() delegate expression must match the signature of the mocked method.",
helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2003.html");

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

Expand All @@ -45,8 +46,6 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
return;
}

var moqExpressionAnalyzer = new MoqExpressionAnalyzer(context.SemanticModel);

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

Expand All @@ -56,6 +55,8 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
}

// If yes, we extract the lambda expression of it.
var moqExpressionAnalyzer = new MoqExpressionAnalyzer(moqSymbols, context.SemanticModel);

var callBackLambdaExpressionSymbol = moqExpressionAnalyzer.ExtractCallBackLambdaExpressionMethod(invocationExpression, out var lambdaExpression, context.CancellationToken);

if (callBackLambdaExpressionSymbol is null)
Expand All @@ -70,6 +71,11 @@ 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.
if (!moqExpressionAnalyzer.IsMockSetupMethod(followingMethod, out var _, context.CancellationToken))
{
continue;
}

var mockedMethod = moqExpressionAnalyzer.ExtractSetupMethod(followingMethod, out var _, context.CancellationToken);

if (mockedMethod is null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ public class ConstructorArgumentCannotBePassedForInterfaceAnalyzer : DiagnosticA
"Compilation",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Constructor arguments cannot be passed for interface mocks.");
description: "Constructor arguments cannot be passed for interface mocks.",
helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2004.html");

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

Expand All @@ -45,10 +46,10 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
return;
}

var moqExpressionAnalyzer = new MoqExpressionAnalyzer(context.SemanticModel);
var moqExpressionAnalyzer = new MoqExpressionAnalyzer(moqSymbols, context.SemanticModel);

// Check there is "new Mock<I>()" statement.
var mockedType = moqExpressionAnalyzer.GetMockedType(moqSymbols, objectCreation, out var typeExpression, context.CancellationToken);
var mockedType = moqExpressionAnalyzer.GetMockedType(objectCreation, out var typeExpression, context.CancellationToken);
if (mockedType is null)
{
return;
Expand Down Expand Up @@ -77,7 +78,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
// Check if the first argument is MockBehavior argument
var argumentCheckStart = 0;

if (moqExpressionAnalyzer.IsStrictBehaviorArgument(moqSymbols, firstArgument, out var _, context.CancellationToken))
if (moqExpressionAnalyzer.IsStrictBehaviorArgument(firstArgument, out var _, context.CancellationToken))
{
argumentCheckStart = 1;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ public class ConstructorArgumentsMustMatchAnalyzer : DiagnosticAnalyzer
"Compilation",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Constructor arguments must match the constructors of the mocked class.");
description: "Constructor arguments must match the constructors of the mocked class.",
helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2005.html");

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

Expand All @@ -46,10 +47,10 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
return;
}

var moqExpressionAnalyzer = new MoqExpressionAnalyzer(context.SemanticModel);
var moqExpressionAnalyzer = new MoqExpressionAnalyzer(moqSymbols, context.SemanticModel);

// Check there is "new Mock<I>()" statement.
var mockedType = moqExpressionAnalyzer.GetMockedType(moqSymbols, objectCreation, out var _, context.CancellationToken);
var mockedType = moqExpressionAnalyzer.GetMockedType(objectCreation, out var _, context.CancellationToken);
if (mockedType is null)
{
return;
Expand Down Expand Up @@ -80,7 +81,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context)

if (firstArgument is not null)
{
if (moqExpressionAnalyzer.IsStrictBehaviorArgument(moqSymbols, firstArgument, out var _, context.CancellationToken))
if (moqExpressionAnalyzer.IsStrictBehaviorArgument(firstArgument, out var _, context.CancellationToken))
{
constructorArguments.RemoveAt(0);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ public class MockInstanceShouldBeStrictBehaviorAnalyzer : DiagnosticAnalyzer
"Design",
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "The Mock<T> instance behavior should be defined to Strict mode.");
description: "The Mock<T> instance behavior should be defined to Strict mode.",
helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Design/PosInfoMoq1001.html");

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

Expand All @@ -47,15 +48,15 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
return;
}

var moqExpressionAnalyzer = new MoqExpressionAnalyzer(context.SemanticModel);
var moqExpressionAnalyzer = new MoqExpressionAnalyzer(moqSymbols, context.SemanticModel);

// Check there is "new Mock<I>()" statement.
if (!moqExpressionAnalyzer.IsMockCreation(moqSymbols, objectCreation, context.CancellationToken))
if (!moqExpressionAnalyzer.IsMockCreation(objectCreation, context.CancellationToken))
{
return;
}

if (!moqExpressionAnalyzer.IsStrictBehavior(moqSymbols, objectCreation, context.CancellationToken))
if (!moqExpressionAnalyzer.IsStrictBehavior(objectCreation, context.CancellationToken))
{
var diagnostic = Diagnostic.Create(Rule, objectCreation.GetLocation());
context.ReportDiagnostic(diagnostic);
Expand Down
Loading

0 comments on commit da2f494

Please sign in to comment.