Rolsyn Code Fix Providers to the Rescue

Introduction

Code Fix Providers are another tool in the Roslyn toolbox that we could leverage to make the life of our development team easier, and help them to focus on the actual code that needs to be implemented to satisfy the business requirements; this is actually the essence of a good developer experience! In this post we want to create a code fix provider for our previously created code analyzer , and we will take a different approach, we will first write some unit tests for it and then will create the code fix provider itself! Let's begin ๐Ÿ˜‰

TDD-ify our Code Fix Provider

You could clone the project from GitHub and we will start from Sample.Analyzer.Tests project; The first thing is to add a reference to Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.XUnit NuGet package:

<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing" Version="1.1.2" />

PS: In previous post and the YouTube video for Testing Code Analyzers we mentioned to use Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit, or in general Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.* package depending on the testing framework you are using, A friend of mine (Stefan Pรถlz) reminded me that they are deprecated and we are anyway using DefaultVerifier instead of XUnitVerifier due to the fact that the latter is obsolete, it is recommended to use Microsoft.CodeAnalysis.CSharp.CodeFix.Testing and Microsoft.CodeAnalysis.CSharp.Analyzer.Testing packages.


This NuGet package provides us with helper classes to write tests against our Code Fix Provider, lets see the helper classes and then define the specifications for our the CodeFixProvider. First let's create a class MaybeCodeFixProviderSpec! As you could recall from Testing Code Analyzer post, there was a CSharpAnalyzerTest<TAnalyzer, TVerifier>() that could be used to test an analyzer, a similar class exists for code fix providers, CSharpCodeFixTest<TAnalyzer, TCodeFix, TVerifier>, it need to know which analyzer and which code fix it is connected to and what would be the Verifier, in our case, it will be MaybeSemanticAnalyzer, DefaultVerifier for verification, and the code fix provider would be MaybeCodeFixProvider

var codeFixTest = new CSharpCodeFixTest<MaybeSemanticAnalyzer, MaybeCodeFixProvider, DefaultVerifier>
{
	// skipped for brevity of code
}

the properties expected for this instance is almost the same as the ones for the CSharpAnalyzerTest with one extra property, FixedCode, which we pass the expected code, after the CodeFixProvider is applied on the code with expected diagnostic! One specification is that If a throw statement exists in method with return type of Maybe, the code should change to return Maybe:

public async Task When_MethodWithReturnTypeMaybe_ContainsThrow_Then_ReplaceThrowWithReturnNone()
{
	//lang=c#
	const string code = 
	"""
	using System;
	using Sample.Fx;

	public class Program
	{
		public Maybe<int> GetValue(string number)
		{
			throw new InvalidOperationException("Could not parse the number");
		}
	}
	""";

	//lang=c#
	const string fixedCode = 
	"""
	using System;
	using Sample.Fx;

	public class Program
	{
		public Maybe<int> GetValue(string number)
		{
			return Sample.Fx.Maybe.None;
		}
	}
	""";
}

Now let's pass the required parameters to the CSharpCodeFixTest instance, it would be the TestState, ExpectedDiagnostics, and FixedCode:

var expectedDiagnostic = CSharpCodeFixVerifier<MaybeSemanticAnalyzer,MaybeCodeFixProvider, DefaultVerifier>
	.Diagnostic()
	.WithLocation(8, 9)

var codeFixTest = new CSharpCodeFixTest<MaybeSemanticAnalyzer, MaybeCodeFixProvider, DefaultVerifier>
{
	TestState = 
	{
		Sources = { code },
		ReferenceAssemblies = ReferenceAssemblies.Net.Net90,
		AdditionalReferences =
		{
			MetadataReference.CreateFromFile(typeof(None).Assembly.Location),
		}
	},
	ExpectedDiagnostics = { expectedDiagnostic },
	FixedCode = fixedCode
}

await codeFixTest.RunAsync();

PS: Beware that I am using CSharpCodeFixVerifier instead of CodeFixVerifier which is obsolete!

Writing the Code Fix Provider

This will of course fail at the moment as the CodeFixProvider does not exists ๐Ÿ˜‰ Let's create it now, in the Sample.Analyzer project create a new class named MaybeCodeFixProvider and inherit from CodeFixProvider, the first property to override is FixableDiagnosticIds returning the Id of the Diagnostic Analzyers the this class can provide code suggestions for them, if you remember every diagnostic analyzer has a unique identifier that could be used in here, the class should also be marked with ExportCodeFixProvider, and Shared attributes:

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MaybeCodeFixProvider)), Shared]
public class MaybeCodeFixProvider : CodeFixProvider
{
	public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } = [MaybeSemanticAnalyzer.DiagnosticId];
	
	// Skipped for code brevity
}

Then we need to override RegisterCodeFixesAsync and within that register an action that should be executed when the light bulb action is clicked by the user, for this purpose we are using CodeAction.Create method:

public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
	// We link only one diagnostic and assume there is only one diagnostic in the context.
	var diagnostic = context.Diagnostics.Single();

	// 'SourceSpan' of 'Location' is the highlighted area. We're going to use this area to find the 'SyntaxNode' to replace.
	var diagnosticSpan = diagnostic.Location.SourceSpan;

	// Get the root of Syntax Tree that contains the highlighted diagnostic.
	var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

	// Find SyntaxNode corresponding to the diagnostic.
	var diagnosticNode = root?.FindNode(diagnosticSpan);

	// To get the required metadata, we should match the Node to the specific type: 'ThrowStatementSyntax'.
	if (diagnosticNode is not ThrowStatementSyntax throwStatementSyntax)
		return;

	// Register a code action that will invoke the fix.
	context.RegisterCodeFix(
		CodeAction.Create(
			title: string.Format(Resources.SHG001CodeFixTitle, "Maybe.None", "throw"),
			token => ReplaceThrowWithReturnStatement(context.Document, throwStatementSyntax, token),
			equivalenceKey: nameof(Resources.SHG001CodeFixTitle)),
		diagnostic
	);
}

In this code example, we will use factory methods from Microsoft.CodeAnalysis.CSharp.SyntaxFactory and to have a smooth experience we will use Roslynquoter tool to create some part of the new SyntaxTree:

After having this code we could get the root node of the current document and replace the thorw statement in that tree with the return statement that we have just created, this will create a new tree as everything is immutable in Roslyn! That means, we need to create a new document as well, by replacing the root node in the old document:

private static async Task<Document> ReplaceThrowWithReturnStatement(
	Document document, CSharpSyntaxNode throwSyntaxNode, CancellationToken cancellationToken)
{
	var returnStatement = ReturnStatement(
		MemberAccessExpression(
			SyntaxKind.SimpleMemberAccessExpression,
			MemberAccessExpression(
				SyntaxKind.SimpleMemberAccessExpression,
				MemberAccessExpression(
					SyntaxKind.SimpleMemberAccessExpression,
					IdentifierName("Sample"),
					IdentifierName("Fx")),
					IdentifierName("Maybe")),
				IdentifierName("None")
	.WithTriviaFrom(throwSyntaxNode)
	.NormalizeWhitespace()));
}

var root = await document.GetSyntaxRootAsync(cancellationToken);
var newRoot = root?.ReplaceNode(throwSyntaxNode, returnStatement);

return document.WithSyntaxRoot(formattedRoot);

Now if we run our tests, it should be green! and if we build our project we should see that a light bulb appears next to the reported diagnostic with suggesting actions and if we click on that it will replace the throw statement with return statement!

That's it! ๐Ÿ˜‰ You could add more specifications to cover more complex scenarios, and implement the code fix provider to support those scenarios as well.

Extra Mile

When I was presenting this topic at Swetugg Stockholm 2025, a question was asked from audience: "Could we not include the namespace in the suggested code if it is already listed in the using directives at the top of the file?", and the answer is a bold YES!. Let's create a spec for it and then change our implementation to cover this scenario as well!

[Fact]
public async Task When_Suggesting_CodeFix_Skip_The_Namespace_If_It_Is_Already_Present()
{
	//lang=c#
	const string code = 
	"""
	using System;
	using Sample.Fx;

	public class Program
	{
		public Maybe<int> GetValue(string number)
		{
			throw new InvalidOperationException("Could not parse the number");
		}
	}
	""";

	//lang=c#
	const string fixedCode = 
	"""
	using System;
	using Sample.Fx;

	public class Program
	{
		public Maybe<int> GetValue(string number)
		{
			return Maybe.None;
		}
	}
	""";

var expectedDiagnostic = CSharpCodeFixVerifier<MaybeSemanticAnalyzer,MaybeCodeFixProvider, DefaultVerifier>
	.Diagnostic()
	.WithLocation(8, 9)

var codeFixTest = new CSharpCodeFixTest<MaybeSemanticAnalyzer, MaybeCodeFixProvider, DefaultVerifier>
{
	TestState = 
	{
		Sources = { code },
		ReferenceAssemblies = ReferenceAssemblies.Net.Net90,
		AdditionalReferences =
		{
			MetadataReference.CreateFromFile(typeof(None).Assembly.Location),
		}
	},
	ExpectedDiagnostics = { expectedDiagnostic },
	FixedCode = fixedCode
}

		await codeFixTest.RunAsync();
	}
}

If now we run the tests we will see the second one will fail with an error similar to the following:

System.InvalidOperationException
Context: Iterative code fix application
content of '/0/Test0.cs' did not match. Diff shown with expected as baseline:
 using System;
 using Sample.Fx;
                             
 public class Program
 {
     public Maybe<int> GetValue(string number)
     {
-        return Maybe.None;
+        return Sample.Fx.Maybe.None;
     }
 }

To fix it, we need to check whether the document already contains a using directive for Sample.Fx namespace, if so we could skip namespace expression in the suggested code! To examine that, we could access the SyntaxTree of the document and get all the child nodes with the type UsingDirectiveSyntax then we create different suggestions whether the namespace is already imported or not:

var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken);
var usingDirectives = syntaxTree!.GetCompilationUnitRoot().DescendantNodes().OfType<UsingDirectiveSyntax>();
var namespaceAlreadyImported = usingDirectives.Any(u => u.ToFullString().Contains("using Sample.Fx;"));

var classWithNamespace =
	MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
		MemberAccessExpression(
		SyntaxKind.SimpleMemberAccessExpression,
			MemberAccessExpression(
			SyntaxKind.SimpleMemberAccessExpression,
				IdentifierName("Sample"),
				IdentifierName("Fx")),
			IdentifierName("Maybe")), IdentifierName("None"));
										
var justClass = MemberAccessExpression(
	SyntaxKind.SimpleMemberAccessExpression, IdentifierName("Maybe"), IdentifierName("None"));

var returnStatement = ReturnStatement(
	namespaceAlreadyImported
		? justClass
		: classWithNamespace
	.WithTriviaFrom(throwSyntaxNode)
	.NormalizeWhitespace());

Run all the tests and they should be passing!

Conclusion

Even though Diagnostic Analyzers are great to catch wrong usage of some patterns or providing hints how a specific code should be used, they might not be enough in some scenarios; a complimentary approach would be to equip the developers with CodeFixProviders and enhance the whole developer experience. Every CodeFixProvider could be connected to multiple DiagnosticAnalyzer! And as always, it is recommended to have unit tests to specify the expected behaviours of your code providers!

At the end, thanks for reading, enjoy coding and Dametoon Garm [**]

Resources

Buy Me a Coffee at ko-fi.com

Create Syntax Trees using Roslyn APIs

Sometimes when we want to generate code whether it is source generators or code fixes for a code analyzer, it is required to know how a syntax tree could be created using the Roslyn Compiler API. There are two ways, that we will discuss them in this post.

Test Your Roslyn Code Analyzers

In the previous post we have seen how it is possible to create a diagnostic analyzer using Roslyn APIs. Being a TDD advocate it would be disappointing not to talk about how we could test our code analyzer! So let's take a look into it.

Write your own Code Analyzer with Roslyn

We are all familiar with diagnostics that are provided from the compiler when we develop an application, they could be in form of warnings, errors or code suggestions. A diagnostic or code analyzer, inspects our open files for various metrics, like style, maintainability, design, etc. However, sometimes we need to write a tailor-made analysis for our specific situation, tool, or project.

An error has occurred. This application may no longer respond until reloaded. Reload x