diff --git a/docs/Rules/MA0009.md b/docs/Rules/MA0009.md index cf057476..8fea5782 100644 --- a/docs/Rules/MA0009.md +++ b/docs/Rules/MA0009.md @@ -5,15 +5,21 @@ Sources: [GeneratedRegexAttributeUsageAnalyzer.cs](https://github.com/meziantou/ ````csharp new Regex(""); // not compliant -new Regex("", RegexOptions.None); // notcompliant +new Regex("", RegexOptions.None); // not compliant new Regex("", RegexOptions.None, TimeSpan.FromSeconds(1)); // ok [GeneratedRegex(""pattern"", RegexOptions.None)] // not compliant private static partial Regex Test(); -[GeneratedRegex(""pattern"", RegexOptions.None, matchTimeoutMilliseconds: 1000)] // ok compliant +[GeneratedRegex(""pattern"", RegexOptions.None, matchTimeoutMilliseconds: 1000)] // compliant private static partial Regex Test(); + +[GeneratedRegex(""pattern"", RegexOptions.None)] // not compliant +private static partial Regex Test { get; } + +[GeneratedRegex(""pattern"", RegexOptions.None, matchTimeoutMilliseconds: 1000)] // compliant +private static partial Regex Test { get; } ```` diff --git a/docs/Rules/MA0023.md b/docs/Rules/MA0023.md index 3cbec0a3..be22c3ca 100644 --- a/docs/Rules/MA0023.md +++ b/docs/Rules/MA0023.md @@ -9,4 +9,10 @@ Using named groups clarifies what is to be captured. It also makes the regex mor new Regex("a(b)"); // non-compliant new Regex("a(b)", RegexOptions.ExplicitCapture); // ok new Regex("a(?b)"); // ok + +[GeneratedRegex("a(b)", RegexOptions.None, matchTimeoutMilliseconds: 1000)] // non-compliant +private static partial Regex SampleRegex { get; } + +[GeneratedRegex("a(b)", RegexOptions.ExplicitCapture, matchTimeoutMilliseconds: 1000)] // ok +private static partial Regex SampleRegex { get; } ```` diff --git a/src/Meziantou.Analyzer/Rules/GeneratedRegexAttributeUsageAnalyzer.cs b/src/Meziantou.Analyzer/Rules/GeneratedRegexAttributeUsageAnalyzer.cs index fb97a549..280aee7b 100644 --- a/src/Meziantou.Analyzer/Rules/GeneratedRegexAttributeUsageAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/GeneratedRegexAttributeUsageAnalyzer.cs @@ -11,6 +11,6 @@ public override void Initialize(AnalysisContext context) context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); - context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method); + context.RegisterSymbolAction(AnalyzeGeneratedRegexSymbol, SymbolKind.Method, SymbolKind.Property); } } diff --git a/src/Meziantou.Analyzer/Rules/RegexUsageAnalyzerBase.cs b/src/Meziantou.Analyzer/Rules/RegexUsageAnalyzerBase.cs index efc75570..3fdc9bab 100644 --- a/src/Meziantou.Analyzer/Rules/RegexUsageAnalyzerBase.cs +++ b/src/Meziantou.Analyzer/Rules/RegexUsageAnalyzerBase.cs @@ -33,21 +33,27 @@ public abstract class RegexUsageAnalyzerBase : DiagnosticAnalyzer public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(TimeoutRule, ExplicitCaptureRule); - protected static void AnalyzeMethod(SymbolAnalysisContext context) + protected static void AnalyzeGeneratedRegexSymbol(SymbolAnalysisContext context) { - var method = (IMethodSymbol)context.Symbol; - if (method.MethodKind is not MethodKind.Ordinary) + if (context.Symbol is IMethodSymbol method && method.MethodKind is not MethodKind.Ordinary) + return; + + if (context.Symbol is not IMethodSymbol and not IPropertySymbol) return; var generatorAttributeSymbol = context.Compilation.GetBestTypeByMetadataName("System.Text.RegularExpressions.GeneratedRegexAttribute"); if (generatorAttributeSymbol is null) return; - foreach (var attribute in method.GetAttributes()) + foreach (var attribute in context.Symbol.GetAttributes()) { // https://github.com/dotnet/runtime/issues/58880 if (attribute.AttributeClass.IsEqualTo(generatorAttributeSymbol)) { + var attributeSyntaxReference = attribute.ApplicationSyntaxReference; + if (attributeSyntaxReference is not null && !context.Symbol.DeclaringSyntaxReferences.Any(reference => reference.SyntaxTree == attributeSyntaxReference.SyntaxTree && reference.Span.Contains(attributeSyntaxReference.Span))) + continue; + var regexOptions = RegexOptions.None; // RegexOptions.ExplicitCapture @@ -57,13 +63,13 @@ protected static void AnalyzeMethod(SymbolAnalysisContext context) regexOptions = (RegexOptions)(int)attribute.ConstructorArguments[1].Value!; if (pattern is not null && ShouldAddExplicitCapture(pattern, regexOptions)) { - if (attribute.ApplicationSyntaxReference is not null) + if (attributeSyntaxReference is not null) { - context.ReportDiagnostic(ExplicitCaptureRule, attribute.ApplicationSyntaxReference); + context.ReportDiagnostic(ExplicitCaptureRule, attributeSyntaxReference); } else { - context.ReportDiagnostic(ExplicitCaptureRule, method); + context.ReportDiagnostic(ExplicitCaptureRule, context.Symbol); } } } @@ -71,13 +77,13 @@ protected static void AnalyzeMethod(SymbolAnalysisContext context) // Timeout if (!HasNonBacktracking(regexOptions) && attribute.ConstructorArguments.Length < 3) { - if (attribute.ApplicationSyntaxReference is not null) + if (attributeSyntaxReference is not null) { - context.ReportDiagnostic(TimeoutRule, attribute.ApplicationSyntaxReference); + context.ReportDiagnostic(TimeoutRule, attributeSyntaxReference); } else { - context.ReportDiagnostic(TimeoutRule, method); + context.ReportDiagnostic(TimeoutRule, context.Symbol); } } } diff --git a/tests/Meziantou.Analyzer.Test/Helpers/ProjectBuilder.cs b/tests/Meziantou.Analyzer.Test/Helpers/ProjectBuilder.cs index 9306f91e..5fe6ec2b 100755 --- a/tests/Meziantou.Analyzer.Test/Helpers/ProjectBuilder.cs +++ b/tests/Meziantou.Analyzer.Test/Helpers/ProjectBuilder.cs @@ -74,6 +74,31 @@ bool IsCacheValid() } if (!IsCacheValid()) + { + await DownloadPackageWithRetries().ConfigureAwait(false); + } + + async Task DownloadPackageWithRetries() + { + const int MaxAttempts = 5; + for (var attempt = 1; ; attempt++) + { + try + { + await DownloadPackage().ConfigureAwait(false); + return; + } + catch (Exception ex) when (!IsLastAttempt(attempt) && IsTransientException(ex)) + { + await Task.Delay(100 * attempt).ConfigureAwait(false); + } + } + + static bool IsLastAttempt(int attempt) => attempt >= MaxAttempts; + static bool IsTransientException(Exception exception) => exception is HttpRequestException or IOException or InvalidDataException; + } + + async Task DownloadPackage() { var tempFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); try diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseRegexOptionsAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseRegexOptionsAnalyzerTests.cs index 46e4ad2e..a847a12e 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/UseRegexOptionsAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/UseRegexOptionsAnalyzerTests.cs @@ -9,6 +9,7 @@ public sealed class UseRegexOptionsAnalyzerTests private static ProjectBuilder CreateProjectBuilder() { return new ProjectBuilder() + .WithFrameworkSourceGenerators() .WithAnalyzer() .WithAnalyzer() .WithCodeFixProvider(); @@ -121,12 +122,7 @@ partial class TestClass partial class TestClass { private static partial Regex Test() => throw null; -}") - .ShouldReportDiagnostic(new DiagnosticResult - { - Id = "MA0023", - Locations = [new DiagnosticResultLocation("Test0.cs", 4, 6, 4, 92)], - }); +}"); await project.ValidateAsync(); } @@ -151,11 +147,6 @@ partial class TestClass private static partial Regex Test() => throw null; } """) - .ShouldReportDiagnostic(new DiagnosticResult - { - Id = "MA0023", - Locations = [new DiagnosticResultLocation("Test0.cs", 4, 6, 4, 92)], - }) .ShouldFixCodeWith(""" using System.Text.RegularExpressions; partial class TestClass @@ -171,4 +162,87 @@ partial class TestClass await project.ValidateAsync(); } + +#if CSHARP13_OR_GREATER + [Fact] + public async Task GeneratedRegexProperty_RegexOptions_Valid() + { + await CreateProjectBuilder() + .WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.Preview) + .WithTargetFramework(TargetFramework.Net9_0) + .WithSourceCode(""" + using System.Text.RegularExpressions; + + partial class TestClass + { + [GeneratedRegex("(?[a-z]+)", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase, 0)] + private static partial Regex Test { get; } + } + partial class TestClass + { + private static partial Regex Test { get => throw null; } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task GeneratedRegexProperty_RegexOptions_Invalid() + { + await CreateProjectBuilder() + .WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.Preview) + .WithTargetFramework(TargetFramework.Net9_0) + .WithSourceCode(""" + using System.Text.RegularExpressions; + + partial class TestClass + { + [[|GeneratedRegex("([a-z]+)", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase, 0)|]] + private static partial Regex Test { get; } + } + partial class TestClass + { + private static partial Regex Test { get => throw null; } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task GeneratedRegexProperty_RegexOptions_Invalid_CodeFix() + { + await new ProjectBuilder() + .WithAnalyzer() + .WithCodeFixProvider() + .WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.Preview) + .WithTargetFramework(TargetFramework.Net9_0) + .WithSourceCode(""" + using System.Text.RegularExpressions; + + partial class TestClass + { + [[|GeneratedRegex("([a-z]+)", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase, 0)|]] + private static partial Regex Test { get; } + } + partial class TestClass + { + private static partial Regex Test { get => throw null; } + } + """) + .ShouldFixCodeWith(""" + using System.Text.RegularExpressions; + + partial class TestClass + { + [GeneratedRegex("([a-z]+)", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture, 0)] + private static partial Regex Test { get; } + } + partial class TestClass + { + private static partial Regex Test { get => throw null; } + } + """) + .ValidateAsync(); + } +#endif } diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseRegexTimeoutAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseRegexTimeoutAnalyzerTests.cs index 4f78a2de..bc68a581 100755 --- a/tests/Meziantou.Analyzer.Test/Rules/UseRegexTimeoutAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/UseRegexTimeoutAnalyzerTests.cs @@ -159,12 +159,7 @@ partial class TestClass var project = CreateProjectBuilder() .WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.Preview) .WithTargetFramework(TargetFramework.Net7_0) - .WithSourceCode(SourceCode) - .ShouldReportDiagnostic(new DiagnosticResult - { - Id = RuleIdentifiers.MissingTimeoutParameterForRegex, - Locations = [new DiagnosticResultLocation("Test0.cs", 4, 6, 4, 50)], - }); + .WithSourceCode(SourceCode); await project.ValidateAsync(); } @@ -230,4 +225,60 @@ partial class TestClass await project.ValidateAsync(); } + +#if CSHARP13_OR_GREATER + [Fact] + public async Task GeneratedRegexProperty_WithoutTimeout() + { + await CreateProjectBuilder() + .WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.Preview) + .WithTargetFramework(TargetFramework.Net9_0) + .WithSourceCode(""" + using System.Text.RegularExpressions; + + partial class TestClass + { + [[|GeneratedRegex("pattern", RegexOptions.None)|]] + private static partial Regex Test { get; } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task GeneratedRegexProperty_WithTimeout() + { + await CreateProjectBuilder() + .WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.Preview) + .WithTargetFramework(TargetFramework.Net9_0) + .WithSourceCode(""" + using System.Text.RegularExpressions; + + partial class TestClass + { + [GeneratedRegex("pattern", RegexOptions.None, matchTimeoutMilliseconds: 1000)] + private static partial Regex Test { get; } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task GeneratedRegexProperty_WithoutTimeout_NonBacktracking() + { + await CreateProjectBuilder() + .WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.Preview) + .WithTargetFramework(TargetFramework.Net9_0) + .WithSourceCode(""" + using System.Text.RegularExpressions; + + partial class TestClass + { + [GeneratedRegex("pattern", RegexOptions.NonBacktracking)] + private static partial Regex Test { get; } + } + """) + .ValidateAsync(); + } +#endif }