March 4th, 2020

How to write a Roslyn Analyzer

Mika Dumont
Senior Product Manager

Roslyn analyzers inspect your code for style, quality, maintainability, design and other issues. Because they are powered by the .NET Compiler Platform, they can produce warnings in your code as you type even before you’ve finished the line. In other words, you don’t have to build your code to find out that you made a mistake. Analyzers can also surface an automatic code fix through the Visual Studio light bulb prompt that allows you to clean up your code immediately. With live, project-based code analyzers in Visual Studio, API authors can ship domain-specific code analysis as part of their NuGet packages.

You don’t have to be a professional API author to write an analyzer. In this post, I’ll show you how to write your very first analyzer.

Getting started

In order to create a Roslyn Analyzer project, you need to install the .NET Compiler Platform SDK via the Visual Studio Installer. There are two different ways to find the .NET Compiler Platform SDK in the Visual Studio Installer:

Install using the Visual Studio Installer – Workloads view:

  1. Run the Visual Studio Installer and select Modify.
    Visual Studio Installer

  2. Check the Visual Studio extension development workload.
    Visual Studio Extension Development Workload

Install using the Visual Studio Installer – Individual components tab:

  1. Run the Visual Studio Installer and select Modify.

  2. Select the Individual components tab.

  3. Check the box for .NET Compiler Platform SDK.
    Visual Studio Individual Components


Writing an analyzer

Let’s begin by creating a syntax tree analyzer. This analyzer generates a syntax warning for any statement that is not enclosed in a block that has curly braces { and }. For example, the following code generates a warning for both the if-statement and the System.Console.WriteLine invocation statement, but the while statement is not flagged:


Brace Analyzer Diagnostic

  1. Open Visual Studio.

  2. On the Create a new project dialog search VSIX and select Analyzer with Code Fix (.NET Standard) in C# and click Next.
    Create New Project Dialog

  3. Name your project BraceAnalyzer and click OK. The solution should contain 3 projects: BraceAnalyzer, BraceAnalyzer.Test, BraceAnalyzer.Vsix.
    Analyzer Solution Layout

    • BraceAnalyzer: This is the core analyzer project that contains the default analyzer implementation that reports a diagnostic for all type names that contain any lowercase letter.
    • BraceAnalyzer.Test: This is a unit test project that lets you make sure your analyzer is producing the right diagnostics and fixes.
    • BraceAnalyzer. Vsix: The VSIX project bundles the analyzer into an extension package (.vsix file). This is the startup project in the solution.

  4. In the Solution Explorer, open Resources.resx in the BraceAnalyzer project. This displays the resource editor.

  5. Replace the existing resource string values for AnalyzerDescription, AnalyzerMessageFormat, and AnalyzerTitle with the following strings:

    • Change AnalyzerDescription to Enclose statement with curly braces.
    • Change AnalyzerMessageFormat to `{` brace expected.
    • Change AnalyzerTitle to Enclose statement with curly braces.


    Resources Resx


  6. Within the BraceAnalyzerAnalyzer.cs file, replace the Initialize method implementation with the following code:

  7. public override void Initialize(AnalysisContext context)
    {
        context.RegisterSyntaxTreeAction(syntaxTreeContext =>
        {
            // Iterate through all statements in the tree
            var root = syntaxTreeContext.Tree.GetRoot(syntaxTreeContext.CancellationToken);
            foreach (var statement in root.DescendantNodes().OfType<StatementSyntax>())
            {
                // Skip analyzing block statements 
                if (statement is BlockSyntax)
                {
                    continue;
                }
    
                // Report issues for all statements that are nested within a statement
                // but not a block statement
                if (statement.Parent is StatementSyntax && !(statement.Parent is BlockSyntax))
                {
                    var diagnostic = Diagnostic.Create(Rule, statement.GetFirstToken().GetLocation());
                    syntaxTreeContext.ReportDiagnostic(diagnostic);
                }
            }
        });
    }


  8. Check your progress by pressing F5 to run your analyzer. Make sure that the BraceAnalyzer.Vsix project is the startup project before pressing F5. Running the VSIX project loads an experimental instance of Visual Studio, which lets Visual Studio keep track of a separate set of Visual Studio extensions.

  9. In the Visual Studio instance, create a new C# class library with the following code to verify that the analyzer diagnostic is neither reported for the method block nor the while statement, but is reported for the if statement and System.Console.WriteLine invocation statement:
    Brace Analyzer Diagnostic

  10. Now, add curly braces around the System.Console.WriteLine invocation statement and verify that the only single warning is now reported for the if statement:
    Brace Diagnostic For If Statement


Writing a code fix

An analyzer can provide one or more code fixes. A code fix defines an edit that addresses the reported issue. For the analyzer that you created, you can provide a code fix that encloses a statement with a curly brace.

  1. Open the BraceAnalyzerCodeFixProvider.cs file. This code fix is already wired up to the Diagnostic ID produced by your diagnostic analyzer, but it doesn’t yet implement the right code transform.

  2. Change the title string to “Add brace”:

  3. private const string title = "Add brace";


  4. Change the following line to register a code fix. Your fix will create a new document that results from adding braces.

  5. context.RegisterCodeFix(
            CodeAction.Create(
                title: title,
                createChangedDocument: c => AddBracesAsync(context.Document, diagnostic, root),
                equivalenceKey: title),
            diagnostic);


  6. You’ll notice red squiggles in the code you just added on the AddBracesAsync symbol. Add a declaration for AddBracesAsync by replacing the MakeUpperCaseAsync method with the following code:

  7. Task<Document> AddBracesAsync(Document document, Diagnostic diagnostic, SyntaxNode root)
            {
                var statement = root.FindNode(diagnostic.Location.SourceSpan).FirstAncestorOrSelf<StatementSyntax>();
                var newRoot = root.ReplaceNode(statement, SyntaxFactory.Block(statement));
                return Task.FromResult(document.WithSyntaxRoot(newRoot));
            }


  8. Press F5 to run the analyzer project in a second instance of Visual Studio. Place your cursor on the diagnostic and press (Ctrl+.) to trigger the Quick Actions and Refactorings menu. Notice your code fix to add a brace!
    Image brace analyzer code fix2


Conclusion

Congratulations! You’ve created your first Roslyn analyzer that performs on-the-fly code analysis to detect an issue and provides a code fix to correct it. Now that you’re familiar with the .NET Compiler Platform SDK (Roslyn APIs), writing your next analyzer will be a breeze.

Category
.NET

Author

Mika Dumont
Senior Product Manager

Mika is a Product Manager on the .NET and GitHub Copilot developer experience.

6 comments

Discussion is closed. Login to edit/delete existing comments.

  • André Silva

    Great content!!

  • Michael Rivers

    Great article. Thank you. I’m excited to add this skill to my toolbox.

  • Kalle Niemitalo

    Any recommendations for the property values in DiagnosticDescriptor?

    Id: How to avoid conflicts with other analyzers and tools? (dotnet/roslyn#4376 talks about what syntax is allowed.)
    Category: Should a custom analyzer use entirely custom categories, or is there a list of recommended category names and their distinctive meanings? (Like there is a list of PowerShell verbs.)
    MessageFormat: Is it OK to have two DiagnosticDescriptor instances for the same issue in different contexts (e.g. in a property accessor...

    Read more