---
name: roslyn-analyzers
description: 'Build, review, debug, package, and test Roslyn diagnostic analyzers, code fix providers, and incremental source generators. Use for DiagnosticAnalyzer, CodeFixProvider, IIncrementalGenerator, IOperation analysis, Microsoft.CodeAnalysis dependency pinning, Roslyn test harnesses, C#/VB tests, and analyzer NuGet packaging.'
---

# Roslyn Analyzers and Source Generators

Use this workflow when adding or changing a Roslyn analyzer, code fix, source generator, tests, dependencies, or package layout. First inspect the repository's target frameworks, central package management, test framework, analyzer and generator conventions, diagnostic ID allocation, localization, and packaging. Preserve established conventions unless they conflict with the compatibility rules below.

## Non-negotiable design rules

- Prefer `IOperation`-based analysis wherever possible. Register the narrowest applicable `OperationKind` and inspect typed operations such as `IInvocationOperation`, `IAwaitOperation`, or `IObjectCreationOperation`. This usually supports C# and VB with one analyzer and gives direct access to symbols and conversions.
- Use syntax analysis only for inherently syntactic rules or syntax not represented adequately by `IOperation`. A syntax-node callback already has a `SemanticModel`; do not call `Compilation.GetSemanticModel` or fetch another semantic model from it. Repeated semantic-model creation is expensive and often indicates that an operation or symbol action is the better abstraction.
- Every analyzer must support C#. VB.NET support is optional until requested or established repository precedent requires it. A language-neutral operation analyzer may declare both languages. If VB support is promised, include equivalent C# and VB snippets in tests; do not infer VB correctness from shared implementation alone.
- Keep analyzer and code-fix providers in distinct assemblies. The analyzer project must never reference Roslyn Workspaces packages. Workspaces dependencies belong only in the code-fix and test projects.
- Analyzer callbacks must be stateless or concurrency-safe. Call `EnableConcurrentExecution()`. Make an explicit generated-code choice with `ConfigureGeneratedCodeAnalysis(...)`; follow repository policy rather than silently accepting the default.
- Respect cancellation where APIs expose a token. Do not retain compilations, operations, syntax trees, symbols, or semantic models in static state.
- Avoid `InternalsVisibleTo`. Test analyzers through their public `DiagnosticAnalyzer` and `CodeFixProvider` APIs and the Roslyn test harness. A small analyzer helper may be public when direct testing is genuinely useful, but most behavior should be tested end to end.
- Centralize metadata names and member names instead of propagating magic strings. Use one shared static catalog for fully qualified type names, namespaces, and API member names.
- Document every diagnostic ID. When the repository uses Docfx, put analyzer documentation under its Docfx tree, typically `docfx/analyzers`, and include each page in the relevant table of contents.
- Set every analyzer assembly's version precisely enough that each commit produces a unique assembly version. When using Nerdbank.GitVersioning, give each analyzer project its own `version.json` with `assemblyVersion.precision` set to `revision` and ensure that repository-wide MSBuild properties do not prevent that file from being discovered.
- Source generators must implement `IIncrementalGenerator`, not `ISourceGenerator`. Design the provider graph so unchanged inputs remain cached and do not regenerate output.
- Source generators must use a small `SourceWriter` abstraction for deterministic newlines, indentation, encoding, and balanced output. Start from [SourceWriter.cs](./references/SourceWriter.cs) and tailor its namespace and target-framework details to the receiving repository.

## Implementation workflow

1. Write down examples that must report and near-misses that must not report. Decide the exact diagnostic span and message arguments before implementation.
2. Determine whether the rule is semantic. Prefer, in order, operation actions, operation-block actions, symbol actions, compilation-start actions that register one of those actions, and finally syntax actions.
3. Resolve well-known types once in a compilation-start action when needed. If a required type is absent, register no inner action. Compare symbols with `SymbolEqualityComparer.Default`, metadata names, arity, containing type, and signature as appropriate; do not identify APIs by simple method name alone.
4. Define a stable diagnostic ID and descriptor. Follow repository practices for category, severity, localization, help links, telemetry tags, and release tracking.
5. Add or update the diagnostic's documentation page and wire the descriptor's `HelpLinkUri` to its published URL.
6. Report the smallest useful source location. Avoid diagnostics on generated code unless that is intentional.
7. Add or update the code fix in the separate code-fix assembly. Preserve trivia, use syntax generators or typed syntax APIs, annotate simplifiable/formattable nodes where appropriate, provide stable equivalence keys for distinct actions, and offer `FixAllProvider` only when batch application is correct.
8. Add positive, negative, edge, and code-fix tests in one test class for the rule.
9. Verify that every analyzer project has per-commit assembly versioning and that its project-local version configuration is actually honored.
10. Run the narrow test class or method first, then the test project, then the repository's normal build or analyzer validation. Honor the repository's test-runner syntax; do not assume VSTest `--filter` is supported.

For a source generator, replace steps 1-8 with: define generated API examples and invalid-input diagnostics; model each independent input; build an incremental provider graph; render with `SourceWriter`; and test output, diagnostics, determinism, and incremental caching before continuing with versioning, packaging, and repository validation.

## Analyzer pattern

This is the preferred shape for a semantic rule. Adapt descriptor construction and generated-code policy to local conventions.

```csharp
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class AvoidBlockingAnalyzer : DiagnosticAnalyzer
{
    public const string DiagnosticId = "LIB0001";

    private static readonly DiagnosticDescriptor Rule = new(
        DiagnosticId,
        "Avoid blocking calls",
        "Do not call '{0}'",
        "Usage",
        DiagnosticSeverity.Warning,
        isEnabledByDefault: true);

    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [Rule];

    public override void Initialize(AnalysisContext context)
    {
        context.EnableConcurrentExecution();
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.RegisterCompilationStartAction(static context =>
        {
            INamedTypeSymbol? taskType = context.Compilation.GetTypeByMetadataName(KnownApis.Task.FullName);
            if (taskType is null)
            {
                return;
            }

            context.RegisterOperationAction(
                operationContext => AnalyzeInvocation(operationContext, taskType),
                OperationKind.Invocation);
        });
    }

    private static void AnalyzeInvocation(OperationAnalysisContext context, INamedTypeSymbol taskType)
    {
        var invocation = (IInvocationOperation)context.Operation;
        IMethodSymbol method = invocation.TargetMethod;
        if (method.Name == KnownApis.Task.Wait &&
            SymbolEqualityComparer.Default.Equals(method.ContainingType, taskType))
        {
            context.ReportDiagnostic(Diagnostic.Create(
                Rule,
                invocation.Syntax.GetLocation(),
                method.Name));
        }
    }
}

public static class KnownApis
{
    public static class Task
    {
        public const string FullName = "System.Threading.Tasks.Task";
        public const string Wait = "Wait";
    }
}
```

When supporting both languages, use `[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]` only if all analysis and diagnostic locations are actually language-neutral or both languages are separately handled and tested.

## Syntax-analysis exception

Use syntax callbacks only when syntax itself controls the rule. Use the callback's existing semantic model if semantic information is unavoidable:

```csharp
private static void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
    var invocation = (InvocationExpressionSyntax)context.Node;
    ISymbol? symbol = context.SemanticModel.GetSymbolInfo(invocation, context.CancellationToken).Symbol;
    // Do not call context.Compilation.GetSemanticModel(invocation.SyntaxTree).
}
```

Before choosing this pattern, check whether `IInvocationOperation.TargetMethod`, `IArgumentOperation.Parameter`, `IConversionOperation`, or another operation supplies the same information.

## Incremental source generators

Always implement `IIncrementalGenerator`. Do not wrap an `ISourceGenerator` with `AsSourceGenerator`; that preserves the old execution model rather than designing for incrementality.

Build a pipeline with these properties:

- Use `SyntaxProvider.ForAttributeWithMetadataName` for attribute-driven discovery. Otherwise, use `CreateSyntaxProvider` with a very cheap syntactic predicate and perform semantic work only in its transform.
- Transform syntax and symbols immediately into small immutable, value-equatable models containing only information needed for generation. Do not carry `SyntaxNode`, `ISymbol`, `SemanticModel`, or `Compilation` into later stages because their identity changes defeat caching.
- Keep independent inputs in independent providers. Combine providers as late as possible. Avoid combining every item with `CompilationProvider` merely for convenience.
- Avoid `Collect()` unless one output truly depends on the complete set. Prefer one output per model so editing one declaration invalidates only that output.
- Use stable equality. Records and immutable value types are good models; add `WithComparer` when normal equality does not represent semantic equivalence.
- Keep transforms and output callbacks static, pure, deterministic, and cancellation-aware. Do not read ambient files, clocks, environment state, or mutable static state. Model configuration, additional files, analyzer config options, and parse options as explicit providers.
- Give providers `WithTrackingName` labels while developing or testing incremental behavior.
- Generate stable, collision-resistant hint names. Sort any aggregated inputs explicitly before emitting output.
- Report invalid user input with generator diagnostics instead of throwing. Generator exceptions surface as compiler failures.
- Use `SourceText` with an explicit encoding when calling `AddSource`.

A compact pipeline should look like this:

```csharp
[Generator(LanguageNames.CSharp)]
public sealed class FactoryGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        IncrementalValuesProvider<TypeModel> models = context.SyntaxProvider
            .ForAttributeWithMetadataName(
                "Product.GenerateFactoryAttribute",
                static (node, _) => node is TypeDeclarationSyntax,
                static (attributeContext, cancellationToken) =>
                    TypeModel.Create(attributeContext, cancellationToken))
            .WithTrackingName("FactoryModels");

        context.RegisterSourceOutput(models, static (productionContext, model) =>
        {
            var writer = new SourceWriter();
            writer.WriteLine("// <auto-generated/>");
            writer.WriteLine($"namespace {model.Namespace};");
            writer.WriteLine();
            writer.WriteLine($"partial class {model.TypeName}");
            writer.WriteLine('{');
            writer.Indentation++;
            writer.WriteLine($"public static {model.TypeName} Create() => new();");
            writer.Indentation--;
            writer.WriteLine('}');
            productionContext.AddSource(model.HintName, writer.ToSourceText());
        });
    }
}
```

The model creation step must normalize symbol data into strings, booleans, enums, and other immutable values, validate unsupported declarations, escape identifiers, and derive a stable hint name. The output callback should only render the model. Use the complete [SourceWriter.cs](./references/SourceWriter.cs) example rather than ad hoc `StringBuilder` concatenation, then adapt indentation style and framework compatibility to the repository.

### Generator tests

Test source generators with `GeneratorDriver` or the repository's generator test harness. Cover:

- exact generated hint names and source text;
- multiple and nested declarations, namespaces, generics, escaped identifiers, and partial types;
- malformed or unsupported inputs and their diagnostic locations;
- additional files, analyzer config options, and parse options when consumed;
- deterministic output independent of input enumeration and operating-system newlines;
- a second driver run with identical inputs that caches all expected tracked steps;
- a narrowly changed input that reruns only the affected pipeline and output;
- compilation of generated output without unexpected diagnostics.

Enable incremental step tracking in tests and assert run reasons such as cached, unchanged, modified, or newly produced according to the Roslyn version in use. Do not settle for snapshot-only tests: identical generated text can hide a pipeline that recomputes everything.

## Diagnostic documentation and help links

Create one discoverable documentation page for every diagnostic code. A useful page includes:

- the diagnostic ID, title, category, and default severity;
- the exact condition that triggers the diagnostic;
- why the reported pattern is problematic;
- bad and corrected C# examples;
- VB examples when VB support is promised;
- exceptions, configuration, suppression guidance, and code-fix behavior when relevant.

If the repository has a Docfx site, place these pages under its existing analyzer documentation area, typically:

```text
docfx/
    analyzers/
        LIB0001.md
    toc.yml
```

Follow the site's existing URL and navigation conventions. Add each page to the applicable `toc.yml` so it is rendered and discoverable. The descriptor's help link must be the public GitHub Pages URL that Docfx will produce, not a source-tree path. Centralize construction when all diagnostics share a route:

```csharp
private static string GetHelpLink(string diagnosticId)
        => $"https://example.github.io/product/analyzers/{diagnosticId}.html";

private static readonly DiagnosticDescriptor Rule = new(
        id: DiagnosticId,
        title: "Avoid blocking calls",
        messageFormat: "Do not call '{0}'",
        category: "Usage",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true,
        helpLinkUri: GetHelpLink(DiagnosticId));
```

Derive the exact hostname, base path, output extension, casing, and route from the repository's Docfx configuration and deployment setup. Confirm that the generated site's route matches `HelpLinkUri`; do not merely assume that a Markdown path maps to the expected URL. When no Docfx folder exists, follow the repository's established documentation system or create an equivalent durable page and public help URL.

## Project and dependency boundaries

A typical solution has three projects:

```text
Product.Analyzers          -> Microsoft.CodeAnalysis.Common/CSharp only
Product.CodeFixes          -> Product.Analyzers + Roslyn Workspaces/Features APIs
Product.Analyzers.Tests    -> analyzer + code-fix projects + current testing packages
```

Target analyzer assemblies conservatively, commonly `netstandard2.0`, unless host requirements dictate otherwise. Treat the analyzer's Roslyn version as a host-compatibility floor, not a routine dependency to float.

Choose exactly one Roslyn reference-assembly version for shipping analyzer projects. Pin it independently from the latest Roslyn version used by code-fix tests or the rest of the repository. Pin the analyzer dependency's entire transitive closure to mutually compatible versions, including packages such as `System.Collections.Immutable`, `System.Memory`, `System.Reflection.Metadata`, `System.Runtime.CompilerServices.Unsafe`, and `System.Threading.Tasks.Extensions` when they appear in restore. Directly pinning only `Microsoft.CodeAnalysis.*` is insufficient: a newer centrally managed transitive dependency can make the analyzer fail in an older compiler host.

With central package management, create a dedicated `Directory.Packages.Analyzers.props` at the repository root and import it only for shipping analyzer, source-generator, and code-fix projects. Do not fold these overrides into the general `Directory.Packages.props`: the separate file makes the compatibility boundary visible and lets dependency automation treat it specially.

Use this three-part pattern.

First, have every shipping analyzer, source-generator, and code-fix project import one shared props file such as `src/AnalyzerCompatibility.props`. That file should identify the project:

```xml
<Project>
    <PropertyGroup>
        <!-- This is set so that Directory.Packages.Analyzers.props can apply version overrides specific to analyzer projects. -->
        <IsAnalyzerCompatibilityProject>true</IsAnalyzerCompatibilityProject>
    </PropertyGroup>
</Project>
```

Second, conditionally import the dedicated package file from a repository-wide props or targets file after central package versions are available:

```xml
<Import Project="$(MSBuildThisFileDirectory)Directory.Packages.Analyzers.props"
                Condition="'$(IsAnalyzerCompatibilityProject)' == 'true'" />
```

Third, copy this `Directory.Packages.Analyzers.props` structure and tailor the baseline comment, Roslyn version, and transitive versions to the oldest supported compiler or SDK host. Use `Update` only for a package already declared by the repository's central package file; use `Include` to add a pin for a package that is otherwise transitive-only. The example assumes the Roslyn packages are already declared and the listed runtime dependencies are new pins:

```xml
<Project>
    <!-- These versions are chosen to support the oldest compiler/SDK host named here. -->
  <PropertyGroup>
        <CodeAnalysisVersionForAnalyzers>4.11.0</CodeAnalysisVersionForAnalyzers>
      <!-- Apply analyzer compatibility pins to packages referenced only transitively. -->
      <CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
  </PropertyGroup>
  <ItemGroup>
        <PackageVersion Update="Microsoft.CodeAnalysis" Version="$(CodeAnalysisVersionForAnalyzers)" />
        <PackageVersion Update="Microsoft.CodeAnalysis.Common" Version="$(CodeAnalysisVersionForAnalyzers)" />
        <PackageVersion Update="Microsoft.CodeAnalysis.CSharp" Version="$(CodeAnalysisVersionForAnalyzers)" />
        <PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="$(CodeAnalysisVersionForAnalyzers)" />
        <PackageVersion Update="Microsoft.CodeAnalysis.VisualBasic" Version="$(CodeAnalysisVersionForAnalyzers)" />
        <PackageVersion Include="Microsoft.CodeAnalysis.VisualBasic.Workspaces" Version="$(CodeAnalysisVersionForAnalyzers)" />
        <PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
        <PackageVersion Include="System.Collections.Immutable" Version="8.0.0" />
        <PackageVersion Include="System.Memory" Version="4.5.5" />
        