Claude
Skills
Sign in
Back

dotnet-roslyn-analyzers

Included with Lifetime
$97 forever

Authoring Roslyn analyzers. DiagnosticAnalyzer, CodeFixProvider, CodeRefactoring, multi-version.

General

What this skill does


# dotnet-roslyn-analyzers

Guidance for **authoring** custom Roslyn analyzers, code fix providers, code refactoring providers, and diagnostic suppressors. Covers project setup, DiagnosticDescriptor conventions, analysis context registration, code fix actions, code refactoring actions, multi-Roslyn-version targeting (3.8 through 4.14), testing with Microsoft.CodeAnalysis.Testing, NuGet packaging, and performance best practices.

**Scope boundary:** This skill covers *writing* analyzers. For *consuming and configuring* existing analyzers (CA rules, EditorConfig severity, third-party packages), see [skill:dotnet-add-analyzers]. For *authoring source generators* (IIncrementalGenerator, syntax providers, code emission), see [skill:dotnet-csharp-source-generators]. Analyzers and source generators share the same NuGet packaging layout (`analyzers/dotnet/cs/`) and `Microsoft.CodeAnalysis.CSharp` dependency, but serve different purposes: analyzers report diagnostics, generators emit code.

Cross-references: [skill:dotnet-csharp-source-generators] for shared Roslyn packaging concepts and IIncrementalGenerator patterns, [skill:dotnet-add-analyzers] for consuming and configuring analyzers, [skill:dotnet-testing-strategy] for general test organization and framework selection, [skill:dotnet-csharp-coding-standards] for naming conventions applied to analyzer code.

---

## Project Setup

Analyzer projects **must** target `netstandard2.0`. The compiler loads analyzers into various host processes (Visual Studio on .NET Framework/Mono, MSBuild on .NET Core, `dotnet build` CLI) -- targeting `net8.0+` breaks compatibility with hosts that do not run on that runtime.

```xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
    <IsRoslynComponent>true</IsRoslynComponent>
    <LangVersion>latest</LangVersion>
  </PropertyGroup>

  <ItemGroup>
    <!-- NuGet: Microsoft.CodeAnalysis.CSharp -->
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" PrivateAssets="all" />
    <!-- NuGet: Microsoft.CodeAnalysis.Analyzers (meta-diagnostics for analyzer authors) -->
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" PrivateAssets="all" />
  </ItemGroup>
</Project>
```

- `EnforceExtendedAnalyzerRules` enables RS-series meta-diagnostics that catch common analyzer authoring mistakes (see Meta-Diagnostics section below).
- `IsRoslynComponent` enables IDE tooling support for the project.
- `LangVersion>latest` lets you write modern C# in the analyzer itself while still targeting `netstandard2.0`.
- All Roslyn SDK packages must use `PrivateAssets="all"` to avoid shipping them as transitive dependencies.

---

## DiagnosticAnalyzer

Every analyzer inherits from `DiagnosticAnalyzer` and must be decorated with `[DiagnosticAnalyzer(LanguageNames.CSharp)]`.

```csharp
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class NoPublicFieldsAnalyzer : DiagnosticAnalyzer
{
    // Diagnostic ID uses project prefix + sequential number
    public const string DiagnosticId = "MYLIB001";

    private static readonly DiagnosticDescriptor Rule = new(
        id: DiagnosticId,
        title: "Public fields should be properties",
        messageFormat: "Field '{0}' is public; use a property instead",
        category: "Design",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true,
        helpLinkUri: $"https://example.com/docs/rules/{DiagnosticId}");

    // Return an ImmutableArray -- allocating a new array per call is RS1030-adjacent waste
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
        = ImmutableArray.Create(Rule);

    public override void Initialize(AnalysisContext context)
    {
        // Required: enable concurrent execution and generated code analysis config
        context.EnableConcurrentExecution();
        context.ConfigureGeneratedCodeAnalysis(
            GeneratedCodeAnalysisFlags.None);

        context.RegisterSymbolAction(AnalyzeField, SymbolKind.Field);
    }

    private static void AnalyzeField(SymbolAnalysisContext context)
    {
        var field = (IFieldSymbol)context.Symbol;

        if (field.DeclaredAccessibility == Accessibility.Public
            && !field.IsConst
            && !field.IsReadOnly)
        {
            var diagnostic = Diagnostic.Create(
                Rule,
                field.Locations[0],
                field.Name);

            context.ReportDiagnostic(diagnostic);
        }
    }
}
```

### Analysis Context Registration

Choose the most appropriate registration method for your analysis:

| Method | Granularity | Use When |
|--------|-------------|----------|
| `RegisterSyntaxNodeAction` | Individual syntax nodes | Pattern matching on specific syntax (e.g., `if` statements, method declarations) |
| `RegisterSymbolAction` | Declared symbols | Checking symbol-level properties (accessibility, type, attributes) |
| `RegisterOperationAction` | IL-level operations | Analyzing semantic operations (assignments, invocations) independent of syntax |
| `RegisterSyntaxTreeAction` | Entire syntax tree | File-level checks (e.g., missing headers, encoding) |
| `RegisterCompilationStartAction` | Compilation start | When you need to accumulate state across the compilation, then report at end |
| `RegisterCompilationAction` | Full compilation | One-shot analysis after all files are compiled |

```csharp
// RegisterSyntaxNodeAction -- analyze specific syntax nodes
context.RegisterSyntaxNodeAction(
    AnalyzeInvocation,
    SyntaxKind.InvocationExpression);

// RegisterOperationAction -- analyze semantic operations
context.RegisterOperationAction(
    AnalyzeAssignment,
    OperationKind.SimpleAssignment);

// RegisterCompilationStartAction -- accumulate state, then report at compilation end
context.RegisterCompilationStartAction(compilationContext =>
{
    // Resolve types once at compilation start
    var disposableType = compilationContext.Compilation
        .GetTypeByMetadataName("System.IDisposable");

    if (disposableType is null)
        return;

    compilationContext.RegisterSymbolAction(
        ctx => AnalyzeTypeDisposal(ctx, disposableType),
        SymbolKind.NamedType);
});
```

---

## DiagnosticDescriptor Conventions

Follow these conventions for all custom analyzers:

### ID Prefix Patterns

Use a short, unique prefix derived from your project or library name, followed by a sequential number:

| Pattern | Example | When |
|---------|---------|------|
| `PROJ###` | `MYLIB001` | Single-project analyzers |
| `AREA####` | `PERF0001` | Category-scoped analyzers (performance, security) |
| `XX####` | `MA0042` | Short-prefix convention (e.g., Meziantou.Analyzer) |

Avoid prefixes reserved by the .NET platform: `CA` (code analysis), `CS` (compiler), `RS` (Roslyn SDK), `IDE` (code style), `IL` (linker), `SYSLIB` (runtime). Include the namespace in the ID constant to prevent collisions when multiple analyzer packages are installed.

### Category Naming

Use standard .NET analysis categories where applicable:

`Design`, `Globalization`, `Interoperability`, `Maintainability`, `Naming`, `Performance`, `Reliability`, `Security`, `Style`, `Usage`

For domain-specific categories, use a clear, titlecase name (e.g., `EntityFramework`, `AspNetCore`).

### Severity Selection

| Severity | Use When |
|----------|----------|
| `Error` | Code will not work correctly at runtime (null deref, SQL injection, resource leak) |
| `Warning` | Code works but violates best practices or has performance issues |
| `Info` | Suggestion for improvement, not a defect |
| `Hidden` | IDE-only refactoring suggestion, not shown in buil

Related in General