My First Roslyn Analyzer and Fixer

I attended our users group last Monday, where Jim Wooly showed us, attendees, how to create our first Analyzer using .NET Compiler Platform, formerly known as Roslyn.  He posted a bunch of resources here.  I wanted to document for myself the steps I took in creating an analyzer/fixer to ensure that all fields inside a class start with an underscore.  I am not starting a debate on naming conventions, but it seems like a good way to experiment. 

First of all, I had to setup the environment.  I used a virtual machine with Windows 10, though it is not a requirement.

Here are installs I ran in order

Visualizer is helpful when exploring source code to decide what API classes should be used in any particular case.  It is not necessary, but I had to use to write my analyzer.

Now the steps to create your first analyzer.

Start new project in VS 2015 and pick template called Analyzer with Code Fix

image

The solution will contain three projects: actual analyzer, tests and VSIX.  VSIX is the actual deployable Visual Studio plug in you can post on Visual Studio gallery for example. The code in the analyzer is trivial, ensuring that class names are in all upper case. 

Now I am going to change the code.  First, the analyzer.  I am going to look for Field declarations and check for variable declaration names.  I want to check to see if they start with an underscore.  There are two steps I need to take.  First of all, I want to register for notification of specific symbols, field declaration symbols.  I can do this in Initializer method of my class, which inherits from DiagnosticAnalyzer.

public override void Initialize(AnalysisContext context) { context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.Field); }

Then I have to write the code to check the names and display a specific message.  This is done in the method passed to register call above – AnalyzeSymbol.

private static void AnalyzeSymbol(SymbolAnalysisContext context) { var filedSymbol = (IFieldSymbol)context.Symbol; if (!filedSymbol.Name.StartsWith("_")) { // For all such symbols, produce a diagnostic. var diagnostic = Diagnostic.Create(Rule, filedSymbol.Locations[0], filedSymbol.Name); context.ReportDiagnostic(diagnostic); } }

This is it.  One thing Jim pointed out is that the code inside AnalyzeSymbol method must be very fast, as it will be called many, many, many times.  Because we registered to be only notified for field symbols, we can safely cast passed in Symbol to IFieldSymbol.   Now, we need to write the fixer code.  Ideas again come from generated sample project template.  One super cool feature included in magical Renamer class.  It will help us simply fix the symbol name without having to manually hunt down all the instances.  Fixer is a separate class, inheriting from CodeFixProvider. FIrst we have to tell Visual Studio what diagnostics we can fix.  In our case, previously defined underscore diagnostic.

public sealed override ImmutableArray<string> FixableDiagnosticIds { get { return ImmutableArray.Create(Analyzer2Analyzer.DiagnosticId); } }

Next, we have to register the fix, including the message we want to show to the coder.

public sealed override async Task RegisterCodeFixesAsync(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<FieldDeclarationSyntax>().First(); // Register a code action that will invoke the fix. context.RegisterCodeFix( CodeAction.Create("Add underscore", c => AddUnderscoreAsync(context.Document, declaration, c)), diagnostic); }

Finally, we need to implement the method we just specified, AddUnderscroreAsync

private async Task<Solution> AddUnderscoreAsync( Document document, FieldDeclarationSyntax fieldDeclaration, CancellationToken cancellationToken) { var variableDeclaration = fieldDeclaration.Declaration.Variables[0]; var newName = variableDeclaration.Identifier.Text; if (!newName.StartsWith("_")) { newName = $"_{newName}"; } // Get the symbol representing the type to be renamed. var semanticModel = await document.GetSemanticModelAsync(cancellationToken); var typeSymbol = semanticModel.GetDeclaredSymbol(variableDeclaration, 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; }

Now, we need to write tests, because it is much easier that live debugging.  Again, I am using generated code as guidelines.  Mainly, I just create source code text that contains an issue, then call analyzer and fixer, then verify corrected code.

using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using TestHelper; using Analyzer2; namespace Analyzer2.Test { [TestClass] public class UnitTest : CodeFixVerifier { //No diagnostics expected to show up [TestMethod] public void Should_Not_Add_Name_Error() { var test = @""; VerifyCSharpDiagnostic(test); } [TestMethod] public void Verify_Error_Message() { var test = @" using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Diagnostics; namespace ConsoleApplication1 { class Random { private string thing; } }"; var expected = new DiagnosticResult { Id = Analyzer2Analyzer.DiagnosticId, Message = String.Format("Field {0} does not start with underscrore", "thing"), Severity = DiagnosticSeverity.Warning, Locations = new[] { new DiagnosticResultLocation("Test0.cs", 13, 28) } }; VerifyCSharpDiagnostic(test, expected); } //Diagnostic and CodeFix both triggered and checked for [TestMethod] public void Verify_Fix() { var test = @" using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Diagnostics; namespace ConsoleApplication1 { class Random { private string thing; } }"; var fixtest = @" using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Diagnostics; namespace ConsoleApplication1 { class Random { private string _thing; } }"; VerifyCSharpFix(test, fixtest); } protected override CodeFixProvider GetCSharpCodeFixProvider() { return new Analyzer2CodeFixProvider(); } protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer() { return new Analyzer2Analyzer(); } } }

 

For final tests, I am building the VSIX project, third project in the solution, then importing generated .vsix file, finally writing a sample class that contains an issue I am troubleshooting, verifying that my analyzer and fixer still works.  It does!

You can download solution here.

Thanks, Jim!

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *