Build Your Own C# Analyzer and Code Fix

Insights and a Brief Tutorial from Cognizant Softivion’s .NET Lead Engineer

Mihai
Mihai

Latest posts by Mihai

I’ve always loved Resharper. It’s a great tool and it helps me with my work every day. But after the number of times, I had to explain to others what it does and why they should use it, why a license is worth paying for and thinking about how it works, the thought of “doing it myself” crossed my mind.

So what does it actually do and how is it related to C# Analyzers? Well, for starters it analyzes your code and gives you hints on how you can improve it. The Code Fixes come in handy because no one likes a tool which tells you what’s wrong with your code but doesn’t at least tell you how to improve it if not fix it for you.

Of course, Resharper was the starting point of my curiosity around the world of Analyzers and Code Fixes. But I haven’t actually started doing something about that until recently when I had to come up with a plan of checking the applications I was working on from the security perspective. So I started investigating the OWASP website for information about vulnerabilities and top tools used to prevent issues.

Most of the useful tools recommended were paid, so my reluctance around the Resharper license was visible again. I can’t help but thinking if by keeping up with the latest security threats we could simply build a series of analyzers ourselves to test our applications and save the money in the process. I know, it’s a long shot, and before we get there we should first learn exactly how to implement pretty much anything we want in analyzer in order to accommodate what could come in the future. In order to know that, we should get acquainted with Roslyn first, and gain some basic knowledge around syntax and semantic analysis and syntax transformation.

Let’s use an example around the “make uppercase” keyword. First, we’re creating the project in VS. Then, we need to tell the analyzer what we’re looking for. We go in the Analyzer.cs class and since we want to basically replace all symbols with UPPERCASE naming, we’ll register the symbol action:

public override void Initialize(AnalysisContext context)
    {

         context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);

     }

And report any lower finding:

private static void AnalyzeSymbol(SymbolAnalysisContext context)

     {

         var namedTypeSymbol = (INamedTypeSymbol)context.Symbol;

         // Find just the 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);

         }

     }

After this step, we are finished with the Analyzer part of the code, and we’ll move to the Code Fix part. We need to iterate through each “issue” signaled by the diagnostic, identify its surroundings and send it to the “FixIt” method:

public sealed override async Task RegisterCodeFixesAsync(CodeFixContext cont)

     {

         var root = await cont.Document.GetSyntaxRootAsync(cont.CancellationToken).ConfigureAwait(false);

         var diag = cont.Diagnostics.First();

         var span = diag.Location.SourceSpan;

         // Find the type declaration identified by the diagnostic.

         var decl = root.FindToken(span.Start).Parent.AncestorsAndSelf().OfType<TypeDeclarationSyntax>().First();

         // Register a code action that will invoke the fix.

         cont.RegisterCodeFix(

             CodeAction.Create(

                 title: title,

                 createChangedSolution: c=>MakeUppercaseAsync(cont.Document, decl, c),

                 equivalenceKey: title), diag);

     }

Notice the CodeAction.Create invocation. We used the Solution type parameter instead of Document. One represents a set of projects and their source code documents and the other is just a part of a project. The actual fixing happens in the MakeUppercaseAsync method:

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;

     }

You can click the Light Bulb from the left or move the editor caret over the squiggly underline and press Ctrl+ to display the suggested code fix:

Looks great in action, doesn’t it? And it didn’t take long to implement. Well, this is where the heavy part begins.

It’s fun to play with these projects and make tests, most of the commonly used analyzers are actually open source and available on GitHub, but we will need to seriously use our coding skills to implement something for the latest and greatest threats. In my mind, in a couple of years, after we have deeply studied security vulnerabilities, we should have a code base for the most common issues and share it between projects. Security is no joke and we should create serious tools to detect and fix our problems.

Share This Article


Mihai
Mihai

Latest posts by Mihai

No Comments

Post A Comment