Skip to content

Commit

Permalink
Add the PosInfoMoq2016 rule to check the lambda expression factory ar…
Browse files Browse the repository at this point in the history
…e used only with class types (fixes: #40).
  • Loading branch information
GillesTourreau committed Oct 23, 2024
1 parent bd80329 commit 97f6686
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 5 deletions.
1 change: 1 addition & 0 deletions PosInformatique.Moq.Analyzers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Compilation", "Compilation"
docs\Compilation\PosInfoMoq2013.md = docs\Compilation\PosInfoMoq2013.md
docs\Compilation\PosInfoMoq2014.md = docs\Compilation\PosInfoMoq2014.md
docs\Compilation\PosInfoMoq2015.md = docs\Compilation\PosInfoMoq2015.md
docs\Compilation\PosInfoMoq2016.md = docs\Compilation\PosInfoMoq2016.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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ All the rules of this category should not be disabled (or changed their severity
| [PosInfoMoq2012: The delegate in the argument of the `Returns()` method must return a value with same type of the mocked method.](docs/Compilation/PosInfoMoq2012.md) | The lambda expression, anonymous method or method in the argument of the `Returns()` must return return a value of the same type as the mocked method or property. |
| [PosInfoMoq2013: The delegate in the argument of the `Returns()`/`ReturnsAsync()` method must have the same parameter types of the mocked method/property.](docs/Compilation/PosInfoMoq2013.md) | The lambda expression, anonymous method or method in the argument of the `Returns()`/`ReturnsAsync()` must have the same arguments type of the mocked method or property. |
| [PosInfoMoq2014: The `Callback()` delegate expression must not return a value.](docs/Compilation/PosInfoMoq2014.md) | The `Callback()` delegate expression must not return a value. |
| [PosInfoMoq2014: PosInfoMoq2015: The `Protected().Setup()` method must match the return type of the mocked method](docs/Compilation/PosInfoMoq2015.md) | The method setup with `Protected().Setup()` must match the return type of the mocked method. |
| [PosInfoMoq2015: The `Protected().Setup()` method must match the return type of the mocked method](docs/Compilation/PosInfoMoq2015.md) | The method setup with `Protected().Setup()` must match the return type of the mocked method. |
| [PosInfoMoq2016: `Mock<T>` constructor with factory lambda expression can be used only with classes.](docs/Compilation/PosInfoMoq2016.md) | The factory lambda expression used in `Mock<T>` instantiation must used only for the classes. |



45 changes: 45 additions & 0 deletions docs/Compilation/PosInfoMoq2016.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# PosInfoMoq2016: `Mock<T>` constructor with factory lambda expression can be used only with classes.

| Property | Value |
|-------------------------------------|-------------------------------------------------------------------------|
| **Rule ID** | PosInfoMoq2016 |
| **Title** | `Mock<T>` constructor with factory lambda expression can be used only with classes. |
| **Category** | Compilation |
| **Default severity** | Error |

## Cause

The factory lambda expression used in `Mock<T>` instantiation must used only for the classes.

## Rule description

When using a lambda expression in the constructor of Mock<T> to create a mock instance, the mocked type must be a class.

```csharp
[Fact]
public void Test()
{
var service1 = new Mock<IService>(() => new Service()); // The factory lambda expression can be used only on classes type.
var service2 = new Mock<Service>(() => new Service()); // OK
}

public interface IService
{
}

public class Service : IService:
{
public Service(string a)
{
}
}
```

## How to fix violations

To fix a violation of this rule, ensure that the lambda expression factory is used with a mocked type that is a 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 `ArgumentException`
thrown with the *"Constructor arguments cannot be passed for interface mocks."* message.
2 changes: 1 addition & 1 deletion src/Moq.Analyzers/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Rule ID | Category | Severity | Notes
--------|----------|----------|-------
PosInfoMoq2014 | Compilation | Error | The `Callback()` delegate expression must not return a value.
PosInfoMoq2015 | Compilation | Error | The `Protected().Setup()` method must match the return type of the mocked method.

PosInfoMoq2016 | Compilation | Error | `Mock<T>` constructor with factory lambda expression can be used only with classes.
## Release 1.10.0

### New Rules
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,20 @@ public class ConstructorArgumentsMustMatchAnalyzer : DiagnosticAnalyzer
description: "Constructor of the mocked class must be accessible.",
helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2011.html");

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(ConstructorArgumentsMustMatchMockedClassRule, ConstructorMockedClassMustBeAccessibleRule);
private static readonly DiagnosticDescriptor ConstructorWithLambdaExpressionCanBeUseWithClassesOnlyRule = new DiagnosticDescriptor(
"PosInfoMoq2016",
"Mock<T> constructor with factory lambda expression can be used only with classes",
"Mock<T> constructor with factory lambda expression can be used only with classes",
"Compilation",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Mock<T> constructor with factory lambda expression can be used only with classes.",
helpLinkUri: "https://posinformatique.github.io/PosInformatique.Moq.Analyzers/docs/Compilation/PosInfoMoq2016.html");

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(
ConstructorArgumentsMustMatchMockedClassRule,
ConstructorMockedClassMustBeAccessibleRule,
ConstructorWithLambdaExpressionCanBeUseWithClassesOnlyRule);

public override void Initialize(AnalysisContext context)
{
Expand All @@ -60,12 +73,27 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
var moqExpressionAnalyzer = new MoqExpressionAnalyzer(moqSymbols, context.SemanticModel);

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

// Check if the mock instantiation is with a factory.
var constructorSymbol = context.SemanticModel.GetSymbolInfo(objectCreation, context.CancellationToken);

if (moqSymbols.IsMockConstructorWithFactory(constructorSymbol.Symbol))
{
// In this case, we ignore the matching of the constructor arguments.
// But we check it is an interface (else it is not supported).
if (mockedType.TypeKind != TypeKind.Class)
{
context.ReportDiagnostic(ConstructorWithLambdaExpressionCanBeUseWithClassesOnlyRule, typeExpression!.GetLocation());
}

return;
}

// Check the type is mockable
if (!moqSymbols.IsMockable(mockedType))
{
Expand Down
3 changes: 2 additions & 1 deletion src/Moq.Analyzers/Moq.Analyzers.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@
<PackageProjectUrl>https://github.com/PosInformatique/PosInformatique.Moq.Analyzers</PackageProjectUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageReleaseNotes>
1.10.0
1.11.0
- Add new rules:
- PosInfoMoq2014: The delegate in the argument of the Returns() method must return a value with same type of the mocked method.
- PosInfoMoq2015: The Protected().Setup() method must match the return type of the mocked method.
- PosInfoMoq2016: Mock&lt;T&gt; constructor with factory lambda expression can be used only with classes.
- Fix the PosInfoMoq1001 rule for the Mock&lt;T&gt; instantiation with the lambda expression to create mock instances.
- Fix the PosInfoMoq2004 rule for the Mock&lt;T&gt; instantiation with the lambda expression to create mock instances.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,84 @@ public C(int a, object c)
await Verifier.VerifyAnalyzerAsync(source);
}

[Theory]
[InlineData("")]
[InlineData(", MockBehavior.Strict")]
public async Task Arguments_WithFactory(string behavior)
{
var source = @"
namespace ConsoleApplication1
{
using Moq;
public class TestClass
{
public void TestMethod()
{
var mock1 = new Mock<C>(() => new C(10)" + behavior + @");
}
}
public class C
{
public C(int a)
{
}
public C(int a, string b)
{
}
public C(int a, object c)
{
}
}
}";

await Verifier.VerifyAnalyzerAsync(source);
}

[Theory]
[InlineData("")]
[InlineData(", MockBehavior.Strict")]
public async Task Arguments_WithFactory_NotClass(string behavior)
{
var source = @"
namespace ConsoleApplication1
{
using Moq;
public class TestClass
{
public void TestMethod()
{
var mock1 = new Mock<{|PosInfoMoq2016:I|}>(() => new C(10)" + behavior + @");
}
}
public interface I
{
}
public class C : I
{
public C(int a)
{
}
public C(int a, string b)
{
}
public C(int a, object c)
{
}
}
}";

await Verifier.VerifyAnalyzerAsync(source);
}

[Fact]
public async Task NoMoqLibrary()
{
Expand Down

0 comments on commit 97f6686

Please sign in to comment.