Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: 2.0.0
default: 2.0.1
VersionSuffix:
type: string
description: The version suffix of the library (for example rc.1)
Expand Down
3 changes: 0 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
# PosInformatique.Moq.Analyzers
<div align="center">

[![Nuget](https://img.shields.io/nuget/v/PosInformatique.Moq.Analyzers)](https://www.nuget.org/packages/PosInformatique.Moq.Analyzers/)
[![NuGet downloads](https://img.shields.io/nuget/dt/PosInformatique.Moq.Analyzers)](https://www.nuget.org/packages/PosInformatique.Moq.Analyzers/)
[![License](https://img.shields.io/github/license/Nonanti/MathFlow?style=flat-square)](LICENSE)
[![Build Status](https://img.shields.io/github/actions/workflow/status/PosInformatique/PosInformatique.Moq.Analyzers/github-actions-ci.yaml?style=flat-square)](https://github.com/PosInformatique/PosInformatique.Moq.Analyzers/actions)
[![.NET Standard 2.0](https://img.shields.io/badge/.NET%20Standard-2.0-512BD4?style=flat-square)](https://learn.microsoft.com/en-us/dotnet/standard/net-standard?tabs=net-standard-2-0)

</div>

PosInformatique.Moq.Analyzers is a set of analyzers to verify syntax and code design when writing the unit tests using the [Moq](https://github.com/devlooped/moq) library.

The analyzers are compiled against [.NET Standard 2.0](https://learn.microsoft.com/en-us/dotnet/standard/net-standard?tabs=net-standard-2-0),
Expand Down
71 changes: 66 additions & 5 deletions src/Moq.Analyzers/Analyzers/ConstructorArgumentsAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,11 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
}

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

if (objectCreation.ArgumentList is not null)
{
constructorArguments.AddRange(objectCreation.ArgumentList.Arguments);
constructorArguments.AddRange(objectCreation.ArgumentList.Arguments.Select(a => a.Expression));
}

// Gets the first argument, check if it is MockBehavior argument and skip it.
Expand Down Expand Up @@ -158,6 +158,18 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
{
matchedConstructor.Try(constructor);

// Special case, if we have only one argument and it is an array of object.
// Use the array of object as the arguments
if (constructorArguments.Count == 1)
{
var objectArrayElements = ExpandObjectArrayElements(constructorArguments[0], context);

if (objectArrayElements is not null)
{
constructorArguments = objectArrayElements;
}
}

// If the number of arguments is different, check the next constructor definition.
if (constructor.Parameters.Length != constructorArguments.Count)
{
Expand All @@ -167,7 +179,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context)

for (var i = 0; i < constructorArguments.Count; i++)
{
if (constructorArguments[i].Expression.IsKind(SyntaxKind.NullLiteralExpression))
if (constructorArguments[i].IsKind(SyntaxKind.NullLiteralExpression))
{
// Null parameter, just check the parameter type is a reference type.
if (!constructor.Parameters[i].Type.IsReferenceType)
Expand All @@ -179,13 +191,13 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
continue;
}

if (constructorArguments[i].Expression.IsKind(SyntaxKind.DefaultLiteralExpression))
if (constructorArguments[i].IsKind(SyntaxKind.DefaultLiteralExpression))
{
// Default parameter, skip the parameter.
continue;
}

var constructorArgumentSymbol = context.SemanticModel.GetTypeInfo(constructorArguments[i].Expression, context.CancellationToken);
var constructorArgumentSymbol = context.SemanticModel.GetTypeInfo(constructorArguments[i], context.CancellationToken);

if (!constructorArgumentSymbol.Type.IsOrInheritFrom(constructor.Parameters[i].Type))
{
Expand Down Expand Up @@ -226,6 +238,55 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
}
}

private static List<ExpressionSyntax>? ExpandObjectArrayElements(ExpressionSyntax argument, SyntaxNodeAnalysisContext context)
{
var argumentType = context.SemanticModel.GetTypeInfo(argument, context.CancellationToken);

var type = argumentType.Type;

if (argumentType.Type is null)
{
type = argumentType.ConvertedType;
}

if (type is not IArrayTypeSymbol arrayTypeSymbol)
{
return null;
}

if (arrayTypeSymbol.ElementType.SpecialType != SpecialType.System_Object)
{
return null;
}

// It is an object[] array, try to extract the arguments of the array creation
if (argument is ArrayCreationExpressionSyntax arrayCreationExpressionSyntax)
{
if (arrayCreationExpressionSyntax.Initializer is not null)
{
return arrayCreationExpressionSyntax.Initializer.Expressions.ToList();
}
}
else if (argument is CollectionExpressionSyntax collectionExpressionSyntax)
{
var expressions = new List<ExpressionSyntax>(collectionExpressionSyntax.Elements.Count);

foreach (var element in collectionExpressionSyntax.Elements)
{
if (element is not ExpressionElementSyntax elementExpression)
{
return null;
}

expressions.Add(elementExpression.Expression);
}

return expressions;
}

return null;
}

private struct MatchedConstructor
{
public MatchedConstructor()
Expand Down
4 changes: 2 additions & 2 deletions src/Moq.Analyzers/Moq.Analyzers.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.0.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.7.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
10 changes: 5 additions & 5 deletions src/Moq.Analyzers/MoqExpressionAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ public bool IsMockCreationStrictBehavior(ObjectCreationExpressionSyntax mockCrea
argument = mockCreation.ArgumentList.Arguments[1];
}

return this.IsStrictBehaviorArgument(argument, cancellationToken);
return this.IsStrictBehaviorArgument(argument.Expression, cancellationToken);
}

public bool IsMockOfStrictBehavior(ArgumentListSyntax? argumentList, CancellationToken cancellationToken)
Expand All @@ -244,15 +244,15 @@ public bool IsMockOfStrictBehavior(ArgumentListSyntax? argumentList, Cancellatio
return false;
}

return this.IsStrictBehaviorArgument(lastArgument, cancellationToken);
return this.IsStrictBehaviorArgument(lastArgument.Expression, cancellationToken);
}

public bool IsStrictBehaviorArgument(ArgumentSyntax argument, out MemberAccessExpressionSyntax? memberAccessExpression, CancellationToken cancellationToken)
public bool IsStrictBehaviorArgument(ExpressionSyntax argument, out MemberAccessExpressionSyntax? memberAccessExpression, CancellationToken cancellationToken)
{
memberAccessExpression = null;

// Check it is a MemberAccessExpressionSyntax (because we searching for MockBehavior.XXXXX).
if (argument.Expression is not MemberAccessExpressionSyntax expression)
if (argument is not MemberAccessExpressionSyntax expression)
{
return false;
}
Expand Down Expand Up @@ -621,7 +621,7 @@ private bool IsMockSetupMethod(InvocationExpressionSyntax invocationExpression,
return true;
}

private bool IsStrictBehaviorArgument(ArgumentSyntax argument, CancellationToken cancellationToken)
private bool IsStrictBehaviorArgument(ExpressionSyntax argument, CancellationToken cancellationToken)
{
if (!this.IsStrictBehaviorArgument(argument, out var memberAccessExpression, cancellationToken))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,13 +212,18 @@ private C()
}

[Theory]
[InlineData("1")]
[InlineData("1, \"B\"")]
[InlineData("1, null")]
[InlineData("default, \"B\"")]
[InlineData("(int)default, default")]
[InlineData("default, null, 1234")]
[InlineData("1, \"An object\", 3, null")]
[InlineData("1, \"An object\", 3, new System.IO.MemoryStream()")]
[InlineData("[new string[] { \"A\", \"B\" }]")]
[InlineData("new object[] { new string[] { \"A\", \"B\" } }")]
[InlineData("[1]")]
[InlineData("[1, \"A\"]")]
public async Task Arguments_Match(string parameters)
{
var source = @"
Expand Down Expand Up @@ -255,6 +260,10 @@ public C(int a, int[] b, int c)
public C(int a, object b, int c, System.IDisposable d)
{
}

public C(string[] array)
{
}
}
}";

Expand Down Expand Up @@ -340,6 +349,8 @@ public void TestMethod()
[InlineData("1, \"B\"")]
[InlineData("1, \"An object\", 3, null")]
[InlineData("1, \"An object\", 3, new System.IO.MemoryStream()")]
[InlineData("[1]")]
[InlineData("[1, \"A\"]")]
public async Task Arguments_Match_WithMockBehavior(string parameters)
{
var source = @"
Expand Down Expand Up @@ -372,6 +383,10 @@ public C(int a, object c)
public C(int a, object b, int c, System.IDisposable d)
{
}

public C(string[] array)
{
}
}
}";

Expand Down Expand Up @@ -408,6 +423,8 @@ public void TestMethod()
[InlineData("null")]
[InlineData("\"The string\", 2")]
[InlineData("1, 2, 3, \"The string\"")]
[InlineData("new int[] { 1, 2 }, 1000")]
[InlineData("new object[] { \"A\", \"B\" }, 1000")]
public async Task Arguments_NotMatch(string parameters)
{
var source = @"
Expand Down Expand Up @@ -440,12 +457,56 @@ public C(int a, object c)
public C(int a, object b, int c, System.IDisposable d)
{
}

public C(string[] array, int b)
{
}
}
}";

await Verifier.VerifyAnalyzerAsync(source);
}

[Theory]
[InlineData("[]")]
public async Task Arguments_NotMatch_EmptyArray(string parameters)
{
var source = @"
namespace ConsoleApplication1
{
using Moq;

public class TestClass
{
public void TestMethod()
{
var mock = new Mock<C>{|PosInfoMoq2005:(" + parameters + @")|};
}
}

public class C
{
public C(int a)
{
}

public C(int a, string b)
{
}

public C(int a, object c)
{
}

public C(string[] array, int b)
{
}
}
}";

await Verifier.VerifyAnalyzerAsync(source);
}

[Fact]
public async Task Arguments_WithDefaultParameters()
{
Expand Down Expand Up @@ -474,7 +535,7 @@ public C(int a = 0, int b = 1, int c = 2, int d = 3)
}

[Fact]
public async Task Arguments_NotMatch_WithNoContructor()
public async Task Arguments_NotMatch_WithNoConstructor()
{
var source = @"
namespace ConsoleApplication1
Expand All @@ -500,6 +561,7 @@ public class ClassWithNoConstructor { }
[InlineData("null")]
[InlineData("\"The string\", 2")]
[InlineData("1, 2, 3, \"The string\"")]
[InlineData("new int[] { 1, 2 }, 1000")]
public async Task Arguments_NotMatch_WithMockBehavior(string parameters)
{
var source = @"
Expand Down Expand Up @@ -528,6 +590,50 @@ public C(int a, string b)
public C(int a, object c)
{
}

public C(string[] array, int b)
{
}
}
}";

await Verifier.VerifyAnalyzerAsync(source);
}

[Theory]
[InlineData("[]")]
public async Task Arguments_NotMatch_WithMockBehavior_EmptyArray(string parameters)
{
var source = @"
namespace ConsoleApplication1
{
using Moq;

public class TestClass
{
public void TestMethod()
{
var mock = new Mock<C>{|PosInfoMoq2005:(MockBehavior.Strict, " + parameters + @")|};
}
}

public class C
{
public C(int a)
{
}

public C(int a, string b)
{
}

public C(int a, object c)
{
}

public C(string[] array, int b)
{
}
}
}";

Expand Down