Skip to content

Commit 2059ab7

Browse files
v2.0.1 (#56)
* Fix a bug when using implicit conversion with the array in the constructor with the PosInfoMoq2005 rule (fixes #55).
1 parent 2eb6f8d commit 2059ab7

6 files changed

Lines changed: 181 additions & 17 deletions

File tree

.github/workflows/github-actions-release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77
type: string
88
description: The version of the library
99
required: true
10-
default: 2.0.0
10+
default: 2.0.1
1111
VersionSuffix:
1212
type: string
1313
description: The version suffix of the library (for example rc.1)

README.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
# PosInformatique.Moq.Analyzers
2-
<div align="center">
32

43
[![Nuget](https://img.shields.io/nuget/v/PosInformatique.Moq.Analyzers)](https://www.nuget.org/packages/PosInformatique.Moq.Analyzers/)
54
[![NuGet downloads](https://img.shields.io/nuget/dt/PosInformatique.Moq.Analyzers)](https://www.nuget.org/packages/PosInformatique.Moq.Analyzers/)
65
[![License](https://img.shields.io/github/license/Nonanti/MathFlow?style=flat-square)](LICENSE)
76
[![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)
87
[![.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)
98

10-
</div>
11-
129
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.
1310

1411
The analyzers are compiled against [.NET Standard 2.0](https://learn.microsoft.com/en-us/dotnet/standard/net-standard?tabs=net-standard-2-0),

src/Moq.Analyzers/Analyzers/ConstructorArgumentsAnalyzer.cs

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,11 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
118118
}
119119

120120
// Gets the list of the constructor arguments
121-
var constructorArguments = new List<ArgumentSyntax>();
121+
var constructorArguments = new List<ExpressionSyntax>();
122122

123123
if (objectCreation.ArgumentList is not null)
124124
{
125-
constructorArguments.AddRange(objectCreation.ArgumentList.Arguments);
125+
constructorArguments.AddRange(objectCreation.ArgumentList.Arguments.Select(a => a.Expression));
126126
}
127127

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

161+
// Special case, if we have only one argument and it is an array of object.
162+
// Use the array of object as the arguments
163+
if (constructorArguments.Count == 1)
164+
{
165+
var objectArrayElements = ExpandObjectArrayElements(constructorArguments[0], context);
166+
167+
if (objectArrayElements is not null)
168+
{
169+
constructorArguments = objectArrayElements;
170+
}
171+
}
172+
161173
// If the number of arguments is different, check the next constructor definition.
162174
if (constructor.Parameters.Length != constructorArguments.Count)
163175
{
@@ -167,7 +179,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
167179

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

182-
if (constructorArguments[i].Expression.IsKind(SyntaxKind.DefaultLiteralExpression))
194+
if (constructorArguments[i].IsKind(SyntaxKind.DefaultLiteralExpression))
183195
{
184196
// Default parameter, skip the parameter.
185197
continue;
186198
}
187199

188-
var constructorArgumentSymbol = context.SemanticModel.GetTypeInfo(constructorArguments[i].Expression, context.CancellationToken);
200+
var constructorArgumentSymbol = context.SemanticModel.GetTypeInfo(constructorArguments[i], context.CancellationToken);
189201

190202
if (!constructorArgumentSymbol.Type.IsOrInheritFrom(constructor.Parameters[i].Type))
191203
{
@@ -226,6 +238,55 @@ private static void Analyze(SyntaxNodeAnalysisContext context)
226238
}
227239
}
228240

241+
private static List<ExpressionSyntax>? ExpandObjectArrayElements(ExpressionSyntax argument, SyntaxNodeAnalysisContext context)
242+
{
243+
var argumentType = context.SemanticModel.GetTypeInfo(argument, context.CancellationToken);
244+
245+
var type = argumentType.Type;
246+
247+
if (argumentType.Type is null)
248+
{
249+
type = argumentType.ConvertedType;
250+
}
251+
252+
if (type is not IArrayTypeSymbol arrayTypeSymbol)
253+
{
254+
return null;
255+
}
256+
257+
if (arrayTypeSymbol.ElementType.SpecialType != SpecialType.System_Object)
258+
{
259+
return null;
260+
}
261+
262+
// It is an object[] array, try to extract the arguments of the array creation
263+
if (argument is ArrayCreationExpressionSyntax arrayCreationExpressionSyntax)
264+
{
265+
if (arrayCreationExpressionSyntax.Initializer is not null)
266+
{
267+
return arrayCreationExpressionSyntax.Initializer.Expressions.ToList();
268+
}
269+
}
270+
else if (argument is CollectionExpressionSyntax collectionExpressionSyntax)
271+
{
272+
var expressions = new List<ExpressionSyntax>(collectionExpressionSyntax.Elements.Count);
273+
274+
foreach (var element in collectionExpressionSyntax.Elements)
275+
{
276+
if (element is not ExpressionElementSyntax elementExpression)
277+
{
278+
return null;
279+
}
280+
281+
expressions.Add(elementExpression.Expression);
282+
}
283+
284+
return expressions;
285+
}
286+
287+
return null;
288+
}
289+
229290
private struct MatchedConstructor
230291
{
231292
public MatchedConstructor()

src/Moq.Analyzers/Moq.Analyzers.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,8 @@
127127
</PropertyGroup>
128128

129129
<ItemGroup>
130-
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" />
131-
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.0.1" />
130+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
131+
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.7.0" />
132132
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15">
133133
<PrivateAssets>all</PrivateAssets>
134134
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

src/Moq.Analyzers/MoqExpressionAnalyzer.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ public bool IsMockCreationStrictBehavior(ObjectCreationExpressionSyntax mockCrea
226226
argument = mockCreation.ArgumentList.Arguments[1];
227227
}
228228

229-
return this.IsStrictBehaviorArgument(argument, cancellationToken);
229+
return this.IsStrictBehaviorArgument(argument.Expression, cancellationToken);
230230
}
231231

232232
public bool IsMockOfStrictBehavior(ArgumentListSyntax? argumentList, CancellationToken cancellationToken)
@@ -244,15 +244,15 @@ public bool IsMockOfStrictBehavior(ArgumentListSyntax? argumentList, Cancellatio
244244
return false;
245245
}
246246

247-
return this.IsStrictBehaviorArgument(lastArgument, cancellationToken);
247+
return this.IsStrictBehaviorArgument(lastArgument.Expression, cancellationToken);
248248
}
249249

250-
public bool IsStrictBehaviorArgument(ArgumentSyntax argument, out MemberAccessExpressionSyntax? memberAccessExpression, CancellationToken cancellationToken)
250+
public bool IsStrictBehaviorArgument(ExpressionSyntax argument, out MemberAccessExpressionSyntax? memberAccessExpression, CancellationToken cancellationToken)
251251
{
252252
memberAccessExpression = null;
253253

254254
// Check it is a MemberAccessExpressionSyntax (because we searching for MockBehavior.XXXXX).
255-
if (argument.Expression is not MemberAccessExpressionSyntax expression)
255+
if (argument is not MemberAccessExpressionSyntax expression)
256256
{
257257
return false;
258258
}
@@ -621,7 +621,7 @@ private bool IsMockSetupMethod(InvocationExpressionSyntax invocationExpression,
621621
return true;
622622
}
623623

624-
private bool IsStrictBehaviorArgument(ArgumentSyntax argument, CancellationToken cancellationToken)
624+
private bool IsStrictBehaviorArgument(ExpressionSyntax argument, CancellationToken cancellationToken)
625625
{
626626
if (!this.IsStrictBehaviorArgument(argument, out var memberAccessExpression, cancellationToken))
627627
{

tests/Moq.Analyzers.Tests/Analyzers/ConstructorArgumentsAnalyzerTest.cs

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,13 +212,18 @@ private C()
212212
}
213213

214214
[Theory]
215+
[InlineData("1")]
215216
[InlineData("1, \"B\"")]
216217
[InlineData("1, null")]
217218
[InlineData("default, \"B\"")]
218219
[InlineData("(int)default, default")]
219220
[InlineData("default, null, 1234")]
220221
[InlineData("1, \"An object\", 3, null")]
221222
[InlineData("1, \"An object\", 3, new System.IO.MemoryStream()")]
223+
[InlineData("[new string[] { \"A\", \"B\" }]")]
224+
[InlineData("new object[] { new string[] { \"A\", \"B\" } }")]
225+
[InlineData("[1]")]
226+
[InlineData("[1, \"A\"]")]
222227
public async Task Arguments_Match(string parameters)
223228
{
224229
var source = @"
@@ -255,6 +260,10 @@ public C(int a, int[] b, int c)
255260
public C(int a, object b, int c, System.IDisposable d)
256261
{
257262
}
263+
264+
public C(string[] array)
265+
{
266+
}
258267
}
259268
}";
260269

@@ -340,6 +349,8 @@ public void TestMethod()
340349
[InlineData("1, \"B\"")]
341350
[InlineData("1, \"An object\", 3, null")]
342351
[InlineData("1, \"An object\", 3, new System.IO.MemoryStream()")]
352+
[InlineData("[1]")]
353+
[InlineData("[1, \"A\"]")]
343354
public async Task Arguments_Match_WithMockBehavior(string parameters)
344355
{
345356
var source = @"
@@ -372,6 +383,10 @@ public C(int a, object c)
372383
public C(int a, object b, int c, System.IDisposable d)
373384
{
374385
}
386+
387+
public C(string[] array)
388+
{
389+
}
375390
}
376391
}";
377392

@@ -408,6 +423,8 @@ public void TestMethod()
408423
[InlineData("null")]
409424
[InlineData("\"The string\", 2")]
410425
[InlineData("1, 2, 3, \"The string\"")]
426+
[InlineData("new int[] { 1, 2 }, 1000")]
427+
[InlineData("new object[] { \"A\", \"B\" }, 1000")]
411428
public async Task Arguments_NotMatch(string parameters)
412429
{
413430
var source = @"
@@ -440,12 +457,56 @@ public C(int a, object c)
440457
public C(int a, object b, int c, System.IDisposable d)
441458
{
442459
}
460+
461+
public C(string[] array, int b)
462+
{
463+
}
443464
}
444465
}";
445466

446467
await Verifier.VerifyAnalyzerAsync(source);
447468
}
448469

470+
[Theory]
471+
[InlineData("[]")]
472+
public async Task Arguments_NotMatch_EmptyArray(string parameters)
473+
{
474+
var source = @"
475+
namespace ConsoleApplication1
476+
{
477+
using Moq;
478+
479+
public class TestClass
480+
{
481+
public void TestMethod()
482+
{
483+
var mock = new Mock<C>{|PosInfoMoq2005:(" + parameters + @")|};
484+
}
485+
}
486+
487+
public class C
488+
{
489+
public C(int a)
490+
{
491+
}
492+
493+
public C(int a, string b)
494+
{
495+
}
496+
497+
public C(int a, object c)
498+
{
499+
}
500+
501+
public C(string[] array, int b)
502+
{
503+
}
504+
}
505+
}";
506+
507+
await Verifier.VerifyAnalyzerAsync(source);
508+
}
509+
449510
[Fact]
450511
public async Task Arguments_WithDefaultParameters()
451512
{
@@ -474,7 +535,7 @@ public C(int a = 0, int b = 1, int c = 2, int d = 3)
474535
}
475536

476537
[Fact]
477-
public async Task Arguments_NotMatch_WithNoContructor()
538+
public async Task Arguments_NotMatch_WithNoConstructor()
478539
{
479540
var source = @"
480541
namespace ConsoleApplication1
@@ -500,6 +561,7 @@ public class ClassWithNoConstructor { }
500561
[InlineData("null")]
501562
[InlineData("\"The string\", 2")]
502563
[InlineData("1, 2, 3, \"The string\"")]
564+
[InlineData("new int[] { 1, 2 }, 1000")]
503565
public async Task Arguments_NotMatch_WithMockBehavior(string parameters)
504566
{
505567
var source = @"
@@ -528,6 +590,50 @@ public C(int a, string b)
528590
public C(int a, object c)
529591
{
530592
}
593+
594+
public C(string[] array, int b)
595+
{
596+
}
597+
}
598+
}";
599+
600+
await Verifier.VerifyAnalyzerAsync(source);
601+
}
602+
603+
[Theory]
604+
[InlineData("[]")]
605+
public async Task Arguments_NotMatch_WithMockBehavior_EmptyArray(string parameters)
606+
{
607+
var source = @"
608+
namespace ConsoleApplication1
609+
{
610+
using Moq;
611+
612+
public class TestClass
613+
{
614+
public void TestMethod()
615+
{
616+
var mock = new Mock<C>{|PosInfoMoq2005:(MockBehavior.Strict, " + parameters + @")|};
617+
}
618+
}
619+
620+
public class C
621+
{
622+
public C(int a)
623+
{
624+
}
625+
626+
public C(int a, string b)
627+
{
628+
}
629+
630+
public C(int a, object c)
631+
{
632+
}
633+
634+
public C(string[] array, int b)
635+
{
636+
}
531637
}
532638
}";
533639

0 commit comments

Comments
 (0)