Emitting Diagnostics from Analysers and Source Generators

Diagnostics are how Roslyn communicates with developers. Every compiler error, warning, and informational message is a diagnostic. When you write an analyser or a source generator, you use the same system to report issues to the developer — and the IDE renders them as squiggly underlines, Error List entries, and build output messages.

Anatomy of a Diagnostic

Every diagnostic is built from a DiagnosticDescriptor:

Example.cs
private static readonly DiagnosticDescriptor InvalidUsage = new(
    id: "MYG001",
    title: "Invalid attribute usage",
    messageFormat: "The type '{0}' must be partial to use [AutoGenerate]",
    category: "Usage",
    defaultSeverity: DiagnosticSeverity.Error,
    isEnabledByDefault: true,
    helpLinkUri: "https://example.com/docs/MYG001");

Each field serves a specific purpose:

Reporting from an Analyser

In a DiagnosticAnalyzer, you report diagnostics through the analysis context:

Example.cs
public override void Initialize(AnalysisContext context)
{
    context.ConfigureGeneratedCodeAnalysis(
        GeneratedCodeAnalysisFlags.None);
    context.EnableConcurrentExecution();

    context.RegisterSymbolAction(AnalyseType, SymbolKind.NamedType);
}

private void AnalyseType(SymbolAnalysisContext context)
{
    var type = (INamedTypeSymbol)context.Symbol;

    if (!HasRequiredAttribute(type))
        return;

    if (type.DeclaringSyntaxReferences.Length == 0)
        return;

    var syntax = type.DeclaringSyntaxReferences[0].GetSyntax();
    if (syntax is ClassDeclarationSyntax cls
        && !cls.Modifiers.Any(SyntaxKind.PartialKeyword))
    {
        context.ReportDiagnostic(Diagnostic.Create(
            InvalidUsage,
            cls.Identifier.GetLocation(),
            type.Name));
    }
}

Location Matters

The location you pass to Diagnostic.Create determines where the squiggly line appears. Be precise. Highlighting the entire class declaration when the problem is the missing partial keyword is unhelpful. Target the identifier or the specific token:

Example.cs
// Good — highlights just the class name
cls.Identifier.GetLocation()

// Bad — highlights the entire class body
cls.GetLocation()

Reporting from a Source Generator

Source generators also report diagnostics, but through a different context:

Example.cs
context.RegisterSourceOutput(pipeline, (ctx, model) =>
{
    if (!model.IsPartial)
    {
        ctx.ReportDiagnostic(Diagnostic.Create(
            InvalidUsage,
            model.Location,
            model.ClassName));
        return; // Don't generate code for invalid inputs
    }

    // ... generate code
});

There is an important constraint: in an incremental generator, you cannot pass Location objects through the pipeline directly because they hold references to syntax trees that break caching. Instead, capture the location data you need:

Example.cs
private record ClassModel(
    string ClassName,
    bool IsPartial,
    Location? Location);

In practice, use Location.None for pipeline-cached models and only attach real locations in the final RegisterSourceOutput stage if needed. Alternatively, capture the location in the transform step and accept that those pipeline stages will re-run more often.

Severity Levels

Choose the right severity for your diagnostic:

Severity Behaviour Use When
Error Fails the build The code is fundamentally broken and cannot proceed
Warning Shows warning, build succeeds Something is wrong but the code will work
Info Subtle indicator in IDE A suggestion for improvement
Hidden Not shown, but available for code fixes The diagnostic only exists to trigger a code fix

Allowing User Configuration

Users can change diagnostic severity via EditorConfig:

ini
[*.cs]
dotnet_diagnostic.MYG001.severity = warning

This works automatically — you do not need to write any code to support it. However, if your diagnostic is reported as Error from a source generator (not an analyser), it cannot be downgraded via EditorConfig. Only analyser diagnostics participate in severity configuration.

Supporting SuppressMessage

Users can suppress individual diagnostics with attributes:

Example.cs
[System.Diagnostics.CodeAnalysis.SuppressMessage(
    "Usage", "MYG001",
    Justification = "Intentionally non-partial")]
public class MyClass { }

This works for analyser diagnostics out of the box. You do not need to do anything special.

Localisation

For open-source or widely distributed analysers, consider localising your diagnostic messages:

Example.cs
private static readonly LocalizableString Title =
    new LocalizableResourceString(
        nameof(Resources.MYG001_Title),
        Resources.ResourceManager,
        typeof(Resources));

For internal analysers, hardcoded strings are perfectly acceptable.

Key Takeaways