Skip to content

Commit 4baf515

Browse files
authored
add support for StringAsserts Matches (#224)
* add support for StringAsserts Matches * add using System.Text.RegularExpressions for xunit tests
1 parent dba288b commit 4baf515

File tree

6 files changed

+94
-1
lines changed

6 files changed

+94
-1
lines changed

src/FluentAssertions.Analyzers.Tests/GenerateCode.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ public static string GenericIListExpressionBodyAssertion(string assertion) => Ge
225225
public static string XunitAssertion(string methodArguments, string assertion) => new StringBuilder()
226226
.AppendLine("using System;")
227227
.AppendLine("using System.Collections.Generic;")
228+
.AppendLine("using System.Text.RegularExpressions;")
228229
.AppendLine("using FluentAssertions;")
229230
.AppendLine("using FluentAssertions.Extensions;")
230231
.AppendLine("using Xunit;")

src/FluentAssertions.Analyzers.Tests/Tips/XunitTests.cs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,33 @@ public void AssertStringDoesNotContain_TestAnalyzer(string assertion) =>
368368
public void AssertStringDoesNotContain_TestCodeFix(string oldAssertion, string newAssertion)
369369
=> VerifyCSharpFix<AssertDoesNotContainCodeFix, AssertDoesNotContainAnalyzer>("string actual, string expected", oldAssertion, newAssertion);
370370

371+
[DataTestMethod]
372+
[DataRow("Assert.Matches(expectedRegexPattern, actual);")]
373+
[Implemented]
374+
public void AssertStringMatches_String_TestAnalyzer(string assertion) =>
375+
VerifyCSharpDiagnostic<AssertMatchesAnalyzer>("string actual, string expectedRegexPattern", assertion);
376+
377+
[DataTestMethod]
378+
[DataRow(
379+
/* oldAssertion: */ "Assert.Matches(expectedRegexPattern, actual);",
380+
/* newAssertion: */ "actual.Should().MatchRegex(expectedRegexPattern);")]
381+
[Implemented]
382+
public void AssertStringMatches_String_TestCodeFix(string oldAssertion, string newAssertion)
383+
=> VerifyCSharpFix<AssertMatchesCodeFix, AssertMatchesAnalyzer>("string actual, string expectedRegexPattern", oldAssertion, newAssertion);
384+
385+
[DataTestMethod]
386+
[DataRow("Assert.Matches(expectedRegex, actual);")]
387+
[Implemented]
388+
public void AssertStringMatches_Regex_TestAnalyzer(string assertion) =>
389+
VerifyCSharpDiagnostic<AssertMatchesAnalyzer>("string actual, Regex expectedRegex", assertion);
390+
391+
[DataTestMethod]
392+
[DataRow(
393+
/* oldAssertion: */ "Assert.Matches(expectedRegex, actual);",
394+
/* newAssertion: */ "actual.Should().MatchRegex(expectedRegex);")]
395+
[Implemented]
396+
public void AssertStringMatches_Regex_TestCodeFix(string oldAssertion, string newAssertion)
397+
=> VerifyCSharpFix<AssertMatchesCodeFix, AssertMatchesAnalyzer>("string actual, Regex expectedRegex", oldAssertion, newAssertion);
371398

372399
private void VerifyCSharpDiagnostic<TDiagnosticAnalyzer>(string methodArguments, string assertion) where TDiagnosticAnalyzer : Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer, new()
373400
{
@@ -383,7 +410,7 @@ public void AssertStringDoesNotContain_TestCodeFix(string oldAssertion, string n
383410
Message = message,
384411
Locations = new DiagnosticResultLocation[]
385412
{
386-
new("Test0.cs", 13, 13)
413+
new("Test0.cs", 14, 13)
387414
},
388415
Severity = DiagnosticSeverity.Info
389416
});

src/FluentAssertions.Analyzers/Constants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ public static class Xunit
131131
public const string AssertNotNull = $"{DiagnosticProperties.IdPrefix}0709";
132132
public const string AssertContains = $"{DiagnosticProperties.IdPrefix}0710";
133133
public const string AssertDoesNotContain = $"{DiagnosticProperties.IdPrefix}0711";
134+
public const string AssertMatches = $"{DiagnosticProperties.IdPrefix}0712";
134135
}
135136
}
136137

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using System.Collections.Generic;
2+
using System.Collections.Immutable;
3+
using System.Composition;
4+
using FluentAssertions.Analyzers.Utilities;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CodeFixes;
7+
using Microsoft.CodeAnalysis.CSharp.Syntax;
8+
using Microsoft.CodeAnalysis.Diagnostics;
9+
10+
namespace FluentAssertions.Analyzers.Xunit;
11+
12+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
13+
public class AssertMatchesAnalyzer : XunitAnalyzer
14+
{
15+
public const string DiagnosticId = Constants.Tips.Xunit.AssertMatches;
16+
public const string Category = Constants.Tips.Category;
17+
18+
public const string Message = "Use .Should().MatchRegex()";
19+
20+
protected override DiagnosticDescriptor Rule => new(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true);
21+
22+
protected override IEnumerable<FluentAssertionsCSharpSyntaxVisitor> Visitors => new FluentAssertionsCSharpSyntaxVisitor[]
23+
{
24+
new AssertMatchesStringSyntaxVisitor()
25+
};
26+
27+
//public static void Matches(string expectedRegexPattern, string? actualString)
28+
//public static void Matches(Regex expectedRegex, string? actualString)
29+
public class AssertMatchesStringSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor
30+
{
31+
public AssertMatchesStringSyntaxVisitor() : base(
32+
MemberValidator.ArgumentsMatch("Matches",
33+
ArgumentValidator.IsAnyType(TypeSelector.GetStringType, TypeSelector.GetRegexType),
34+
ArgumentValidator.IsType(TypeSelector.GetStringType))
35+
)
36+
{
37+
}
38+
}
39+
}
40+
41+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AssertMatchesCodeFix)), Shared]
42+
public class AssertMatchesCodeFix : XunitCodeFixProvider
43+
{
44+
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(AssertMatchesAnalyzer.DiagnosticId);
45+
46+
protected override ExpressionSyntax GetNewExpression(
47+
ExpressionSyntax expression,
48+
FluentAssertionsDiagnosticProperties properties)
49+
{
50+
switch (properties.VisitorName)
51+
{
52+
case nameof(AssertMatchesAnalyzer.AssertMatchesStringSyntaxVisitor):
53+
return RenameMethodAndReorderActualExpectedAndReplaceWithSubjectShould(expression, "Matches", "MatchRegex");
54+
default:
55+
throw new System.InvalidOperationException($"Invalid visitor name - {properties.VisitorName}");
56+
}
57+
}
58+
}

src/FluentAssertions.Analyzers/Utilities/ArgumentValidator.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ public static ArgumentPredicate IsIdentifier()
1111
=> (argument, semanticModel) => argument.Expression.IsKind(SyntaxKind.IdentifierName);
1212
public static ArgumentPredicate IsType(Func<SemanticModel, INamedTypeSymbol> typeSelector)
1313
=> (argument, semanticModel) => semanticModel.GetTypeInfo(argument.Expression).Type?.Equals(typeSelector(semanticModel), SymbolEqualityComparer.Default) ?? false;
14+
public static ArgumentPredicate IsAnyType(params Func<SemanticModel, INamedTypeSymbol>[] typeSelectors)
15+
=> (argument, semanticModel) => Array.Exists(typeSelectors, typeSelector => IsType(typeSelector)(argument, semanticModel));
1416
public static ArgumentPredicate IsNull()
1517
=> (argument, semanticModel) => argument.Expression is LiteralExpressionSyntax literal && literal.Token.IsKind(SyntaxKind.NullKeyword);
1618
}

src/FluentAssertions.Analyzers/Utilities/TypeSelector.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Collections;
44
using System.Collections.Generic;
55
using System.Globalization;
6+
using System.Text.RegularExpressions;
67

78
namespace FluentAssertions.Analyzers.Utilities
89
{
@@ -32,6 +33,9 @@ public static INamedTypeSymbol GetTimeSpanType(this SemanticModel semanticModel)
3233
public static INamedTypeSymbol GetStringType(this SemanticModel semanticModel)
3334
=> GetTypeFrom(semanticModel, SpecialType.System_String);
3435

36+
public static INamedTypeSymbol GetRegexType(this SemanticModel semanticModel)
37+
=> GetTypeFrom(semanticModel, typeof(Regex));
38+
3539
public static INamedTypeSymbol GetCultureInfoType(this SemanticModel semanticModel)
3640
=> GetTypeFrom(semanticModel, typeof(CultureInfo));
3741

0 commit comments

Comments
 (0)