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:
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:
- id: A unique identifier (e.g.,
CS8602,CA1062). Convention is a short prefix followed by a number. - title: A brief description shown in the rule documentation.
- messageFormat: Supports
{0},{1}placeholders, filled in when the diagnostic is created. - category: Groups diagnostics in the Error List (e.g., "Design", "Performance", "Usage").
- defaultSeverity: One of
Hidden,Info,Warning, orError. - helpLinkUri: A URL the IDE can link to for documentation.
Reporting from an Analyser
In a DiagnosticAnalyzer, you report diagnostics through the analysis context:
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:
// 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:
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:
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:
[*.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:
[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:
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
- Use precise locations — highlight the specific token that is wrong.
- Choose severity carefully:
Errorfor broken code,Warningfor problems,Infofor suggestions. - Remember that generator diagnostics and analyser diagnostics behave differently with respect to severity configuration.
- Be careful with
Locationobjects in incremental generator pipelines — they can break caching. - Always set
isEnabledByDefault: trueunless you have a strong reason not to.