Creating a Code Fix
Let's start by creating a new diagnostic project called IfClauseDiagnostic. The template generates three different projects for you: the diagnostic, a test project, and a vsix installer that installs the diagnostic. You can follow along with the code in GitHub.
The default project for this solution is the vsix installer. If you press Ctrl-F5 at this point, you'll launch a new copy of Visual Studio, with your code fix installed. Your diagnostic is only active in this second copy because Visual Studio starts that copy using a different registry hive for add-ins. Your new diagnostic is installed only in that separate hive. This workflow is a bit cumbersome, so I like to rely on unit testing when I create diagnostics and code fixes.
Before writing our diagnostic, let's look at the template-generated code, including the tests. The template creates a sample diagnostic that replaces any lower-case characters in a type name (class, struct, or interface) with the equivalent uppercase character. Check out the branch 01-TemplateFiles to see the generated code.
Let's begin by examining the two unit tests generated by the template:
- A test whose input is the empty string. This test expects the diagnostic to do nothing.
- A test containing a type name with lowercase characters. This test expects the diagnostic to find and replace that code.
These tests are implemented using a base class, CodeFixVerifier, that contains numerous helper methods that make writing tests for a diagnostic and code fix much easier. The most important routines verify the changes that your code fix would make to a section of code. Perfecting that code can be difficult, and these utility routines make it much easier to validate your code fixes. As you write your own diagnostics, add new tests here for different variations on the code that your diagnostic should be finding.
The format of each of test is similar:
- Create a string to represent the code.
- If the string represents code that triggers your diagnostic, create a DiagnosticResult that represents the diagnostics.
- If the string represents code that triggers your diagnostic, create a string that represents the fixed code.
- Send the source code to the diagnostic, and then check the results provided by the CodeFixVerifier class. Note that in all tests where your diagnostic should not be triggered, the CodeFixVerifier class can ensure that the diagnostic is not triggered.
As we work on the project, we'll follow the same format for our unit tests. In fact, I'll replace the source and fixed code to validate the if clause tests.
Moving on from the unit tests, let's look at the sample diagnostic that the template generates. By examining this code, we'll learn how to create our specific diagnostic in the proper way. Let's start with the DiagnosticAnalyzer class. This code analyzes source to find type names that have lowercase letters. The diagnostic has three parts:
- Rule structure
- Initializing the diagnostic
- Scanning code for possible violations
Here's the definition of the rule for the lowercase diagnostic:
internal static DiagnosticDescriptor Rule = new DiagnosticDescriptor( DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true);
Most of the arguments for the DiagnosticDescriptor are human-readable strings. Some of them are important in how the diagnostic works:
- DiagnosticId, which is a string, must be unique to identify this diagnostic. As I'll show later, this ID is also used to match the diagnostic with the code fix.
- Category should match the FxCop category for your diagnostic. The categories are listed on MSDN.
- DiagnosticSeverity is an enum that indicates this diagnostic's warning level (error, warning, info, or hidden). You use this value to indicate how the user learns about your diagnostic. The more severe the error, the more prominent the warning.
This Rule object is returned by the SupportedDiagnostics public property:
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }
The template code supports a single diagnostic, but the property returns an array. Your diagnostic may support multiple diagnostics.
The final setup action taken by this class is in the overridden Initialize() method:
public override void Initialize(AnalysisContext context) { context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType); }
This method registers any analysis actions in your diagnostic. The template code has only one. The first parameter is the method that analyzes the code (we'll look at this shortly). The remaining parameters are a params array of SymbolKind enums that represent the source symbols your analyzer examines.
Finally, let's look at the AnalyzeSymbol method. This method examines the source context, looking for violations of your diagnostic rules:
private static void AnalyzeSymbol(SymbolAnalysisContext context) { var namedTypeSymbol = (INamedTypeSymbol)context.Symbol; // Find just those named type symbols with names containing lowercase letters. if (namedTypeSymbol.Name.ToCharArray().Any(char.IsLower)) { // For all such symbols, produce a diagnostic. var diagnostic = Diagnostic.Create(Rule, namedTypeSymbol.Locations[0], namedTypeSymbol.Name); context.ReportDiagnostic(diagnostic); } }
The template code looks at the symbol, which represents a type name, and examines it for any lowercase characters. If any are found, the code creates a diagnostic object representing the rule and the code location, and it reports that diagnostic. If the code has no mistakes, the diagnostic does nothing.
If you want to write a diagnostic, but aren't sure how to automate the fix, this is all you need to implement. As we build the fix for the If clauses, we'll write a code fix to add the braces where necessary. That means we'll be building and modifying the CodeFixProvider to modify the code. The CodeFixProvider has three responsibilities related to fixing the code. First, it must override GetFixableDiagnosticIds() to let Visual Studio know which diagnostic IDs it can fix. Note that it uses the same diagnostic ID from the DiagnosticAnalyzer class:
public sealed override ImmutableArray<string> GetFixableDiagnosticIds() { return ImmutableArray.Create(IfClauseDiagnosticAnalyzer.DiagnosticId); }
Next, it needs to register the method that would modify the code, if the user chooses that option. That's done in the override of ComputeFixesAsync:
public sealed override async Task ComputeFixesAsync(CodeFixContext context) { var root = await context.Document .GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); var diagnostic = context.Diagnostics.First(); var diagnosticSpan = diagnostic.Location.SourceSpan; // Find the type declaration identified by the diagnostic. var declaration = root.FindToken(diagnosticSpan.Start) .Parent.AncestorsAndSelf().OfType<TypeDeclarationSyntax>().First(); // Register a code action that will invoke the fix. context.RegisterFix( CodeAction.Create("Make uppercase", c => MakeUppercaseAsync(context.Document, declaration, c)), diagnostic); }
The argument to this function is a CodeFixContext that represents the code that was identified by the DiagnosticAnalyzer. The code finds the location in the document that must be modified, and it registers the action that will fix the code.
This code follows some very important practices for creating code fixes. The semantic and syntactic trees are immutable, so you can't simply replace nodes in place. Instead, what you'll do is create a copy of the entire solution where you've made the modifications you want. That's not as hard as it sounds, because the APIs include utility methods that provide many of the transformations you'll need. The MakeUppercaseAsync method uses one of those utility methods to ensure that the rename operation updates all references to the type as well:
private async Task<Solution> MakeUppercaseAsync(Document document, TypeDeclarationSyntax typeDecl, CancellationToken cancellationToken) { // Compute new uppercase name. var identifierToken = typeDecl.Identifier; var newName = identifierToken.Text.ToUpperInvariant(); // Get the symbol representing the type to be renamed. var semanticModel = await document.GetSemanticModelAsync(cancellationToken); var typeSymbol = semanticModel.GetDeclaredSymbol(typeDecl, cancellationToken); // Produce a new solution that has all references to that type renamed, // including the declaration. var originalSolution = document.Project.Solution; var optionSet = originalSolution.Workspace.Options; var newSolution = await Renamer.RenameSymbolAsync(document.Project.Solution, typeSymbol, newName, optionSet, cancellationToken).ConfigureAwait(false); // Return the new solution with the now-uppercase type name. return newSolution; }
The transformations may take time, so this async method returns a Task<Solution>. You can see that it's building a new Solution object that is a copy of the original, with the chosen type symbol renamed.