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) 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), diff --git a/src/Moq.Analyzers/Analyzers/ConstructorArgumentsAnalyzer.cs b/src/Moq.Analyzers/Analyzers/ConstructorArgumentsAnalyzer.cs index 11eb390..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,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)) { @@ -226,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 447bdfc..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,6 +220,10 @@ private C() [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 = @" @@ -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) + { + } } }"; @@ -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 = @" @@ -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) + { + } } }"; @@ -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 = @" @@ -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{|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() { @@ -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 @@ -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 = @" @@ -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{|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) + { + } } }";