Code Fix Providers: Turning Analyser Warnings into One-Click Fixes
An analyser that only reports problems is useful. An analyser that also fixes them is invaluable. Code fix providers are the mechanism Roslyn uses to offer those lightbulb suggestions in the IDE — the ones that rewrite your code with a single click.
How Code Fixes Work
When an analyser reports a diagnostic, Visual Studio and other Roslyn-aware editors check whether any registered CodeFixProvider handles that diagnostic ID. If one does, it gets a chance to propose one or more code actions. The user sees these as lightbulb or screwdriver suggestions.
The flow is:
- Analyser reports diagnostic
XX001at a location. - IDE asks all code fix providers: "Can you fix
XX001?" - Your provider computes a replacement syntax tree.
- The IDE applies the replacement when the user clicks the fix.
A Practical Example
Let us build a code fix for an analyser that detects string.Empty usage and suggests replacing it with "". First, assume we have an analyser that reports diagnostic SE001 on any MemberAccessExpressionSyntax that resolves to string.Empty.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Immutable;
using System.Composition;
[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
public class UseEmptyStringLiteralFix : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds
=> ImmutableArray.Create("SE001");
public override FixAllProvider? GetFixAllProvider()
=> WellKnownFixAllProviders.BatchFixer;
Two things to note. The ExportCodeFixProvider attribute registers this provider with MEF, which is how the IDE discovers it. The GetFixAllProvider method returns a BatchFixer, which allows users to apply the fix to an entire document, project, or solution in one go.
Registering the Code Action
public override async Task RegisterCodeFixesAsync(
CodeFixContext context)
{
var root = await context.Document
.GetSyntaxRootAsync(context.CancellationToken);
if (root is null) return;
var diagnostic = context.Diagnostics[0];
var span = diagnostic.Location.SourceSpan;
var node = root.FindNode(span);
context.RegisterCodeFix(
CodeAction.Create(
title: "Use \"\" instead of string.Empty",
createChangedDocument: ct =>
ReplaceWithEmptyStringLiteral(
context.Document, node, ct),
equivalenceKey: "SE001-fix"),
diagnostic);
}
The equivalenceKey is used by the Fix All provider to group equivalent fixes. If two diagnostics have the same equivalence key, the Fix All provider knows it can apply the same transformation to both.
Rewriting the Syntax Tree
The actual fix creates a new syntax tree with the offending node replaced:
private async Task<Document> ReplaceWithEmptyStringLiteral(
Document document, SyntaxNode node, CancellationToken ct)
{
var root = await document.GetSyntaxRootAsync(ct);
if (root is null) return document;
var emptyStringLiteral = SyntaxFactory.LiteralExpression(
SyntaxKind.StringLiteralExpression,
SyntaxFactory.Literal(""));
// Preserve the original trivia (whitespace, comments)
var newNode = emptyStringLiteral
.WithLeadingTrivia(node.GetLeadingTrivia())
.WithTrailingTrivia(node.GetTrailingTrivia());
var newRoot = root.ReplaceNode(node, newNode);
return document.WithSyntaxRoot(newRoot);
}
}
Preserving Trivia
The WithLeadingTrivia and WithTrailingTrivia calls are critical. Without them, the fix would strip whitespace and comments from around the replaced node, producing code that compiles but looks mangled. Always transfer trivia from the old node to the new one.
Testing a Code Fix Provider
The Microsoft.CodeAnalysis.CSharp.CodeFix.Testing package makes it straightforward to verify your fix works correctly:
using Microsoft.CodeAnalysis.Testing;
using Verify = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixVerifier<
StringEmptyAnalyser,
UseEmptyStringLiteralFix,
DefaultVerifier>;
[Fact]
public async Task StringEmpty_Is_Replaced_With_EmptyLiteral()
{
var test = """
class C
{
string s = {|SE001:string.Empty|};
}
""";
var fix = """
class C
{
string s = "";
}
""";
await Verify.VerifyCodeFixAsync(test, fix);
}
The {|SE001:...||} syntax marks where the diagnostic is expected. The verifier confirms that the analyser fires at that location and that applying the fix produces the expected output.
Fix All Support
The BatchFixer we returned from GetFixAllProvider handles the "Fix all in Document/Project/Solution" scenarios. It works by applying each individual fix sequentially. For most code fixes this is sufficient. If your fix has complex interactions (where fixing one instance changes the tree structure such that another fix's span is invalidated), you may need to implement a custom FixAllProvider.
Key Points
- Code fix providers are discovered via MEF — use
[ExportCodeFixProvider]. - Always preserve trivia when replacing nodes.
- Return
WellKnownFixAllProviders.BatchFixerunless you have a reason not to. - Use the testing framework to verify both the diagnostic and the fix output.
- The
equivalenceKeygroups related fixes for batch application.