From 614aa1bd3430a4ac471038911f2c3b7acf4198b4 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 23 Oct 2025 18:10:33 +0200 Subject: [PATCH 1/4] Fix a bug when using implicit conversion with the array in the constructor with the PosInfoMoq2005 rule (fixes #55). --- .../Analyzers/ConstructorArgumentsAnalyzer.cs | 24 +++++++++++++++++-- .../ConstructorArgumentsAnalyzerTest.cs | 22 +++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/Moq.Analyzers/Analyzers/ConstructorArgumentsAnalyzer.cs b/src/Moq.Analyzers/Analyzers/ConstructorArgumentsAnalyzer.cs index 11eb390..0ec8804 100644 --- a/src/Moq.Analyzers/Analyzers/ConstructorArgumentsAnalyzer.cs +++ b/src/Moq.Analyzers/Analyzers/ConstructorArgumentsAnalyzer.cs @@ -187,10 +187,30 @@ private static void Analyze(SyntaxNodeAnalysisContext context) var constructorArgumentSymbol = context.SemanticModel.GetTypeInfo(constructorArguments[i].Expression, context.CancellationToken); + if (constructorArgumentSymbol.Type is null) + { + // Try to test if there is not an implicit conversion + var conversion = context.Compilation.ClassifyConversion(constructor.Parameters[i].Type, constructorArgumentSymbol.ConvertedType!); + + if (!conversion.IsImplicit) + { + matchedConstructor.Cancel(); + break; + } + + continue; + } + if (!constructorArgumentSymbol.Type.IsOrInheritFrom(constructor.Parameters[i].Type)) { - matchedConstructor.Cancel(); - break; + // Try to test if there is not an implicit conversion + var conversion = context.Compilation.ClassifyConversion(constructor.Parameters[i].Type, constructorArgumentSymbol.Type); + + if (!conversion.IsImplicit) + { + matchedConstructor.Cancel(); + break; + } } } diff --git a/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentsAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentsAnalyzerTest.cs index 447bdfc..a1c8a04 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentsAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentsAnalyzerTest.cs @@ -219,6 +219,8 @@ private C() [InlineData("default, null, 1234")] [InlineData("1, \"An object\", 3, null")] [InlineData("1, \"An object\", 3, new System.IO.MemoryStream()")] + [InlineData("[\"A\", \"B\"]")] + [InlineData("new object[] { \"A\", \"B\" }")] public async Task Arguments_Match(string parameters) { var source = @" @@ -255,6 +257,10 @@ public C(int a, int[] b, int c) public C(int a, object b, int c, System.IDisposable d) { } + + public C(string[] array) + { + } } }"; @@ -340,6 +346,8 @@ public void TestMethod() [InlineData("1, \"B\"")] [InlineData("1, \"An object\", 3, null")] [InlineData("1, \"An object\", 3, new System.IO.MemoryStream()")] + [InlineData("[\"A\", \"B\"]")] + [InlineData("new object[] { \"A\", \"B\" }")] public async Task Arguments_Match_WithMockBehavior(string parameters) { var source = @" @@ -372,6 +380,10 @@ public C(int a, object c) public C(int a, object b, int c, System.IDisposable d) { } + + public C(string[] array) + { + } } }"; @@ -408,6 +420,7 @@ public void TestMethod() [InlineData("null")] [InlineData("\"The string\", 2")] [InlineData("1, 2, 3, \"The string\"")] + [InlineData("new int[] { 1, 2 }, 1000")] public async Task Arguments_NotMatch(string parameters) { var source = @" @@ -440,6 +453,10 @@ public C(int a, object c) public C(int a, object b, int c, System.IDisposable d) { } + + public C(string[] array, int b) + { + } } }"; @@ -500,6 +517,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 = @" @@ -528,6 +546,10 @@ public C(int a, string b) public C(int a, object c) { } + + public C(string[] array, int b) + { + } } }"; From 282dce796392ebefc7e1b34a10a4854038a6c473 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Thu, 23 Oct 2025 18:11:03 +0200 Subject: [PATCH 2/4] Fix version --- .github/workflows/github-actions-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-actions-release.yml b/.github/workflows/github-actions-release.yml index 1c0a449..03c1a56 100644 --- a/.github/workflows/github-actions-release.yml +++ b/.github/workflows/github-actions-release.yml @@ -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) From ff4ab79376ff1be42d242ea330605776064b3dea Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Fri, 24 Oct 2025 17:28:01 +0200 Subject: [PATCH 3/4] Refactoring of the last fix and improve to detect wrong usage of "params object[]" when instantiate Mock. --- .../Analyzers/ConstructorArgumentsAnalyzer.cs | 95 +++++++++++++------ src/Moq.Analyzers/Moq.Analyzers.csproj | 4 +- src/Moq.Analyzers/MoqExpressionAnalyzer.cs | 10 +- .../ConstructorArgumentsAnalyzerTest.cs | 94 +++++++++++++++++- 4 files changed, 164 insertions(+), 39 deletions(-) diff --git a/src/Moq.Analyzers/Analyzers/ConstructorArgumentsAnalyzer.cs b/src/Moq.Analyzers/Analyzers/ConstructorArgumentsAnalyzer.cs index 0ec8804..a853052 100644 --- a/src/Moq.Analyzers/Analyzers/ConstructorArgumentsAnalyzer.cs +++ b/src/Moq.Analyzers/Analyzers/ConstructorArgumentsAnalyzer.cs @@ -118,11 +118,11 @@ private static void Analyze(SyntaxNodeAnalysisContext context) } // Gets the list of the constructor arguments - var constructorArguments = new List(); + var constructorArguments = new List(); 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. @@ -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) { @@ -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) @@ -179,38 +191,18 @@ 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); - - if (constructorArgumentSymbol.Type is null) - { - // Try to test if there is not an implicit conversion - var conversion = context.Compilation.ClassifyConversion(constructor.Parameters[i].Type, constructorArgumentSymbol.ConvertedType!); - - if (!conversion.IsImplicit) - { - matchedConstructor.Cancel(); - break; - } - - continue; - } + var constructorArgumentSymbol = context.SemanticModel.GetTypeInfo(constructorArguments[i], context.CancellationToken); if (!constructorArgumentSymbol.Type.IsOrInheritFrom(constructor.Parameters[i].Type)) { - // Try to test if there is not an implicit conversion - var conversion = context.Compilation.ClassifyConversion(constructor.Parameters[i].Type, constructorArgumentSymbol.Type); - - if (!conversion.IsImplicit) - { - matchedConstructor.Cancel(); - break; - } + matchedConstructor.Cancel(); + break; } } @@ -246,6 +238,55 @@ private static void Analyze(SyntaxNodeAnalysisContext context) } } + private static List? 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(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() diff --git a/src/Moq.Analyzers/Moq.Analyzers.csproj b/src/Moq.Analyzers/Moq.Analyzers.csproj index 82d371c..68efed3 100644 --- a/src/Moq.Analyzers/Moq.Analyzers.csproj +++ b/src/Moq.Analyzers/Moq.Analyzers.csproj @@ -127,8 +127,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Moq.Analyzers/MoqExpressionAnalyzer.cs b/src/Moq.Analyzers/MoqExpressionAnalyzer.cs index 3ce9631..5cebcbd 100644 --- a/src/Moq.Analyzers/MoqExpressionAnalyzer.cs +++ b/src/Moq.Analyzers/MoqExpressionAnalyzer.cs @@ -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) @@ -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; } @@ -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)) { diff --git a/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentsAnalyzerTest.cs b/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentsAnalyzerTest.cs index a1c8a04..628618f 100644 --- a/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentsAnalyzerTest.cs +++ b/tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentsAnalyzerTest.cs @@ -212,6 +212,7 @@ private C() } [Theory] + [InlineData("1")] [InlineData("1, \"B\"")] [InlineData("1, null")] [InlineData("default, \"B\"")] @@ -219,8 +220,10 @@ private C() [InlineData("default, null, 1234")] [InlineData("1, \"An object\", 3, null")] [InlineData("1, \"An object\", 3, new System.IO.MemoryStream()")] - [InlineData("[\"A\", \"B\"]")] - [InlineData("new object[] { \"A\", \"B\" }")] + [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 = @" @@ -346,8 +349,8 @@ public void TestMethod() [InlineData("1, \"B\"")] [InlineData("1, \"An object\", 3, null")] [InlineData("1, \"An object\", 3, new System.IO.MemoryStream()")] - [InlineData("[\"A\", \"B\"]")] - [InlineData("new object[] { \"A\", \"B\" }")] + [InlineData("[1]")] + [InlineData("[1, \"A\"]")] public async Task Arguments_Match_WithMockBehavior(string parameters) { var source = @" @@ -421,6 +424,7 @@ public void TestMethod() [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 = @" @@ -463,6 +467,46 @@ 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{|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() { @@ -491,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 @@ -556,6 +600,46 @@ 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{|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) + { + } + } + }"; + + await Verifier.VerifyAnalyzerAsync(source); + } + [Fact] public async Task Arguments_NotMatch_WithMockBehavior_WithNoConstructor() { From 10321440457103af629cc475fa239b274157c1b3 Mon Sep 17 00:00:00 2001 From: Gilles TOURREAU Date: Fri, 7 Nov 2025 10:04:26 +0100 Subject: [PATCH 4/4] Fix README. --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index caeb6d7..9507ef5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ # PosInformatique.Moq.Analyzers -
[![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/) @@ -7,8 +6,6 @@ [![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) -
- 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),