Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Atc.Analyzer.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<Project Path="sample/Atc.Analyzer.Sample.Atc210/Atc.Analyzer.Sample.Atc210.csproj" />
<Project Path="sample/Atc.Analyzer.Sample.Atc220/Atc.Analyzer.Sample.Atc220.csproj" />
<Project Path="sample/Atc.Analyzer.Sample.Atc221/Atc.Analyzer.Sample.Atc221.csproj" />
<Project Path="sample/Atc.Analyzer.Sample.Atc301/Atc.Analyzer.Sample.Atc301.csproj" />
</Folder>
<Folder Name="/src/">
<Project Path="src/Atc.Analyzer/Atc.Analyzer.csproj" />
Expand Down
34 changes: 34 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
<ItemGroup Label="Code Analyzers">
<PackageReference Include="AsyncFixer" Version="1.6.0" PrivateAssets="All" />
<PackageReference Include="Asyncify" Version="0.9.7" PrivateAssets="All" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.253" PrivateAssets="All" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.254" PrivateAssets="All" />
<PackageReference Include="SecurityCodeScan.VS2019" Version="5.6.7" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.507" PrivateAssets="All" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.15.0.120848" PrivateAssets="All" />
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
Expand Down
247 changes: 247 additions & 0 deletions docs/rules/ATC301.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<IsTestProject>false</IsTestProject>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Atc.Analyzer\Atc.Analyzer.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<OutputItemType>Analyzer</OutputItemType>
</ProjectReference>
</ItemGroup>

</Project>
4 changes: 4 additions & 0 deletions sample/Atc.Analyzer.Sample.Atc301/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Global using directives

global using System.Text.RegularExpressions;
global using Atc.Analyzer.Sample.Atc301.Scenarios;
27 changes: 27 additions & 0 deletions sample/Atc.Analyzer.Sample.Atc301/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace Atc.Analyzer.Sample.Atc301;

#pragma warning disable CA1303

/// <summary>
/// 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
/// </summary>
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");
}
}
Loading