diff --git a/Atc.Analyzer.slnx b/Atc.Analyzer.slnx
index fc60e87..e0f50f1 100644
--- a/Atc.Analyzer.slnx
+++ b/Atc.Analyzer.slnx
@@ -13,6 +13,7 @@
+
diff --git a/CLAUDE.md b/CLAUDE.md
index e901dcc..66c5877 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -116,6 +116,40 @@ private static void AnalyzeSomething(SyntaxNodeAnalysisContext context)
This pattern is used by GlobalUsings analyzers and should be applied to other analyzers that shouldn't analyze generated code.
+#### Analyzing Source-Generated Code
+
+**Default behavior:** Most analyzers use `ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None)` to skip generated code.
+
+**Special case:** Analyzers that need to analyze **user-written attributes or declarations** that trigger source generators should use different flags:
+
+```csharp
+public override void Initialize(AnalysisContext context)
+{
+ // For analyzers that need to analyze source-generated code declarations
+ context.ConfigureGeneratedCodeAnalysis(
+ GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
+ context.EnableConcurrentExecution();
+
+ context.RegisterSyntaxNodeAction(AnalyzeMethodDeclaration, SyntaxKind.MethodDeclaration);
+}
+```
+
+**When to use `Analyze | ReportDiagnostics`:**
+- Analyzing attributes that trigger source generators (e.g., `[GeneratedRegex]`)
+- Examining method/type declarations that will have generated implementations
+- Checking parameters or configurations of source generator attributes
+- Any scenario where the user-written code (not the generated output) needs analysis
+
+**Example:**
+The `GeneratedRegexCompiledFlagAnalyzer` (ATC301) must use `Analyze | ReportDiagnostics` because:
+- It analyzes the `[GeneratedRegex]` attribute parameters (user-written code)
+- The attribute triggers a source generator, but the attribute itself is in user source files
+- The redundant flag can be detected before code generation occurs
+
+**Important:** Register for the appropriate syntax kind:
+- For attribute-triggered analyzers, register for the decorated member type (e.g., `SyntaxKind.MethodDeclaration`)
+- NOT `SyntaxKind.Attribute` directly, as source-generated methods may not trigger attribute node analysis
+
### Code Fix Provider Implementation Pattern
When an analyzer can suggest automatic fixes, implement a CodeFixProvider in the same directory:
diff --git a/Directory.Build.props b/Directory.Build.props
index 2d1bf7b..37e5d32 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -41,7 +41,7 @@
-
+
diff --git a/README.md b/README.md
index a30acd0..b7ca17a 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,7 @@ Once installed, the analyzer will automatically run during compilation and highl
| [ATC210](docs/rules/ATC210.md) | Style | Use expression body syntax when appropriate | ⚠️ Warning | ✔️ Yes | ✅ Yes |
| [ATC220](docs/rules/ATC220.md) | Style | Use global usings for all namespaces (strict policy) | ⚠️ Warning | ✔️ Yes | ✅ Yes |
| [ATC221](docs/rules/ATC221.md) | Style | Use global usings for common namespaces (lenient policy) | ⚠️ Warning | ✔️ Yes | ✅ Yes |
+| [ATC301](docs/rules/ATC301.md) | Usage | Remove redundant RegexOptions.Compiled flag from [GeneratedRegex] attribute | ⚠️ Warning | ✔️ Yes | ✅ Yes |
### Severity Levels
@@ -36,6 +37,10 @@ Once installed, the analyzer will automatically run during compilation and highl
Rules that enforce consistent code formatting and style conventions.
+### Usage
+
+Rules that detect incorrect or redundant API usage patterns.
+
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
diff --git a/docs/rules/ATC301.md b/docs/rules/ATC301.md
new file mode 100644
index 0000000..4faa906
--- /dev/null
+++ b/docs/rules/ATC301.md
@@ -0,0 +1,247 @@
+# 🔗 ATC301: Remove redundant RegexOptions.Compiled flag from [GeneratedRegex] attribute
+
+## 📂 Category
+
+Usage
+
+## ⚠️ Severity
+
+Warning
+
+## 📖 Description
+
+Detects and removes redundant `RegexOptions.Compiled` flags from `[GeneratedRegex]` attributes. The `[GeneratedRegex]` attribute uses source generators to produce compiled regex code at build time, making the `Compiled` flag unnecessary and redundant.
+
+## 📋 Rules
+
+1. **GeneratedRegex attributes**: Do not use `RegexOptions.Compiled` flag with `[GeneratedRegex]` attributes
+2. **Already compiled**: The `[GeneratedRegex]` attribute generates compiled regex code at build time using source generators
+3. **No performance benefit**: Adding `RegexOptions.Compiled` has no effect and provides no additional performance benefit
+4. **Code clarity**: Removing the redundant flag improves code clarity by eliminating confusion about how `[GeneratedRegex]` works
+5. **Applies to**: All usages of the `[GeneratedRegex]` attribute from `System.Text.RegularExpressions`
+6. **Does NOT apply to**: Traditional regex construction using `new Regex()` where `RegexOptions.Compiled` is still meaningful
+
+## 💡 Motivation
+
+Using `RegexOptions.Compiled` with `[GeneratedRegex]`:
+- Is redundant because the source generator already produces compiled code
+- Can confuse developers about how source-generated regexes work
+- Adds unnecessary noise to the code
+- May mislead readers into thinking it provides additional optimization
+- Violates the principle of avoiding redundant code
+
+The `[GeneratedRegex]` attribute was introduced in .NET 7 to generate highly optimized regex code at compile time using source generators. This generated code is already compiled and optimized, making the `RegexOptions.Compiled` flag meaningless in this context.
+
+## 📝 Examples
+
+### Basic usage with single flag
+
+```csharp
+// non-compliant - Compiled flag is redundant
+[GeneratedRegex(@"\d+", RegexOptions.Compiled)]
+private static partial Regex NumberRegex();
+
+// compliant - No Compiled flag needed
+[GeneratedRegex(@"\d+")]
+private static partial Regex NumberRegex();
+```
+
+### Usage with multiple flags
+
+```csharp
+// non-compliant - Compiled flag is redundant even with other flags
+[GeneratedRegex(@"[a-z]+", RegexOptions.Compiled | RegexOptions.IgnoreCase)]
+private static partial Regex CaseInsensitiveRegex();
+
+// compliant - Keep only meaningful flags
+[GeneratedRegex(@"[a-z]+", RegexOptions.IgnoreCase)]
+private static partial Regex CaseInsensitiveRegex();
+```
+
+### Named parameters
+
+```csharp
+// non-compliant - Compiled flag in named parameter
+[GeneratedRegex(
+ pattern: @"[$>]",
+ options: RegexOptions.Compiled | RegexOptions.ExplicitCapture,
+ matchTimeoutMilliseconds: 1000)]
+private static partial Regex PromptRegex();
+
+// compliant - Remove Compiled flag
+[GeneratedRegex(
+ pattern: @"[$>]",
+ options: RegexOptions.ExplicitCapture,
+ matchTimeoutMilliseconds: 1000)]
+private static partial Regex PromptRegex();
+```
+
+### Multiple flags with Compiled in different positions
+
+```csharp
+// non-compliant - Compiled at the beginning
+[GeneratedRegex(@"^\d+$", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase)]
+private static partial Regex MultiRegex1();
+
+// non-compliant - Compiled in the middle
+[GeneratedRegex(@"^\d+$", RegexOptions.Multiline | RegexOptions.Compiled | RegexOptions.IgnoreCase)]
+private static partial Regex MultiRegex2();
+
+// non-compliant - Compiled at the end
+[GeneratedRegex(@"^\d+$", RegexOptions.Multiline | RegexOptions.IgnoreCase | RegexOptions.Compiled)]
+private static partial Regex MultiRegex3();
+
+// compliant - No Compiled flag
+[GeneratedRegex(@"^\d+$", RegexOptions.Multiline | RegexOptions.IgnoreCase)]
+private static partial Regex MultiRegex();
+```
+
+### Only Compiled flag
+
+```csharp
+// non-compliant - Compiled is the only option
+[GeneratedRegex(@"\w+", RegexOptions.Compiled)]
+private static partial Regex WordRegex();
+
+// compliant - Remove entire options parameter
+[GeneratedRegex(@"\w+")]
+private static partial Regex WordRegex();
+```
+
+### Traditional Regex construction (not affected by this rule)
+
+```csharp
+// compliant - This is NOT a GeneratedRegex attribute, so Compiled flag is meaningful
+private static readonly Regex CompiledRegex = new Regex(@"\d+", RegexOptions.Compiled);
+
+// compliant - Traditional Regex construction with multiple options
+private static readonly Regex MultiOptionsRegex =
+ new Regex(@"[a-z]+", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+```
+
+### Real-world example from user's code
+
+```csharp
+// non-compliant - User's original example
+[GeneratedRegex(
+ pattern: "[$>]",
+ options: RegexOptions.Compiled | RegexOptions.ExplicitCapture,
+ matchTimeoutMilliseconds: RegexTimeoutPrompt)]
+private static partial Regex UserOrFtpPromptRegex();
+
+// compliant - Corrected version
+[GeneratedRegex(
+ pattern: "[$>]",
+ options: RegexOptions.ExplicitCapture,
+ matchTimeoutMilliseconds: RegexTimeoutPrompt)]
+private static partial Regex UserOrFtpPromptRegex();
+```
+
+## ⚙️ Configuration
+
+This rule is not configurable. It always flags `RegexOptions.Compiled` when used with `[GeneratedRegex]` attributes.
+
+## 🤖 Source-Generated Code
+
+This rule analyzes methods with the `[GeneratedRegex]` attribute, which is a source generator attribute. The analyzer needs to examine these methods because:
+
+1. **User-written attributes**: The `[GeneratedRegex]` attribute and its parameters are written by developers in source code
+2. **Build-time generation**: The source generator creates the implementation at build time, but the attribute declaration remains in user code
+3. **Detectable before generation**: The redundant `RegexOptions.Compiled` flag can be detected in the attribute before code generation occurs
+
+**Important:** While the `[GeneratedRegex]` attribute triggers source generation, the attribute itself and its parameters are part of the user's source code and should be analyzed for correctness.
+
+Example of analyzed code:
+
+```csharp
+using System.Text.RegularExpressions;
+
+public partial class MyClass
+{
+ // ATC301 analyzes this attribute declaration (user-written code)
+ // even though the method implementation will be source-generated
+ [GeneratedRegex(@"\d+", RegexOptions.Compiled)] // ⚠️ ATC301 warning
+ private static partial Regex NumberRegex();
+}
+```
+
+## 🔧 Code Fix
+
+A code fix is available that automatically removes the redundant `RegexOptions.Compiled` flag from `[GeneratedRegex]` attributes.
+
+**How to apply:**
+1. Position cursor on the `[GeneratedRegex]` attribute that contains `RegexOptions.Compiled`
+2. Click the lightbulb (💡) or press `Ctrl+.` (Windows/Linux) or `Cmd+.` (Mac)
+3. Select **"Remove redundant RegexOptions.Compiled flag"**
+
+The code fix will:
+- Remove `RegexOptions.Compiled` from the options parameter
+- If `Compiled` is combined with other flags using `|`, remove only the `Compiled` flag and one adjacent `|` operator
+- If `Compiled` is the only flag, remove the entire options parameter
+- Preserve all other flags and formatting
+- Maintain proper syntax and structure of the attribute
+
+**Example 1 - Remove only flag:**
+```csharp
+// Before code fix
+[GeneratedRegex(@"\d+", RegexOptions.Compiled)]
+private static partial Regex NumberRegex();
+
+// After code fix (automatic)
+[GeneratedRegex(@"\d+")]
+private static partial Regex NumberRegex();
+```
+
+**Example 2 - Remove flag from combination (first position):**
+```csharp
+// Before code fix
+[GeneratedRegex(@"\d+", RegexOptions.Compiled | RegexOptions.IgnoreCase)]
+private static partial Regex NumberRegex();
+
+// After code fix (automatic)
+[GeneratedRegex(@"\d+", RegexOptions.IgnoreCase)]
+private static partial Regex NumberRegex();
+```
+
+**Example 3 - Remove flag from combination (last position):**
+```csharp
+// Before code fix
+[GeneratedRegex(@"\d+", RegexOptions.IgnoreCase | RegexOptions.Compiled)]
+private static partial Regex NumberRegex();
+
+// After code fix (automatic)
+[GeneratedRegex(@"\d+", RegexOptions.IgnoreCase)]
+private static partial Regex NumberRegex();
+```
+
+**Example 4 - Remove flag from multiple flags (middle position):**
+```csharp
+// Before code fix
+[GeneratedRegex(@"\d+", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Multiline)]
+private static partial Regex NumberRegex();
+
+// After code fix (automatic)
+[GeneratedRegex(@"\d+", RegexOptions.IgnoreCase | RegexOptions.Multiline)]
+private static partial Regex NumberRegex();
+```
+
+**Example 5 - Multi-line attribute with named parameters:**
+```csharp
+// Before code fix
+[GeneratedRegex(
+ pattern: @"[$>]",
+ options: RegexOptions.Compiled | RegexOptions.ExplicitCapture,
+ matchTimeoutMilliseconds: 1000)]
+private static partial Regex PromptRegex();
+
+// After code fix (automatic)
+[GeneratedRegex(
+ pattern: @"[$>]",
+ options: RegexOptions.ExplicitCapture,
+ matchTimeoutMilliseconds: 1000)]
+private static partial Regex PromptRegex();
+```
+
+## 🔗 Related Rules
+
+This is the first Usage category rule in the Atc.Analyzer project. As more Usage rules are added, they will be linked here.
diff --git a/sample/Atc.Analyzer.Sample.Atc301/Atc.Analyzer.Sample.Atc301.csproj b/sample/Atc.Analyzer.Sample.Atc301/Atc.Analyzer.Sample.Atc301.csproj
new file mode 100644
index 0000000..3b0ff39
--- /dev/null
+++ b/sample/Atc.Analyzer.Sample.Atc301/Atc.Analyzer.Sample.Atc301.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ net10.0
+ false
+ false
+
+
+
+
+ false
+ Analyzer
+
+
+
+
diff --git a/sample/Atc.Analyzer.Sample.Atc301/GlobalUsings.cs b/sample/Atc.Analyzer.Sample.Atc301/GlobalUsings.cs
new file mode 100644
index 0000000..d95ab04
--- /dev/null
+++ b/sample/Atc.Analyzer.Sample.Atc301/GlobalUsings.cs
@@ -0,0 +1,4 @@
+// Global using directives
+
+global using System.Text.RegularExpressions;
+global using Atc.Analyzer.Sample.Atc301.Scenarios;
\ No newline at end of file
diff --git a/sample/Atc.Analyzer.Sample.Atc301/Program.cs b/sample/Atc.Analyzer.Sample.Atc301/Program.cs
new file mode 100644
index 0000000..771becf
--- /dev/null
+++ b/sample/Atc.Analyzer.Sample.Atc301/Program.cs
@@ -0,0 +1,27 @@
+namespace Atc.Analyzer.Sample.Atc301;
+
+#pragma warning disable CA1303
+
+///
+/// This sample demonstrates VALID scenarios for ATC301 analyzer.
+/// ATC301 (GeneratedRegexCompiledFlagAnalyzer): Remove redundant RegexOptions.Compiled flag from [GeneratedRegex] attribute.
+/// - The [GeneratedRegex] attribute generates compiled regex code at build time using source generators
+/// - Adding RegexOptions.Compiled to the options parameter is redundant and has no effect
+/// - This sample shows correct usage WITHOUT the Compiled flag
+///
+internal static class Program
+{
+ private static void Main()
+ {
+ Console.WriteLine("ATC301: GeneratedRegex Compiled Flag Analyzer - Valid Scenarios Demo");
+ Console.WriteLine();
+
+ // Demonstrate various valid scenarios
+ ValidExamples.DemonstrateAllValidScenarios();
+ Console.WriteLine();
+
+ // Note: Invalid examples (with RegexOptions.Compiled) are commented out
+ // because they would trigger ATC301 warnings
+ Console.WriteLine("Invalid examples are commented out in Scenarios/InvalidExamples.cs");
+ }
+}
\ No newline at end of file
diff --git a/sample/Atc.Analyzer.Sample.Atc301/Scenarios/InvalidExamples.cs b/sample/Atc.Analyzer.Sample.Atc301/Scenarios/InvalidExamples.cs
new file mode 100644
index 0000000..5a18c05
--- /dev/null
+++ b/sample/Atc.Analyzer.Sample.Atc301/Scenarios/InvalidExamples.cs
@@ -0,0 +1,38 @@
+namespace Atc.Analyzer.Sample.Atc301.Scenarios;
+
+///
+/// Invalid examples that would trigger ATC301 warnings.
+/// These are commented out to allow the sample project to build without warnings.
+///
+internal static partial class InvalidExamples
+{
+ // INVALID: GeneratedRegex with RegexOptions.Compiled flag
+ // The Compiled flag is redundant because GeneratedRegex already generates compiled code.
+ // ⚠️ ATC301: The RegexOptions.Compiled flag is redundant for [GeneratedRegex] attributes
+ //
+ // [GeneratedRegex(@"\d+", RegexOptions.Compiled)]
+ // private static partial Regex InvalidCompiledOnlyRegex();
+
+ // INVALID: GeneratedRegex with Compiled flag combined with other options
+ // ⚠️ ATC301: The RegexOptions.Compiled flag is redundant for [GeneratedRegex] attributes
+ //
+ // [GeneratedRegex(@"[a-z]+", RegexOptions.Compiled | RegexOptions.IgnoreCase)]
+ // private static partial Regex InvalidCompiledWithIgnoreCaseRegex();
+
+ // INVALID: GeneratedRegex with Compiled flag in named parameter
+ // ⚠️ ATC301: The RegexOptions.Compiled flag is redundant for [GeneratedRegex] attributes
+ //
+ // [GeneratedRegex(
+ // pattern: @"[$>]",
+ // options: RegexOptions.Compiled | RegexOptions.ExplicitCapture,
+ // matchTimeoutMilliseconds: 1000)]
+ // private static partial Regex InvalidNamedParameterRegex();
+
+ // INVALID: Multiple flags including Compiled
+ // ⚠️ ATC301: The RegexOptions.Compiled flag is redundant for [GeneratedRegex] attributes
+ //
+ // [GeneratedRegex(
+ // @"^\d+$",
+ // RegexOptions.Multiline | RegexOptions.Compiled | RegexOptions.IgnoreCase)]
+ // private static partial Regex InvalidMultipleFlagsRegex();
+}
\ No newline at end of file
diff --git a/sample/Atc.Analyzer.Sample.Atc301/Scenarios/ValidExamples.cs b/sample/Atc.Analyzer.Sample.Atc301/Scenarios/ValidExamples.cs
new file mode 100644
index 0000000..1e9200e
--- /dev/null
+++ b/sample/Atc.Analyzer.Sample.Atc301/Scenarios/ValidExamples.cs
@@ -0,0 +1,81 @@
+// ReSharper disable RedundantVerbatimStringPrefix
+namespace Atc.Analyzer.Sample.Atc301.Scenarios;
+
+#pragma warning disable CA1303, MA0009, MA0110, SYSLIB1045
+
+///
+/// Valid examples of [GeneratedRegex] usage without RegexOptions.Compiled flag.
+///
+internal static partial class ValidExamples
+{
+ ///
+ /// GeneratedRegex without any options parameter - VALID.
+ ///
+ [GeneratedRegex(@"\d+")]
+ private static partial Regex SimpleNumberRegex();
+
+ ///
+ /// GeneratedRegex with IgnoreCase option (no Compiled flag) - VALID.
+ ///
+ [GeneratedRegex(@"[a-z]+", RegexOptions.IgnoreCase)]
+ private static partial Regex CaseInsensitiveRegex();
+
+ ///
+ /// GeneratedRegex with multiple options (no Compiled flag) - VALID.
+ ///
+ [GeneratedRegex(@"^\d+$", RegexOptions.Multiline | RegexOptions.IgnoreCase)]
+ private static partial Regex MultipleOptionsRegex();
+
+ ///
+ /// GeneratedRegex with named parameters (no Compiled flag) - VALID.
+ ///
+ [GeneratedRegex(
+ pattern: @"[$>]",
+ options: RegexOptions.ExplicitCapture,
+ matchTimeoutMilliseconds: 1000)]
+ private static partial Regex UserOrFtpPromptRegex();
+
+ ///
+ /// GeneratedRegex with None option explicitly - VALID.
+ ///
+ [GeneratedRegex(@"\w+", RegexOptions.None)]
+ private static partial Regex NoneOptionRegex();
+
+ ///
+ /// Regular (non-generated) Regex with Compiled flag - VALID.
+ /// This is a normal Regex construction, not a GeneratedRegex attribute,
+ /// so ATC301 does not apply here.
+ ///
+ private static readonly Regex TraditionalCompiledRegex =
+ new(@"\d+", RegexOptions.Compiled);
+
+ public static void DemonstrateAllValidScenarios()
+ {
+ Console.WriteLine("Valid GeneratedRegex Examples:");
+ Console.WriteLine();
+
+ // Test SimpleNumberRegex
+ var numberMatch = SimpleNumberRegex().Match("abc123def");
+ Console.WriteLine($"SimpleNumberRegex matched: {numberMatch.Value}");
+
+ // Test CaseInsensitiveRegex
+ var caseMatch = CaseInsensitiveRegex().Match("ABC");
+ Console.WriteLine($"CaseInsensitiveRegex matched: {caseMatch.Success}");
+
+ // Test MultipleOptionsRegex
+ var multiMatch = MultipleOptionsRegex().Match("123");
+ Console.WriteLine($"MultipleOptionsRegex matched: {multiMatch.Success}");
+
+ // Test UserOrFtpPromptRegex
+ var promptMatch = UserOrFtpPromptRegex().Match("user$ ");
+ Console.WriteLine($"UserOrFtpPromptRegex matched: {promptMatch.Value}");
+
+ // Test NoneOptionRegex
+ var noneMatch = NoneOptionRegex().Match("test");
+ Console.WriteLine($"NoneOptionRegex matched: {noneMatch.Value}");
+
+ // Test TraditionalCompiledRegex
+ var traditionalMatch = TraditionalCompiledRegex.Match("456");
+ Console.WriteLine($"TraditionalCompiledRegex matched: {traditionalMatch.Value}");
+ }
+}
\ No newline at end of file
diff --git a/src/Atc.Analyzer/AnalyzerReleases.Unshipped.md b/src/Atc.Analyzer/AnalyzerReleases.Unshipped.md
index 9b238c2..80adc37 100644
--- a/src/Atc.Analyzer/AnalyzerReleases.Unshipped.md
+++ b/src/Atc.Analyzer/AnalyzerReleases.Unshipped.md
@@ -7,3 +7,4 @@ Rule ID | Category | Severity | Notes
--------|----------|----------|-------
ATC220 | Style | Warning | Use global usings for all namespaces (strict policy)
ATC221 | Style | Warning | Use global usings for common namespaces (lenient policy)
+ATC301 | Usage | Warning | Remove redundant RegexOptions.Compiled flag from [GeneratedRegex] attribute
diff --git a/src/Atc.Analyzer/Atc.Analyzer.csproj b/src/Atc.Analyzer/Atc.Analyzer.csproj
index edc03c8..058bb2c 100644
--- a/src/Atc.Analyzer/Atc.Analyzer.csproj
+++ b/src/Atc.Analyzer/Atc.Analyzer.csproj
@@ -32,8 +32,8 @@
-
-
+
+
diff --git a/src/Atc.Analyzer/Atc.Analyzer.sln b/src/Atc.Analyzer/Atc.Analyzer.sln
new file mode 100644
index 0000000..bbf21d0
--- /dev/null
+++ b/src/Atc.Analyzer/Atc.Analyzer.sln
@@ -0,0 +1,24 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.2.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atc.Analyzer", "Atc.Analyzer.csproj", "{A6AE4418-C740-D332-B6E6-093A872AC736}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {A6AE4418-C740-D332-B6E6-093A872AC736}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A6AE4418-C740-D332-B6E6-093A872AC736}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A6AE4418-C740-D332-B6E6-093A872AC736}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A6AE4418-C740-D332-B6E6-093A872AC736}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {77F2935A-8249-4C61-8097-8256C85468C2}
+ EndGlobalSection
+EndGlobal
diff --git a/src/Atc.Analyzer/RuleIdentifierConstants.cs b/src/Atc.Analyzer/RuleIdentifierConstants.cs
index fd59e05..addb10b 100644
--- a/src/Atc.Analyzer/RuleIdentifierConstants.cs
+++ b/src/Atc.Analyzer/RuleIdentifierConstants.cs
@@ -40,4 +40,15 @@ internal static class Style
///
internal const string GlobalUsingsCommon = "ATC221";
}
+
+ ///
+ /// Usage - RuleIdentifiers from ATC301 to RuleIdentifier ATC399.
+ ///
+ internal static class Usage
+ {
+ ///
+ /// Remove redundant RegexOptions.Compiled flag from [GeneratedRegex] attribute.
+ ///
+ internal const string GeneratedRegexCompiledFlag = "ATC301";
+ }
}
\ No newline at end of file
diff --git a/src/Atc.Analyzer/Rules/Usage/GeneratedRegexCompiledFlagAnalyzer.cs b/src/Atc.Analyzer/Rules/Usage/GeneratedRegexCompiledFlagAnalyzer.cs
new file mode 100644
index 0000000..d4fd7cc
--- /dev/null
+++ b/src/Atc.Analyzer/Rules/Usage/GeneratedRegexCompiledFlagAnalyzer.cs
@@ -0,0 +1,206 @@
+namespace Atc.Analyzer.Rules.Usage;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class GeneratedRegexCompiledFlagAnalyzer : DiagnosticAnalyzer
+{
+ private static readonly DiagnosticDescriptor Rule = new(
+ RuleIdentifierConstants.Usage.GeneratedRegexCompiledFlag,
+ title: "Remove redundant RegexOptions.Compiled flag from [GeneratedRegex] attribute",
+ messageFormat: "The RegexOptions.Compiled flag is redundant for [GeneratedRegex] attributes as they are already compiled at build time",
+ RuleCategoryConstants.Usage,
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: "The [GeneratedRegex] attribute generates compiled regex code at build time using source generators. Adding RegexOptions.Compiled to the options parameter is redundant and has no effect, as the generated regex is already compiled.",
+ helpLinkUri: RuleIdentifierHelper.GetHelpUri(RuleIdentifierConstants.Usage.GeneratedRegexCompiledFlag));
+
+ public override ImmutableArray SupportedDiagnostics => [Rule];
+
+ public override void Initialize(AnalysisContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
+ context.EnableConcurrentExecution();
+
+ context.RegisterSyntaxNodeAction(AnalyzeMethodDeclaration, SyntaxKind.MethodDeclaration);
+ }
+
+ [SuppressMessage("Design", "MA0051:Method is too long", Justification = "OK.")]
+ private static void AnalyzeMethodDeclaration(SyntaxNodeAnalysisContext context)
+ {
+ if (context.Node.SyntaxTree.IsAutoGeneratedFile())
+ {
+ return;
+ }
+
+ var methodDeclaration = (MethodDeclarationSyntax)context.Node;
+
+ // Find GeneratedRegex attribute
+ AttributeSyntax? generatedRegexAttribute = null;
+ foreach (var attributeList in methodDeclaration.AttributeLists)
+ {
+ foreach (var attribute in attributeList.Attributes)
+ {
+ var attributeName = attribute.Name.ToString();
+ if (!attributeName.Contains("GeneratedRegex"))
+ {
+ continue;
+ }
+
+ // Check if this is actually the GeneratedRegex attribute via semantic model
+ var symbolInfo = context.SemanticModel.GetSymbolInfo(attribute, context.CancellationToken);
+ if (symbolInfo.Symbol is IMethodSymbol methodSymbol &&
+ methodSymbol.ContainingType?.ToDisplayString() == "System.Text.RegularExpressions.GeneratedRegexAttribute")
+ {
+ generatedRegexAttribute = attribute;
+ break;
+ }
+ }
+
+ if (generatedRegexAttribute is not null)
+ {
+ break;
+ }
+ }
+
+ if (generatedRegexAttribute is null)
+ {
+ return;
+ }
+
+ var attributeSyntax = generatedRegexAttribute;
+
+ // Get the attribute arguments
+ if (attributeSyntax.ArgumentList is null)
+ {
+ return;
+ }
+
+ // Find the options parameter (second parameter or named parameter)
+ AttributeArgumentSyntax? optionsArgument = null;
+
+ // Check for named parameter "options"
+ foreach (var argument in attributeSyntax.ArgumentList.Arguments)
+ {
+ if (argument.NameEquals?.Name.Identifier.Text == "options")
+ {
+ optionsArgument = argument;
+ break;
+ }
+ }
+
+ // If not found as named parameter, check for second positional parameter
+ if (optionsArgument is null && attributeSyntax.ArgumentList.Arguments.Count >= 2)
+ {
+ optionsArgument = attributeSyntax.ArgumentList.Arguments[1];
+ }
+
+ if (optionsArgument is null)
+ {
+ return;
+ }
+
+ // Check if the options expression contains RegexOptions.Compiled
+ var containsCompiled = ContainsRegexOptionsCompiled(context, optionsArgument.Expression);
+ if (!containsCompiled.HasValue || !containsCompiled.Value)
+ {
+ return;
+ }
+
+ // Find the exact location of RegexOptions.Compiled in the expression
+ var compiledLocation = FindCompiledFlagLocation(context, optionsArgument.Expression);
+ if (compiledLocation is null)
+ {
+ // Fallback to the entire options argument
+ compiledLocation = optionsArgument.GetLocation();
+ }
+
+ var diagnostic = Diagnostic.Create(Rule, compiledLocation);
+ context.ReportDiagnostic(diagnostic);
+ }
+
+ private static bool? ContainsRegexOptionsCompiled(SyntaxNodeAnalysisContext context, ExpressionSyntax expression)
+ {
+ // Check if this is a simple member access: RegexOptions.Compiled
+ if (expression is MemberAccessExpressionSyntax memberAccess)
+ {
+ var symbolInfo = context.SemanticModel.GetSymbolInfo(memberAccess, context.CancellationToken);
+ if (symbolInfo.Symbol is IFieldSymbol fieldSymbol)
+ {
+ return fieldSymbol.ContainingType?.ToDisplayString() == "System.Text.RegularExpressions.RegexOptions"
+ && fieldSymbol.Name == "Compiled";
+ }
+ }
+
+ // Check if this is a binary expression with OR operator
+ if (expression is BinaryExpressionSyntax binaryExpression && binaryExpression.IsKind(SyntaxKind.BitwiseOrExpression))
+ {
+ var leftContains = ContainsRegexOptionsCompiled(context, binaryExpression.Left);
+ var rightContains = ContainsRegexOptionsCompiled(context, binaryExpression.Right);
+
+ return (leftContains.HasValue && leftContains.Value) || (rightContains.HasValue && rightContains.Value);
+ }
+
+ // Check if this is a cast expression
+ if (expression is CastExpressionSyntax castExpression)
+ {
+ return ContainsRegexOptionsCompiled(context, castExpression.Expression);
+ }
+
+ // Check if this is a parenthesized expression
+ if (expression is ParenthesizedExpressionSyntax parenthesizedExpression)
+ {
+ return ContainsRegexOptionsCompiled(context, parenthesizedExpression.Expression);
+ }
+
+ return false;
+ }
+
+ private static Location? FindCompiledFlagLocation(SyntaxNodeAnalysisContext context, ExpressionSyntax expression)
+ {
+ // Check if this is a simple member access: RegexOptions.Compiled
+ if (expression is MemberAccessExpressionSyntax memberAccess)
+ {
+ var symbolInfo = context.SemanticModel.GetSymbolInfo(memberAccess, context.CancellationToken);
+ if (symbolInfo.Symbol is IFieldSymbol fieldSymbol
+ && fieldSymbol.ContainingType?.ToDisplayString() == "System.Text.RegularExpressions.RegexOptions"
+ && fieldSymbol.Name == "Compiled")
+ {
+ return memberAccess.GetLocation();
+ }
+ }
+
+ // Check if this is a binary expression with OR operator
+ if (expression is BinaryExpressionSyntax binaryExpression && binaryExpression.IsKind(SyntaxKind.BitwiseOrExpression))
+ {
+ var leftLocation = FindCompiledFlagLocation(context, binaryExpression.Left);
+ if (leftLocation is not null)
+ {
+ return leftLocation;
+ }
+
+ var rightLocation = FindCompiledFlagLocation(context, binaryExpression.Right);
+ if (rightLocation is not null)
+ {
+ return rightLocation;
+ }
+ }
+
+ // Check if this is a cast expression
+ if (expression is CastExpressionSyntax castExpression)
+ {
+ return FindCompiledFlagLocation(context, castExpression.Expression);
+ }
+
+ // Check if this is a parenthesized expression
+ if (expression is ParenthesizedExpressionSyntax parenthesizedExpression)
+ {
+ return FindCompiledFlagLocation(context, parenthesizedExpression.Expression);
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/Atc.Analyzer/Rules/Usage/GeneratedRegexCompiledFlagCodeFixProvider.cs b/src/Atc.Analyzer/Rules/Usage/GeneratedRegexCompiledFlagCodeFixProvider.cs
new file mode 100644
index 0000000..af73e18
--- /dev/null
+++ b/src/Atc.Analyzer/Rules/Usage/GeneratedRegexCompiledFlagCodeFixProvider.cs
@@ -0,0 +1,235 @@
+namespace Atc.Analyzer.Rules.Usage;
+
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(GeneratedRegexCompiledFlagCodeFixProvider))]
+[Shared]
+public sealed class GeneratedRegexCompiledFlagCodeFixProvider : CodeFixProvider
+{
+ public override ImmutableArray FixableDiagnosticIds
+ => [RuleIdentifierConstants.Usage.GeneratedRegexCompiledFlag];
+
+ public override FixAllProvider GetFixAllProvider()
+ => WellKnownFixAllProviders.BatchFixer;
+
+ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+ if (root is null)
+ {
+ return;
+ }
+
+ var diagnostic = context.Diagnostics.First();
+ var diagnosticSpan = diagnostic.Location.SourceSpan;
+
+ // Find the attribute node
+ var node = root.FindNode(diagnosticSpan);
+
+ // Find the attribute containing this node
+ var attribute = node.FirstAncestorOrSelf();
+ if (attribute is null)
+ {
+ return;
+ }
+
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title: "Remove redundant RegexOptions.Compiled flag",
+ createChangedDocument: c => RemoveCompiledFlagAsync(context.Document, attribute, c),
+ equivalenceKey: nameof(GeneratedRegexCompiledFlagCodeFixProvider)),
+ context.Diagnostics);
+ }
+
+ [SuppressMessage("Design", "MA0051:Method is too long", Justification = "OK.")]
+ private static async Task RemoveCompiledFlagAsync(
+ Document document,
+ AttributeSyntax attribute,
+ CancellationToken cancellationToken)
+ {
+ var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
+ if (root is null)
+ {
+ return document;
+ }
+
+ if (attribute.ArgumentList is null)
+ {
+ return document;
+ }
+
+ // Find the options argument
+ AttributeArgumentSyntax? optionsArgument = null;
+ var optionsArgumentIndex = -1;
+
+ // Check for named parameter "options"
+ for (var i = 0; i < attribute.ArgumentList.Arguments.Count; i++)
+ {
+ var argument = attribute.ArgumentList.Arguments[i];
+ if (argument.NameEquals?.Name.Identifier.Text == "options")
+ {
+ optionsArgument = argument;
+ optionsArgumentIndex = i;
+ break;
+ }
+ }
+
+ // If not found as named parameter, check for second positional parameter
+ if (optionsArgument is null && attribute.ArgumentList.Arguments.Count >= 2)
+ {
+ optionsArgument = attribute.ArgumentList.Arguments[1];
+ optionsArgumentIndex = 1;
+ }
+
+ if (optionsArgument is null)
+ {
+ return document;
+ }
+
+ // Try to remove just the Compiled flag from the expression
+ var newExpression = RemoveCompiledFromExpression(optionsArgument.Expression);
+
+ AttributeSyntax newAttribute;
+
+ if (newExpression is null)
+ {
+ // The Compiled flag was the only option, remove the entire options argument
+ var newArguments = attribute.ArgumentList.Arguments.RemoveAt(optionsArgumentIndex);
+
+ // Clean up separators and whitespace
+ var newArgumentList = attribute.ArgumentList.WithArguments(newArguments);
+
+ // If we removed the last argument and there's trailing trivia, clean it up
+ if (newArguments.Count > 0 && optionsArgumentIndex == attribute.ArgumentList.Arguments.Count - 1)
+ {
+ var lastArgIndex = newArguments.Count - 1;
+ var lastArg = newArguments[lastArgIndex];
+ newArgumentList = newArgumentList.WithArguments(
+ newArguments.Replace(lastArg, lastArg.WithTrailingTrivia()));
+ }
+
+ newAttribute = attribute.WithArgumentList(newArgumentList);
+ }
+ else
+ {
+ // Replace the options argument with the modified expression
+ var newOptionsArgument = optionsArgument.WithExpression(newExpression);
+ var newArguments = attribute.ArgumentList.Arguments.Replace(optionsArgument, newOptionsArgument);
+ newAttribute = attribute.WithArgumentList(attribute.ArgumentList.WithArguments(newArguments));
+ }
+
+ var newRoot = root.ReplaceNode(attribute, newAttribute);
+ return document.WithSyntaxRoot(newRoot);
+ }
+
+ [SuppressMessage("Design", "MA0051:Method is too long", Justification = "OK.")]
+ private static ExpressionSyntax? RemoveCompiledFromExpression(ExpressionSyntax expression)
+ {
+ // If this is just RegexOptions.Compiled by itself, return null to indicate removal
+ if (IsCompiledFlag(expression))
+ {
+ return null;
+ }
+
+ // If this is a binary expression with OR operator
+ if (expression is BinaryExpressionSyntax binaryExpression && binaryExpression.IsKind(SyntaxKind.BitwiseOrExpression))
+ {
+ var leftIsCompiled = IsCompiledFlag(binaryExpression.Left);
+ var rightIsCompiled = IsCompiledFlag(binaryExpression.Right);
+
+ if (leftIsCompiled)
+ {
+ // Remove left side and the OR operator, return just the right side
+ // Preserve any leading trivia from the binary expression
+ return binaryExpression.Right.WithLeadingTrivia(binaryExpression.GetLeadingTrivia());
+ }
+
+ if (rightIsCompiled)
+ {
+ // Remove right side and the OR operator, return just the left side
+ // Preserve any trailing trivia from the binary expression
+ return binaryExpression.Left.WithTrailingTrivia(binaryExpression.GetTrailingTrivia());
+ }
+
+ // Check if Compiled is nested deeper in the left or right side
+ var newLeft = RemoveCompiledFromExpression(binaryExpression.Left);
+ if (newLeft is not null && newLeft != binaryExpression.Left)
+ {
+ return binaryExpression.WithLeft(newLeft);
+ }
+
+ var newRight = RemoveCompiledFromExpression(binaryExpression.Right);
+ if (newRight is not null && newRight != binaryExpression.Right)
+ {
+ return binaryExpression.WithRight(newRight);
+ }
+
+ // If the left side was removed (null), return just the right
+ if (newLeft is null)
+ {
+ return binaryExpression.Right.WithLeadingTrivia(binaryExpression.GetLeadingTrivia());
+ }
+
+ // If the right side was removed (null), return just the left
+ if (newRight is null)
+ {
+ return binaryExpression.Left.WithTrailingTrivia(binaryExpression.GetTrailingTrivia());
+ }
+ }
+
+ // If this is a parenthesized expression
+ if (expression is ParenthesizedExpressionSyntax parenthesizedExpression)
+ {
+ var newInner = RemoveCompiledFromExpression(parenthesizedExpression.Expression);
+ if (newInner is null)
+ {
+ return null;
+ }
+
+ if (newInner != parenthesizedExpression.Expression)
+ {
+ // If the inner expression changed, update it
+ // If the inner is no longer a binary expression, we might want to remove the parentheses
+ if (newInner is not BinaryExpressionSyntax)
+ {
+ return newInner.WithTriviaFrom(parenthesizedExpression);
+ }
+
+ return parenthesizedExpression.WithExpression(newInner);
+ }
+ }
+
+ // If this is a cast expression
+ if (expression is CastExpressionSyntax castExpression)
+ {
+ var newInner = RemoveCompiledFromExpression(castExpression.Expression);
+ if (newInner is null)
+ {
+ return null;
+ }
+
+ if (newInner != castExpression.Expression)
+ {
+ return castExpression.WithExpression(newInner);
+ }
+ }
+
+ return expression;
+ }
+
+ private static bool IsCompiledFlag(ExpressionSyntax expression)
+ {
+ // Strip parentheses
+ while (expression is ParenthesizedExpressionSyntax parenthesized)
+ {
+ expression = parenthesized.Expression;
+ }
+
+ // Check if this is RegexOptions.Compiled
+ if (expression is MemberAccessExpressionSyntax memberAccess)
+ {
+ return memberAccess.Name.Identifier.Text == "Compiled"
+ && memberAccess.Expression is IdentifierNameSyntax { Identifier.Text: "RegexOptions" };
+ }
+
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/test/Atc.Analyzer.Tests/Atc.Analyzer.Tests.csproj b/test/Atc.Analyzer.Tests/Atc.Analyzer.Tests.csproj
index 6f07521..4133725 100644
--- a/test/Atc.Analyzer.Tests/Atc.Analyzer.Tests.csproj
+++ b/test/Atc.Analyzer.Tests/Atc.Analyzer.Tests.csproj
@@ -18,7 +18,7 @@
-
+
diff --git a/test/Atc.Analyzer.Tests/GlobalUsings.cs b/test/Atc.Analyzer.Tests/GlobalUsings.cs
index e0127f5..b24449b 100644
--- a/test/Atc.Analyzer.Tests/GlobalUsings.cs
+++ b/test/Atc.Analyzer.Tests/GlobalUsings.cs
@@ -2,6 +2,7 @@
global using Atc.Analyzer.Extensions;
global using Atc.Analyzer.Rules.Style;
+global using Atc.Analyzer.Rules.Usage;
global using Atc.Analyzer.Tests.Extensions;
global using Atc.Analyzer.Tests.Verifiers;
diff --git a/test/Atc.Analyzer.Tests/Rules/Usage/GeneratedRegexCompiledFlagAnalyzerNoDiagnosticTests.cs b/test/Atc.Analyzer.Tests/Rules/Usage/GeneratedRegexCompiledFlagAnalyzerNoDiagnosticTests.cs
new file mode 100644
index 0000000..b177712
--- /dev/null
+++ b/test/Atc.Analyzer.Tests/Rules/Usage/GeneratedRegexCompiledFlagAnalyzerNoDiagnosticTests.cs
@@ -0,0 +1,146 @@
+namespace Atc.Analyzer.Tests.Rules.Usage;
+
+#pragma warning disable SA1135 // Using directives must be qualified
+using AnalyzerVerifier = CSharpAnalyzerVerifier;
+#pragma warning restore SA1135 // Using directives must be qualified
+
+[SuppressMessage("", "AsyncFixer01:The method does not need to use async/await", Justification = "OK - Test code")]
+[SuppressMessage("Naming", "MA0048:File name must match type name", Justification = "OK - Partial class")]
+public sealed partial class GeneratedRegexCompiledFlagAnalyzerTests
+{
+ [Fact]
+ public async Task NoDiagnostic_GeneratedRegexWithoutOptions()
+ {
+ const string source = """
+ using System.Text.RegularExpressions;
+
+ public partial class Sample
+ {
+ [GeneratedRegex("\\d+")]
+ private static partial Regex NumberRegex();
+ }
+ """;
+
+ await AnalyzerVerifier.VerifyAnalyzerAsync(source);
+ }
+
+ [Fact]
+ public async Task NoDiagnostic_GeneratedRegexWithIgnoreCaseOption()
+ {
+ const string source = """
+ using System.Text.RegularExpressions;
+
+ public partial class Sample
+ {
+ [GeneratedRegex("\\d+", RegexOptions.IgnoreCase)]
+ private static partial Regex NumberRegex();
+ }
+ """;
+
+ await AnalyzerVerifier.VerifyAnalyzerAsync(source);
+ }
+
+ [Fact]
+ public async Task NoDiagnostic_GeneratedRegexWithMultilineOption()
+ {
+ const string source = """
+ using System.Text.RegularExpressions;
+
+ public partial class Sample
+ {
+ [GeneratedRegex("^\\d+$", RegexOptions.Multiline)]
+ private static partial Regex NumberRegex();
+ }
+ """;
+
+ await AnalyzerVerifier.VerifyAnalyzerAsync(source);
+ }
+
+ [Fact]
+ public async Task NoDiagnostic_GeneratedRegexWithMultipleOptionsWithoutCompiled()
+ {
+ const string source = """
+ using System.Text.RegularExpressions;
+
+ public partial class Sample
+ {
+ [GeneratedRegex("\\d+", RegexOptions.IgnoreCase | RegexOptions.Multiline)]
+ private static partial Regex NumberRegex();
+ }
+ """;
+
+ await AnalyzerVerifier.VerifyAnalyzerAsync(source);
+ }
+
+ [Fact]
+ public async Task NoDiagnostic_GeneratedRegexWithNamedOptionsParameter()
+ {
+ const string source = """
+ using System.Text.RegularExpressions;
+
+ public partial class Sample
+ {
+ [GeneratedRegex("\\d+", options: RegexOptions.IgnoreCase)]
+ private static partial Regex NumberRegex();
+ }
+ """;
+
+ await AnalyzerVerifier.VerifyAnalyzerAsync(source);
+ }
+
+ [Fact]
+ public async Task NoDiagnostic_GeneratedRegexWithTimeoutParameter()
+ {
+ const string source = """
+ using System.Text.RegularExpressions;
+
+ public partial class Sample
+ {
+ [GeneratedRegex("\\d+", RegexOptions.None, 1000)]
+ private static partial Regex NumberRegex();
+ }
+ """;
+
+ await AnalyzerVerifier.VerifyAnalyzerAsync(source);
+ }
+
+ [Fact]
+ public async Task NoDiagnostic_RegularRegexWithCompiled()
+ {
+ const string source = """
+ using System.Text.RegularExpressions;
+
+ public class Sample
+ {
+ private static readonly Regex NumberRegex = new Regex("\\d+", RegexOptions.Compiled);
+ }
+ """;
+
+ await AnalyzerVerifier.VerifyAnalyzerAsync(source);
+ }
+
+ [Fact]
+ public async Task NoDiagnostic_NonRegexAttribute()
+ {
+ const string source = """
+ using System;
+
+ public class SampleAttribute : Attribute
+ {
+ public SampleAttribute(string pattern, int options)
+ {
+ }
+ }
+
+ public class Test
+ {
+ [Sample("test", 1)]
+ public void Method()
+ {
+ }
+ }
+ """;
+
+ await AnalyzerVerifier.VerifyAnalyzerAsync(source);
+ }
+}
\ No newline at end of file
diff --git a/test/Atc.Analyzer.Tests/Rules/Usage/GeneratedRegexCompiledFlagAnalyzerReportsDiagnosticTests.cs b/test/Atc.Analyzer.Tests/Rules/Usage/GeneratedRegexCompiledFlagAnalyzerReportsDiagnosticTests.cs
new file mode 100644
index 0000000..522668d
--- /dev/null
+++ b/test/Atc.Analyzer.Tests/Rules/Usage/GeneratedRegexCompiledFlagAnalyzerReportsDiagnosticTests.cs
@@ -0,0 +1,106 @@
+namespace Atc.Analyzer.Tests.Rules.Usage;
+
+#pragma warning disable SA1135 // Using directives must be qualified
+using AnalyzerVerifier = CSharpAnalyzerVerifier;
+#pragma warning restore SA1135 // Using directives must be qualified
+
+[SuppressMessage("", "AsyncFixer01:The method does not need to use async/await", Justification = "OK - Test code")]
+[SuppressMessage("Naming", "MA0048:File name must match type name", Justification = "OK - Partial class")]
+public sealed partial class GeneratedRegexCompiledFlagAnalyzerTests
+{
+ [Fact]
+ public async Task ReportsDiagnostic_GeneratedRegexWithCompiledOption()
+ {
+ const string source = """
+ using System.Text.RegularExpressions;
+
+ public partial class Sample
+ {
+ [GeneratedRegex("\\d+", [|RegexOptions.Compiled|])]
+ private static partial Regex NumberRegex();
+ }
+ """;
+
+ await AnalyzerVerifier.VerifyAnalyzerAsync(source);
+ }
+
+ [Fact]
+ public async Task ReportsDiagnostic_GeneratedRegexWithCompiledAndIgnoreCase()
+ {
+ const string source = """
+ using System.Text.RegularExpressions;
+
+ public partial class Sample
+ {
+ [GeneratedRegex("\\d+", [|RegexOptions.Compiled|] | RegexOptions.IgnoreCase)]
+ private static partial Regex NumberRegex();
+ }
+ """;
+
+ await AnalyzerVerifier.VerifyAnalyzerAsync(source);
+ }
+
+ [Fact]
+ public async Task ReportsDiagnostic_GeneratedRegexWithIgnoreCaseAndCompiled()
+ {
+ const string source = """
+ using System.Text.RegularExpressions;
+
+ public partial class Sample
+ {
+ [GeneratedRegex("\\d+", RegexOptions.IgnoreCase | [|RegexOptions.Compiled|])]
+ private static partial Regex NumberRegex();
+ }
+ """;
+
+ await AnalyzerVerifier.VerifyAnalyzerAsync(source);
+ }
+
+ [Fact]
+ public async Task ReportsDiagnostic_GeneratedRegexWithMultipleOptionsIncludingCompiled()
+ {
+ const string source = """
+ using System.Text.RegularExpressions;
+
+ public partial class Sample
+ {
+ [GeneratedRegex("\\d+", RegexOptions.IgnoreCase | [|RegexOptions.Compiled|] | RegexOptions.Multiline)]
+ private static partial Regex NumberRegex();
+ }
+ """;
+
+ await AnalyzerVerifier.VerifyAnalyzerAsync(source);
+ }
+
+ [Fact]
+ public async Task ReportsDiagnostic_GeneratedRegexWithNamedOptionsParameter()
+ {
+ const string source = """
+ using System.Text.RegularExpressions;
+
+ public partial class Sample
+ {
+ [GeneratedRegex("\\d+", options: [|RegexOptions.Compiled|])]
+ private static partial Regex NumberRegex();
+ }
+ """;
+
+ await AnalyzerVerifier.VerifyAnalyzerAsync(source);
+ }
+
+ [Fact]
+ public async Task ReportsDiagnostic_GeneratedRegexWithNamedOptionsParameterAndMultipleFlags()
+ {
+ const string source = """
+ using System.Text.RegularExpressions;
+
+ public partial class Sample
+ {
+ [GeneratedRegex("\\d+", options: [|RegexOptions.Compiled|] | RegexOptions.IgnoreCase)]
+ private static partial Regex NumberRegex();
+ }
+ """;
+
+ await AnalyzerVerifier.VerifyAnalyzerAsync(source);
+ }
+}
\ No newline at end of file
diff --git a/test/Atc.Analyzer.Tests/Rules/Usage/GeneratedRegexCompiledFlagCodeFixProviderTests.cs b/test/Atc.Analyzer.Tests/Rules/Usage/GeneratedRegexCompiledFlagCodeFixProviderTests.cs
new file mode 100644
index 0000000..c509f3e
--- /dev/null
+++ b/test/Atc.Analyzer.Tests/Rules/Usage/GeneratedRegexCompiledFlagCodeFixProviderTests.cs
@@ -0,0 +1,253 @@
+namespace Atc.Analyzer.Tests.Rules.Usage;
+
+#pragma warning disable SA1135 // Using directives must be qualified
+using CodeFixVerifier = CSharpCodeFixVerifier;
+#pragma warning restore SA1135 // Using directives must be qualified
+
+[SuppressMessage("", "AsyncFixer01:The method does not need to use async/await", Justification = "OK - Test code")]
+public sealed class GeneratedRegexCompiledFlagCodeFixProviderTests
+{
+ [Fact]
+ public async Task FixCompiledOption_WhenOnlyOption()
+ {
+ const string source = """
+ using System.Text.RegularExpressions;
+
+ public partial class Sample
+ {
+ [GeneratedRegex("\\d+", RegexOptions.Compiled)]
+ private static partial Regex NumberRegex();
+ }
+ """;
+
+ const string fixedSource = """
+ using System.Text.RegularExpressions;
+
+ public partial class Sample
+ {
+ [GeneratedRegex("\\d+")]
+ private static partial Regex NumberRegex();
+ }
+ """;
+
+ var expected = new[]
+ {
+ new DiagnosticResult(
+ "ATC301",
+ DiagnosticSeverity.Warning)
+ .WithSpan(5, 29, 5, 50),
+ };
+
+ await CodeFixVerifier.VerifyCodeFixAsync(source, expected, fixedSource);
+ }
+
+ [Fact]
+ public async Task FixCompiledOption_WhenCompiledIsFirstInCombination()
+ {
+ const string source = """
+ using System.Text.RegularExpressions;
+
+ public partial class Sample
+ {
+ [GeneratedRegex("\\d+", RegexOptions.Compiled | RegexOptions.IgnoreCase)]
+ private static partial Regex NumberRegex();
+ }
+ """;
+
+ const string fixedSource = """
+ using System.Text.RegularExpressions;
+
+ public partial class Sample
+ {
+ [GeneratedRegex("\\d+", RegexOptions.IgnoreCase)]
+ private static partial Regex NumberRegex();
+ }
+ """;
+
+ var expected = new[]
+ {
+ new DiagnosticResult(
+ "ATC301",
+ DiagnosticSeverity.Warning)
+ .WithSpan(5, 29, 5, 50),
+ };
+
+ await CodeFixVerifier.VerifyCodeFixAsync(source, expected, fixedSource);
+ }
+
+ [Fact]
+ public async Task FixCompiledOption_WhenCompiledIsLastInCombination()
+ {
+ const string source = """
+ using System.Text.RegularExpressions;
+
+ public partial class Sample
+ {
+ [GeneratedRegex("\\d+", RegexOptions.IgnoreCase | RegexOptions.Compiled)]
+ private static partial Regex NumberRegex();
+ }
+ """;
+
+ const string fixedSource = """
+ using System.Text.RegularExpressions;
+
+ public partial class Sample
+ {
+ [GeneratedRegex("\\d+", RegexOptions.IgnoreCase)]
+ private static partial Regex NumberRegex();
+ }
+ """;
+
+ var expected = new[]
+ {
+ new DiagnosticResult(
+ "ATC301",
+ DiagnosticSeverity.Warning)
+ .WithSpan(5, 55, 5, 76),
+ };
+
+ await CodeFixVerifier.VerifyCodeFixAsync(source, expected, fixedSource);
+ }
+
+ [Fact]
+ public async Task FixCompiledOption_WhenCompiledIsInMiddleOfCombination()
+ {
+ const string source = """
+ using System.Text.RegularExpressions;
+
+ public partial class Sample
+ {
+ [GeneratedRegex("\\d+", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Multiline)]
+ private static partial Regex NumberRegex();
+ }
+ """;
+
+ const string fixedSource = """
+ using System.Text.RegularExpressions;
+
+ public partial class Sample
+ {
+ [GeneratedRegex("\\d+", RegexOptions.IgnoreCase | RegexOptions.Multiline)]
+ private static partial Regex NumberRegex();
+ }
+ """;
+
+ var expected = new[]
+ {
+ new DiagnosticResult(
+ "ATC301",
+ DiagnosticSeverity.Warning)
+ .WithSpan(5, 55, 5, 76),
+ };
+
+ await CodeFixVerifier.VerifyCodeFixAsync(source, expected, fixedSource);
+ }
+
+ [Fact]
+ public async Task FixCompiledOption_WithNamedParameter()
+ {
+ const string source = """
+ using System.Text.RegularExpressions;
+
+ public partial class Sample
+ {
+ [GeneratedRegex("\\d+", options: RegexOptions.Compiled)]
+ private static partial Regex NumberRegex();
+ }
+ """;
+
+ const string fixedSource = """
+ using System.Text.RegularExpressions;
+
+ public partial class Sample
+ {
+ [GeneratedRegex("\\d+")]
+ private static partial Regex NumberRegex();
+ }
+ """;
+
+ var expected = new[]
+ {
+ new DiagnosticResult(
+ "ATC301",
+ DiagnosticSeverity.Warning)
+ .WithSpan(5, 38, 5, 59),
+ };
+
+ await CodeFixVerifier.VerifyCodeFixAsync(source, expected, fixedSource);
+ }
+
+ [Fact]
+ public async Task FixCompiledOption_WithNamedParameterAndMultipleOptions()
+ {
+ const string source = """
+ using System.Text.RegularExpressions;
+
+ public partial class Sample
+ {
+ [GeneratedRegex("\\d+", options: RegexOptions.Compiled | RegexOptions.IgnoreCase)]
+ private static partial Regex NumberRegex();
+ }
+ """;
+
+ const string fixedSource = """
+ using System.Text.RegularExpressions;
+
+ public partial class Sample
+ {
+ [GeneratedRegex("\\d+", options: RegexOptions.IgnoreCase)]
+ private static partial Regex NumberRegex();
+ }
+ """;
+
+ var expected = new[]
+ {
+ new DiagnosticResult(
+ "ATC301",
+ DiagnosticSeverity.Warning)
+ .WithSpan(5, 38, 5, 59),
+ };
+
+ await CodeFixVerifier.VerifyCodeFixAsync(source, expected, fixedSource);
+ }
+
+ [Fact]
+ public async Task FixCompiledOption_WithMultilineAttribute()
+ {
+ const string source = """
+ using System.Text.RegularExpressions;
+
+ public partial class Sample
+ {
+ [GeneratedRegex(
+ pattern: "\\d+",
+ options: RegexOptions.Compiled | RegexOptions.IgnoreCase,
+ matchTimeoutMilliseconds: 1000)]
+ private static partial Regex NumberRegex();
+ }
+ """;
+
+ const string fixedSource = """
+ using System.Text.RegularExpressions;
+
+ public partial class Sample
+ {
+ [GeneratedRegex(
+ pattern: "\\d+",
+ options: RegexOptions.IgnoreCase,
+ matchTimeoutMilliseconds: 1000)]
+ private static partial Regex NumberRegex();
+ }
+ """;
+
+ var expected = new[]
+ {
+ new DiagnosticResult(
+ "ATC301",
+ DiagnosticSeverity.Warning)
+ .WithSpan(7, 18, 7, 39),
+ };
+
+ await CodeFixVerifier.VerifyCodeFixAsync(source, expected, fixedSource);
+ }
+}
\ No newline at end of file