Skip to content

Commit

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

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.
3 changes: 2 additions & 1 deletion src/Moq.Analyzers/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

Rule ID | Category | Severity | Notes
--------|----------|----------|--------------------
PosInfoMoq2004 | Compilation | Error | ConstructorArgumentCannotBeParsedForInterfaceAnalyzer
PosInfoMoq2004 | Compilation | Error | Constructor arguments cannot be passed for interface mocks.
PosInfoMoq2005 | Compilation | Error | Constructor arguments must match the constructors of the mocked class.

## Release 1.3.0

Expand Down
143 changes: 143 additions & 0 deletions src/Moq.Analyzers/Analyzers/ConstructorArgumentsMustMatchAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
//-----------------------------------------------------------------------
// <copyright file="ConstructorArgumentsMustMatchAnalyzer.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;
using Microsoft.CodeAnalysis.Text;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ConstructorArgumentsMustMatchAnalyzer : DiagnosticAnalyzer
{
internal static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
"PosInfoMoq2005",
"Constructor arguments must match the constructors of the mocked class",
"Constructor arguments must match the constructors of the mocked class",
"Compilation",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Constructor arguments must match the constructors of the mocked class.");

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

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

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

private static void Analyze(SyntaxNodeAnalysisContext context)
{
var objectCreation = (ObjectCreationExpressionSyntax)context.Node;

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

if (moqSymbols is null)
{
return;
}

var moqExpressionAnalyzer = new MoqExpressionAnalyzer(context.SemanticModel);

// Check there is "new Mock<I>()" statement.
var mockedType = moqExpressionAnalyzer.GetMockedType(moqSymbols, objectCreation, out var _, context.CancellationToken);
if (mockedType is null)
{
return;
}

// Check the type is mockable
if (!moqSymbols.IsMockable(mockedType))
{
return;
}

// Check the type is a named type
if (mockedType is not INamedTypeSymbol namedTypeSymbol)
{
return;
}

// Gets the list of the constructor arguments
var constructorArguments = new List<ArgumentSyntax>();

if (objectCreation.ArgumentList is not null)
{
constructorArguments.AddRange(objectCreation.ArgumentList.Arguments);
}

// Gets the first argument, check if it is MockBehavior argument and skip it.
var firstArgument = constructorArguments.FirstOrDefault();

if (firstArgument is not null)
{
if (moqExpressionAnalyzer.IsStrictBehaviorArgument(moqSymbols, firstArgument, out var _, context.CancellationToken))
{
constructorArguments.RemoveAt(0);
}
}

var matchedConstructor = true;

// Iterate on each constructor and check if the arguments match.
foreach (var constructor in namedTypeSymbol.Constructors)
{
matchedConstructor = true;

// If the number of arguments is different, check the next constructor definition.
if (constructor.Parameters.Length != constructorArguments.Count)
{
matchedConstructor = false;
continue;
}

for (var i = 0; i < constructorArguments.Count; i++)
{
var constructorArgumentSymbol = context.SemanticModel.GetTypeInfo(constructorArguments[i].Expression, context.CancellationToken);

if (!SymbolEqualityComparer.Default.Equals(constructorArgumentSymbol.Type, constructor.Parameters[i].Type))
{
matchedConstructor = false;
break;
}
}

if (matchedConstructor)
{
break;
}
}

if (matchedConstructor)
{
return;
}

Location location;

if (constructorArguments.Count == 0)
{
location = objectCreation.ArgumentList!.GetLocation();
}
else
{
var firstLocation = constructorArguments.First().GetLocation();
var lastLocation = constructorArguments.Last().GetLocation();

location = Location.Create(context.Node.SyntaxTree, new TextSpan(firstLocation.SourceSpan.Start, lastLocation.SourceSpan.End - firstLocation.SourceSpan.Start));
}

var diagnostic = Diagnostic.Create(Rule, location);
context.ReportDiagnostic(diagnostic);
}
}
}
12 changes: 1 addition & 11 deletions src/Moq.Analyzers/Analyzers/NoSealedClassAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
return;
}

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

if (mockedType.IsAbstract)
{
return;
}

if (!mockedType.IsSealed)
if (moqSymbols.IsMockable(mockedType))
{
return;
}
Expand Down
20 changes: 20 additions & 0 deletions src/Moq.Analyzers/MoqSymbols.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,5 +230,25 @@ public bool IsOverridable(ISymbol method)

return false;
}

public bool IsMockable(ITypeSymbol type)
{
if (type.TypeKind == TypeKind.Interface)
{
return true;
}

if (type.IsAbstract)
{
return true;
}

if (!type.IsSealed)
{
return true;
}

return false;
}
}
}
15 changes: 15 additions & 0 deletions tests/Moq.Analyzers.Sandbox/Moq.Analyzers.Sandbox.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Moq" Version="4.20.70" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Moq.Analyzers\Moq.Analyzers.csproj" PrivateAssets="all" ReferenceOutputAssembly="false" OutputItemType="Analyzer" Condition="'$(Configuration)' == 'Debug'" />
</ItemGroup>

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

namespace PosInformatique.Moq.Analyzers.Sandbox
{
public class Sandbox
{
public void TestCodeHere()
{
}

public class Test
{
public Test(int a)
{
}
}
}
}
Loading

0 comments on commit 3d87963

Please sign in to comment.