Skip to content

Commit

Permalink
Add the rule PosInfoMoq2006 to check Protected() method setup.
Browse files Browse the repository at this point in the history
  • Loading branch information
GillesTourreau committed Jun 7, 2024
1 parent 7ac080f commit 4f28a5e
Show file tree
Hide file tree
Showing 10 changed files with 532 additions and 3 deletions.
1 change: 1 addition & 0 deletions PosInformatique.Moq.Analyzers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Compilation", "Compilation"
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}"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@ All the rules of this category should not be disabled (or changed their severity
| [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. |

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 have the following criteria:
- Is not existing
- Is not virtual
- Is not abstract

## Rule description

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

```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` or `abstract`).

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.
1 change: 1 addition & 0 deletions src/Moq.Analyzers/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//-----------------------------------------------------------------------
// <copyright file="SetupProtectedMustBeUsedWithProtectedOrInternalMembersAnalyzer.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 SetupProtectedMustBeUsedWithProtectedOrInternalMembersAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
"PosInfoMoq2006",
"The Protected().Setup() method must be use with overridable protected or internal methods",
"The Protected().Setup() method must be use with overridable protected or internal methods",
"Compilation",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "The Protected().Setup() method must be use with overridable protected or internal methods.");

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(context.SemanticModel);

// Check is Protected() method.
if (!moqExpressionAnalyzer.IsMockSetupMethodProtected(moqSymbols, invocationExpression, out var localVariableExpression, context.CancellationToken))
{
return;
}

// Gets the first argument to retrieve the name of the method.
if (invocationExpression.ArgumentList is null)
{
return;
}

if (invocationExpression.ArgumentList.Arguments.Count == 0)
{
return;
}

if (invocationExpression.ArgumentList.Arguments[0].Expression is not LiteralExpressionSyntax literalExpression)
{
return;
}

var methodName = literalExpression.Token.ValueText;

// Gets the mocked type
var mockedType = moqExpressionAnalyzer.GetMockedType(moqSymbols, localVariableExpression!, context.CancellationToken);

if (mockedType is null)
{
return;
}

// Check if a method exists with the specified name
foreach (var method in mockedType.GetMembers(methodName).OfType<IMethodSymbol>())
{
if (!method.IsAbstract && !method.IsVirtual)
{
continue;
}

if (method.DeclaredAccessibility == Accessibility.Protected)
{
return;
}

if (method.DeclaredAccessibility == Accessibility.Internal)
{
return;
}

if (method.DeclaredAccessibility == Accessibility.ProtectedOrInternal)
{
return;
}
}

// No returns method has been specified with Strict mode. Report the diagnostic issue.
var diagnostic = Diagnostic.Create(Rule, literalExpression.GetLocation());
context.ReportDiagnostic(diagnostic);
}
}
}
1 change: 1 addition & 0 deletions src/Moq.Analyzers/Moq.Analyzers.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- Add new rules:
- PosInfoMoq2004: Check that constructor arguments can not be passed to a mocked interface.
- PosInfoMoq2005: Check the constructors arguments passed to mocked class.
- PosInfoMoq2006: Check that the setup method with Protected() exists, are overridable and is protected or internal.

1.4.0
- Reduce the dependency of Microsoft.CodeAnalysis.CSharp to the release 4.0.1.
Expand Down
87 changes: 87 additions & 0 deletions src/Moq.Analyzers/MoqExpressionAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

namespace PosInformatique.Moq.Analyzers
{
using System.Data;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

Expand All @@ -28,6 +29,33 @@ public bool IsMockCreation(MoqSymbols moqSymbols, ObjectCreationExpressionSyntax
return true;
}

public ITypeSymbol? GetMockedType(MoqSymbols moqSymbols, IdentifierNameSyntax expression, CancellationToken cancellationToken)
{
var symbolInfo = this.semanticModel.GetSymbolInfo(expression, cancellationToken);

if (symbolInfo.Symbol is not ILocalSymbol localVariableSymbol)
{
return null;
}

if (localVariableSymbol.Type is not INamedTypeSymbol typeSymbol)
{
return null;
}

if (!moqSymbols.IsMock(typeSymbol))
{
return null;
}

if (typeSymbol.TypeArguments.Length != 1)
{
return null;
}

return typeSymbol.TypeArguments[0];
}

public ITypeSymbol? GetMockedType(MoqSymbols moqSymbols, ObjectCreationExpressionSyntax expression, out TypeSyntax? typeExpression, CancellationToken cancellationToken)
{
typeExpression = null;
Expand Down Expand Up @@ -94,6 +122,65 @@ public bool IsMockSetupMethod(MoqSymbols moqSymbols, InvocationExpressionSyntax
return true;
}

public bool IsMockSetupMethodProtected(MoqSymbols moqSymbols, InvocationExpressionSyntax invocationExpression, out IdentifierNameSyntax? localVariableExpression, CancellationToken cancellationToken)
{
localVariableExpression = null;

// Gets the member access expression "mock.XXXXX"
if (invocationExpression.Expression is not MemberAccessExpressionSyntax memberAccessExpression)
{
return false;
}

if (memberAccessExpression.Expression is not InvocationExpressionSyntax protectedInvocationExpression)
{
return false;
}

// Check it is a Protected() method
var method = this.semanticModel.GetSymbolInfo(protectedInvocationExpression.Expression, cancellationToken);

if (!moqSymbols.IsProtectedMethod(method.Symbol))
{
return false;
}

// Retrieve the "mock" variable
if (protectedInvocationExpression.Expression is not MemberAccessExpressionSyntax protectedMemberAccessExpression)
{
return false;
}

if (protectedMemberAccessExpression.Expression is not IdentifierNameSyntax identifierName)
{
return false;
}

localVariableExpression = identifierName;

var instanceVariable = this.semanticModel.GetSymbolInfo(identifierName, cancellationToken);

if (instanceVariable.Symbol is not ILocalSymbol instanceVariableSymbol)
{
return false;
}

if (!moqSymbols.IsMock(instanceVariableSymbol.Type))
{
return false;
}

// Gets the method and check it is Setup() method.
var methodSymbolInfo = this.semanticModel.GetSymbolInfo(invocationExpression.Expression, cancellationToken);

if (!moqSymbols.IsSetupProtectedMethod(methodSymbolInfo.Symbol))
{
return false;
}

return true;
}

public bool IsStrictBehavior(MoqSymbols moqSymbols, ObjectCreationExpressionSyntax mockCreationExpression, CancellationToken cancellationToken)
{
// Check that the "new Mock<I>()" statement have at least one argument (else Strict is missing...).
Expand Down
49 changes: 47 additions & 2 deletions src/Moq.Analyzers/MoqSymbols.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,21 @@ internal sealed class MoqSymbols

private readonly IReadOnlyList<IMethodSymbol> setupMethods;

private readonly IReadOnlyList<IMethodSymbol> setupProtectedMethods;

private readonly ISymbol mockBehaviorStrictField;

private readonly ISymbol isAnyTypeClass;

private MoqSymbols(INamedTypeSymbol mockClass, INamedTypeSymbol mockBehaviorEnum, ISymbol isAnyTypeClass)
private MoqSymbols(INamedTypeSymbol mockClass, INamedTypeSymbol mockBehaviorEnum, ISymbol isAnyTypeClass, INamedTypeSymbol protectedMockInterface)
{
this.mockClass = mockClass;
this.mockBehaviorEnum = mockBehaviorEnum;
this.isAnyTypeClass = isAnyTypeClass;

this.setupMethods = mockClass.GetMembers("Setup").OfType<IMethodSymbol>().ToArray();
this.mockBehaviorStrictField = mockBehaviorEnum.GetMembers("Strict").First();
this.setupProtectedMethods = protectedMockInterface.GetMembers("Setup").OfType<IMethodSymbol>().ToArray();
}

public static MoqSymbols? FromCompilation(Compilation compilation)
Expand All @@ -53,7 +56,14 @@ private MoqSymbols(INamedTypeSymbol mockClass, INamedTypeSymbol mockBehaviorEnum
return null;
}

return new MoqSymbols(mockClass, mockBehaviorEnum, isAnyTypeClass);
var protectedMockInterface = compilation.GetTypeByMetadataName("Moq.Protected.IProtectedMock`1");

if (protectedMockInterface is null)
{
return null;
}

return new MoqSymbols(mockClass, mockBehaviorEnum, isAnyTypeClass, protectedMockInterface);
}

public bool IsAnyType(ITypeSymbol symbol)
Expand Down Expand Up @@ -101,6 +111,26 @@ public bool IsSetupMethod(ISymbol? symbol)
return false;
}

public bool IsSetupProtectedMethod(ISymbol? symbol)
{
if (symbol is null)
{
return false;
}

var originalDefinition = symbol.OriginalDefinition;

foreach (var setupProtectedMethod in this.setupProtectedMethods)
{
if (SymbolEqualityComparer.Default.Equals(originalDefinition, setupProtectedMethod))
{
return true;
}
}

return false;
}

public bool IsCallback(ISymbol? symbol)
{
if (symbol is null)
Expand All @@ -116,6 +146,21 @@ public bool IsCallback(ISymbol? symbol)
return true;
}

public bool IsProtectedMethod(ISymbol? symbol)
{
if (symbol is null)
{
return false;
}

if (symbol.Name != "Protected")
{
return false;
}

return true;
}

public bool IsReturnsMethod(ISymbol? symbol)
{
if (symbol is null)
Expand Down
Loading

0 comments on commit 4f28a5e

Please sign in to comment.