Skip to content

Commit

Permalink
Add the PosInfoMoq2007 rule to check the As<T>() use interface.
Browse files Browse the repository at this point in the history
  • Loading branch information
GillesTourreau committed Jun 12, 2024
1 parent f19dd54 commit 8f8d95d
Show file tree
Hide file tree
Showing 10 changed files with 317 additions and 2 deletions.
1 change: 1 addition & 0 deletions PosInformatique.Moq.Analyzers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ 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
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,6 @@ 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. |


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.
10 changes: 9 additions & 1 deletion src/Moq.Analyzers/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
## 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.

## Release 1.5.0

### New Rules

Expand Down
66 changes: 66 additions & 0 deletions src/Moq.Analyzers/Analyzers/AsMustBeUsedWithInterfaceAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//-----------------------------------------------------------------------
// <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.");

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);

var asMethodType = moqExpressionAnalyzer.ExtractAsMethodType(moqSymbols, 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);
}
}
}
31 changes: 31 additions & 0 deletions src/Moq.Analyzers/MoqExpressionAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,37 @@ public IReadOnlyList<SetupMember> ExtractSetupMembers(InvocationExpressionSyntax
return methodSymbol;
}

public ITypeSymbol? ExtractAsMethodType(MoqSymbols moqSymbols, InvocationExpressionSyntax invocationExpression, out TypeSyntax? typeSyntax, CancellationToken cancellationToken)
{
typeSyntax = null;

if (invocationExpression.Expression is not MemberAccessExpressionSyntax memberAccessExpression)
{
return null;
}

if (memberAccessExpression.Name is not GenericNameSyntax genericName)
{
return null;
}

var symbol = this.semanticModel.GetSymbolInfo(memberAccessExpression.Name, cancellationToken);

if (symbol.Symbol is not IMethodSymbol methodSymbol)
{
return null;
}

if (!moqSymbols.IsAsMethod(methodSymbol))
{
return null;
}

typeSyntax = genericName.TypeArgumentList.Arguments[0];

return methodSymbol.TypeArguments[0];
}

private static ObjectCreationExpressionSyntax? FindMockCreation(BlockSyntax block, string variableName)
{
foreach (var statement in block.Statements.OfType<LocalDeclarationStatementSyntax>())
Expand Down
14 changes: 14 additions & 0 deletions src/Moq.Analyzers/MoqSymbols.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

namespace PosInformatique.Moq.Analyzers
{
using System;
using Microsoft.CodeAnalysis;

internal sealed class MoqSymbols
Expand All @@ -22,6 +23,8 @@ internal sealed class MoqSymbols

private readonly ISymbol isAnyTypeClass;

private readonly ISymbol asMethod;

private MoqSymbols(INamedTypeSymbol mockClass, INamedTypeSymbol mockBehaviorEnum, ISymbol isAnyTypeClass, INamedTypeSymbol protectedMockInterface)
{
this.mockClass = mockClass;
Expand All @@ -31,6 +34,7 @@ private MoqSymbols(INamedTypeSymbol mockClass, INamedTypeSymbol mockBehaviorEnum
this.setupMethods = mockClass.GetMembers("Setup").OfType<IMethodSymbol>().ToArray();
this.mockBehaviorStrictField = mockBehaviorEnum.GetMembers("Strict").First();
this.setupProtectedMethods = protectedMockInterface.GetMembers("Setup").OfType<IMethodSymbol>().ToArray();
this.asMethod = mockClass.GetMembers("As").Single();
}

public static MoqSymbols? FromCompilation(Compilation compilation)
Expand Down Expand Up @@ -275,5 +279,15 @@ public bool IsMockable(ITypeSymbol type)

return false;
}

public bool IsAsMethod(IMethodSymbol method)
{
if (!SymbolEqualityComparer.Default.Equals(method.OriginalDefinition, this.asMethod))
{
return false;
}

return true;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
//-----------------------------------------------------------------------
// <copyright file="AsMustBeUsedWithInterfaceAnalyzerTest.cs" company="P.O.S Informatique">
// Copyright (c) P.O.S Informatique. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

namespace PosInformatique.Moq.Analyzers.Tests
{
using System.Threading.Tasks;
using Xunit;
using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier<
AsMustBeUsedWithInterfaceAnalyzer,
Microsoft.CodeAnalysis.Testing.DefaultVerifier>;

public class AsMustBeUsedWithInterfaceAnalyzerTest
{
[Fact]
public async Task AsMethod_WithInterface()
{
var source = @"
namespace ConsoleApplication1
{
using Moq;
public class TestClass
{
public void TestMethod()
{
var mock1 = new Mock<C>();
mock1.As<I>();
}
}
public class C
{
}
public interface I
{
}
}" + MoqLibrary.Code;

await Verify.VerifyAnalyzerAsync(source);
}

[Fact]
public async Task AsMethod_WithClass()
{
var source = @"
namespace ConsoleApplication1
{
using Moq;
public class TestClass
{
public void TestMethod()
{
var mock1 = new Mock<C>();
mock1.As<[|C2|]>();
}
}
public class C
{
}
public class C2
{
}
}" + MoqLibrary.Code;

await Verify.VerifyAnalyzerAsync(source);
}

[Fact]
public async Task NoAsMethod()
{
var source = @"
namespace ConsoleApplication1
{
using Moq;
public class TestClass
{
public void TestMethod()
{
var mock1 = new Mock<C>();
mock1.Setup(m => m.Method());
var action = new System.Action(() => { });
action(); // Ignored by the ExtractAsMethodType() method because not a MemberAccessExpressionSyntax.
var otherClass = new OtherClass();
otherClass.GenericMethodNotAs<C>(); // Ignored by the ExtractAsMethodType() method because it not As<T>() symbol.
}
}
public class C
{
public virtual void Method() { }
}
public class OtherClass
{
public void GenericMethodNotAs<T>() { }
}
}" + MoqLibrary.Code;

await Verify.VerifyAnalyzerAsync(source);
}

[Fact]
public async Task NoMoqLibrary()
{
var source = @"
namespace ConsoleApplication1
{
using OtherNamespace;
public class TestClass
{
public void TestMethod()
{
var mock1 = new Mock<I>();
mock1.As<I>();
}
}
public interface I
{
}
}
namespace OtherNamespace
{
public class Mock<T>
{
public void As<TInterface>() { }
}
}";

await Verify.VerifyAnalyzerAsync(source);
}
}
}
2 changes: 2 additions & 0 deletions tests/Moq.Analyzers.Tests/MoqLibrary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public Mock(MockBehavior _ = MockBehavior.Loose, params object[] args) { }
public Mock(params object[] args) { }
public Mock<TInterface> As<TInterface>() { return null; }
public ISetup Setup(Action<T> act) { return null; }
public ISetup<TResult> Setup<TResult>(Func<T, TResult> func) { return default; }
Expand Down

0 comments on commit 8f8d95d

Please sign in to comment.