Skip to content

Commit

Permalink
add support for StringAsserts Matches (#224)
Browse files Browse the repository at this point in the history
* add support for StringAsserts Matches

* add using System.Text.RegularExpressions for xunit tests
  • Loading branch information
Meir017 authored Sep 27, 2023
1 parent dba288b commit 4baf515
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 1 deletion.
1 change: 1 addition & 0 deletions src/FluentAssertions.Analyzers.Tests/GenerateCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ public static string GenericIListExpressionBodyAssertion(string assertion) => Ge
public static string XunitAssertion(string methodArguments, string assertion) => new StringBuilder()
.AppendLine("using System;")
.AppendLine("using System.Collections.Generic;")
.AppendLine("using System.Text.RegularExpressions;")
.AppendLine("using FluentAssertions;")
.AppendLine("using FluentAssertions.Extensions;")
.AppendLine("using Xunit;")
Expand Down
29 changes: 28 additions & 1 deletion src/FluentAssertions.Analyzers.Tests/Tips/XunitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,33 @@ public void AssertStringDoesNotContain_TestAnalyzer(string assertion) =>
public void AssertStringDoesNotContain_TestCodeFix(string oldAssertion, string newAssertion)
=> VerifyCSharpFix<AssertDoesNotContainCodeFix, AssertDoesNotContainAnalyzer>("string actual, string expected", oldAssertion, newAssertion);

[DataTestMethod]
[DataRow("Assert.Matches(expectedRegexPattern, actual);")]
[Implemented]
public void AssertStringMatches_String_TestAnalyzer(string assertion) =>
VerifyCSharpDiagnostic<AssertMatchesAnalyzer>("string actual, string expectedRegexPattern", assertion);

[DataTestMethod]
[DataRow(
/* oldAssertion: */ "Assert.Matches(expectedRegexPattern, actual);",
/* newAssertion: */ "actual.Should().MatchRegex(expectedRegexPattern);")]
[Implemented]
public void AssertStringMatches_String_TestCodeFix(string oldAssertion, string newAssertion)
=> VerifyCSharpFix<AssertMatchesCodeFix, AssertMatchesAnalyzer>("string actual, string expectedRegexPattern", oldAssertion, newAssertion);

[DataTestMethod]
[DataRow("Assert.Matches(expectedRegex, actual);")]
[Implemented]
public void AssertStringMatches_Regex_TestAnalyzer(string assertion) =>
VerifyCSharpDiagnostic<AssertMatchesAnalyzer>("string actual, Regex expectedRegex", assertion);

[DataTestMethod]
[DataRow(
/* oldAssertion: */ "Assert.Matches(expectedRegex, actual);",
/* newAssertion: */ "actual.Should().MatchRegex(expectedRegex);")]
[Implemented]
public void AssertStringMatches_Regex_TestCodeFix(string oldAssertion, string newAssertion)
=> VerifyCSharpFix<AssertMatchesCodeFix, AssertMatchesAnalyzer>("string actual, Regex expectedRegex", oldAssertion, newAssertion);

private void VerifyCSharpDiagnostic<TDiagnosticAnalyzer>(string methodArguments, string assertion) where TDiagnosticAnalyzer : Microsoft.CodeAnalysis.Diagnostics.DiagnosticAnalyzer, new()
{
Expand All @@ -383,7 +410,7 @@ public void AssertStringDoesNotContain_TestCodeFix(string oldAssertion, string n
Message = message,
Locations = new DiagnosticResultLocation[]
{
new("Test0.cs", 13, 13)
new("Test0.cs", 14, 13)
},
Severity = DiagnosticSeverity.Info
});
Expand Down
1 change: 1 addition & 0 deletions src/FluentAssertions.Analyzers/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ public static class Xunit
public const string AssertNotNull = $"{DiagnosticProperties.IdPrefix}0709";
public const string AssertContains = $"{DiagnosticProperties.IdPrefix}0710";
public const string AssertDoesNotContain = $"{DiagnosticProperties.IdPrefix}0711";
public const string AssertMatches = $"{DiagnosticProperties.IdPrefix}0712";
}
}

Expand Down
58 changes: 58 additions & 0 deletions src/FluentAssertions.Analyzers/Tips/Xunit/AssertMatches.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using FluentAssertions.Analyzers.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace FluentAssertions.Analyzers.Xunit;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AssertMatchesAnalyzer : XunitAnalyzer
{
public const string DiagnosticId = Constants.Tips.Xunit.AssertMatches;
public const string Category = Constants.Tips.Category;

public const string Message = "Use .Should().MatchRegex()";

protected override DiagnosticDescriptor Rule => new(DiagnosticId, Title, Message, Category, DiagnosticSeverity.Info, true);

protected override IEnumerable<FluentAssertionsCSharpSyntaxVisitor> Visitors => new FluentAssertionsCSharpSyntaxVisitor[]
{
new AssertMatchesStringSyntaxVisitor()
};

//public static void Matches(string expectedRegexPattern, string? actualString)
//public static void Matches(Regex expectedRegex, string? actualString)
public class AssertMatchesStringSyntaxVisitor : FluentAssertionsCSharpSyntaxVisitor
{
public AssertMatchesStringSyntaxVisitor() : base(
MemberValidator.ArgumentsMatch("Matches",
ArgumentValidator.IsAnyType(TypeSelector.GetStringType, TypeSelector.GetRegexType),
ArgumentValidator.IsType(TypeSelector.GetStringType))
)
{
}
}
}

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AssertMatchesCodeFix)), Shared]
public class AssertMatchesCodeFix : XunitCodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(AssertMatchesAnalyzer.DiagnosticId);

protected override ExpressionSyntax GetNewExpression(
ExpressionSyntax expression,
FluentAssertionsDiagnosticProperties properties)
{
switch (properties.VisitorName)
{
case nameof(AssertMatchesAnalyzer.AssertMatchesStringSyntaxVisitor):
return RenameMethodAndReorderActualExpectedAndReplaceWithSubjectShould(expression, "Matches", "MatchRegex");
default:
throw new System.InvalidOperationException($"Invalid visitor name - {properties.VisitorName}");
}
}
}
2 changes: 2 additions & 0 deletions src/FluentAssertions.Analyzers/Utilities/ArgumentValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public static ArgumentPredicate IsIdentifier()
=> (argument, semanticModel) => argument.Expression.IsKind(SyntaxKind.IdentifierName);
public static ArgumentPredicate IsType(Func<SemanticModel, INamedTypeSymbol> typeSelector)
=> (argument, semanticModel) => semanticModel.GetTypeInfo(argument.Expression).Type?.Equals(typeSelector(semanticModel), SymbolEqualityComparer.Default) ?? false;
public static ArgumentPredicate IsAnyType(params Func<SemanticModel, INamedTypeSymbol>[] typeSelectors)
=> (argument, semanticModel) => Array.Exists(typeSelectors, typeSelector => IsType(typeSelector)(argument, semanticModel));
public static ArgumentPredicate IsNull()
=> (argument, semanticModel) => argument.Expression is LiteralExpressionSyntax literal && literal.Token.IsKind(SyntaxKind.NullKeyword);
}
Expand Down
4 changes: 4 additions & 0 deletions src/FluentAssertions.Analyzers/Utilities/TypeSelector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;

namespace FluentAssertions.Analyzers.Utilities
{
Expand Down Expand Up @@ -32,6 +33,9 @@ public static INamedTypeSymbol GetTimeSpanType(this SemanticModel semanticModel)
public static INamedTypeSymbol GetStringType(this SemanticModel semanticModel)
=> GetTypeFrom(semanticModel, SpecialType.System_String);

public static INamedTypeSymbol GetRegexType(this SemanticModel semanticModel)
=> GetTypeFrom(semanticModel, typeof(Regex));

public static INamedTypeSymbol GetCultureInfoType(this SemanticModel semanticModel)
=> GetTypeFrom(semanticModel, typeof(CultureInfo));

Expand Down

0 comments on commit 4baf515

Please sign in to comment.