Comments

How do I Analyze Comments?

When creating a Visual Studio analyzer, you may ask yourself the question, how can I analyze comments? I say to you good question. Let’s start by trying the obvious:

 context.RegisterSyntaxNodeAction(CommentMethod, SyntaxKind.SingleLineCommentTrivia)

You would think that this would work, as I did the first time I tried this. Unfortunately, comment trivia is handled differently and your CommentMethod will never be called. To properly handle comment trivia, you need to register for the entire syntax tree and then parse out the nodes that you want.

Fortunately, this is not difficult. First, in your initialize method, you want to register a SyntaxTreeAction

public override void Initialize(AnalysisContext context)
{
    context.RegisterSyntaxTreeAction(this.HandleSyntaxTree);
}

Then in your syntax tree action, you can parse out the nodes that you care about. For example, below I am looking for empty comments, so I parse out the SingleLineCommentTrivia and MultiLineCommentTrivia :

private void HandleSyntaxTree(SyntaxTreeAnalysisContext context)
{
    SyntaxNode root = context.Tree.GetCompilationUnitRoot(context.CancellationToken);
    var commentNodes = from node in root.DescendantTrivia() where node.IsKind(SyntaxKind.MultiLineCommentTrivia) || node.IsKind(SyntaxKind.SingleLineCommentTrivia) select node;

    if (!commentNodes.Any())
    {
        return;
    }
    foreach (var node in commentNodes)
    {
        string commentText = "";
        switch (node.Kind())
        {
            case SyntaxKind.SingleLineCommentTrivia:
                commentText = node.ToString().TrimStart('/');
                break;
            case SyntaxKind.MultiLineCommentTrivia:
                var nodeText = node.ToString();

                commentText = nodeText.Substring(2, nodeText.Length-4);
                break;
        }

        if (String.IsNullOrWhiteSpace(commentText))
        {
            var diagnostic = Diagnostic.Create(Rule, node.GetLocation());
            context.ReportDiagnostic(diagnostic);
        }
    }

}

This method is pretty straightforward. I use the DescentdantTrivia from the root node to get all of the nodes and then filter out anything except the comment nodes that I wish to parse.

After that it is just removing the comment characters and parsing the text of the comments to ensure they are not empty.

As you can see, parsing comments using the Roslyn code analysis engine is easy once you know the correct way to access the syntax trivia nodes.

Image Credit: Archivo-FSP via Creative Commons

Test

Testing Your Analyzer

As we all know, unit tests are a requirement of modern day development and writing unit tests for your analyzers couldn’t be easier. There are a lot of helper methods provided by the Roslyn team to get you going, so there is no excuse to not test your analyzer.

In the Creating Your First Analyzer post, we discussed how to use the templates to create your first analyzer. When you used the template, one of the projects created is the MyFirstAnalyzer.Test project. If you look at the UnitTests.cs file, you’ll notice there are two tests and a couple of methods that are overridden. You’ll also notice that this test class derives from the CodeFixVerifier base class. If you only wanted to test your diagnostics, you can simply derive from the DiagnosticVerifier base class.

To start, lets look at the overridden methods:

protected override CodeFixProvider GetCSharpCodeFixProvider()
{
    return new MyFirstAnalyzerCodeFixProvider();
}

protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
{
    return new MyFirstAnalyzerAnalyzer();
}

The purpose of these two methods are to provide the base class with instances of the implementation of your diagnostic and verifier, so they can be tested. Failure to override these methods will result in failures when making the call to verify the diagnostic and code fix.

The other two methods in this class are unit tests. One is a negative test, ensuring the diagnostic does not trigger on a blank file. The other is a test that verifies the diagnostic is triggered at the correct location and it also executes the code fix and ensures that the fixed code matches the desired result. Let’s start by looking into the portion that tests the diagnostic:

        var test = @"
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;

namespace ConsoleApplication1
{
    class TypeName
    {   
    }
}";
        var expected = new DiagnosticResult
        {
            Id = MyFirstAnalyzerAnalyzer.DiagnosticId,
            Message = String.Format("Type name '{0}' contains lowercase letters", "TypeName"),
            Severity = DiagnosticSeverity.Warning,
            Locations =
                new[] {
                        new DiagnosticResultLocation("Test0.cs", 11, 15)
                    }
        };

        VerifyCSharpDiagnostic(test, expected);

As you can see, the code being tested is simply passed as a string to the VerifyCSharpDiagnostic method. The DiagnosticResult is created as the expectation for the output of the diagnostic. One thing I would change about this default behavior is that the Message string is hard-coded here as well as in the diagnostic. It may be better to share the strings across both the test and diagnostic, but that is a personal preference. Running the VerifyCSharpDiagnostic will ensure that the correct diagnostics are triggered at the expected locations.

Once we have verified that the diagnostics is triggered, we can also verify that the codefix works. So, in this example, the code fix code is very straightforward:

        var fixtest = @"
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;

namespace ConsoleApplication1
{
    class TYPENAME
    {   
    }
}";
        VerifyCSharpFix(test, fixtest);

As you can see, it creates the desired code as a string and then passes in the original code and the desired code to the VerifyCSharpFix method. This method will verify that the passed in original code is correctly transformed to the fixed code after the code fix is applied.

Note: If you are working with Visual Basic code, you can use the equivalent VerifyBasicDiagnostic and VerifyBasicFix methods to test Visual Basic code. This can be very useful in cases where your diagnostic or code fix will result in different behavior for VB.Net code.

As you can see, writing tests for analyzers is very straightforward and (as usual) the Roslyn team has provided great tooling for getting your unit tests up an running very quickly.

Image Credit: Alberto G. via Creative Commons

FullSyntaxGraph

Using the Roslyn Syntax Visualizer

If you plan on creating an analyzer that analyzes more than just simple types, you are going to have to get familiar with the syntax graph. Luckily the Roslyn team has created a syntax visualizer that lets you see the syntax graph of the code that is currently selected in the IDE. First steps first, you need to download the syntax visualizer from the Extensions and Updates section in Visual Studio. Do a search for “Roslyn Syntax” and it should be one of the first items in the list.

syntaxvisualizer

Make sure you get the visualizer that matches version of Visual Studio that you have installed. You will need to restart Visual Studio after installing the visualizer. You can now navigate to the View->Other Windows menu and the Roslyn Syntax Visualizer is available from there.

SyntaxVisualizer

Now, as you navigate through your code, you can see the syntax graph that represents the code at the location of your cursor. For example, I have the following code:

namespace WindowsFormsApplication1
{
    public class Class1
    {
        public Class1()
        {
            var x = 1;
            System.Console.WriteLine(x);
        }
    }
}

The syntax visualizer will show the following:

SyntaxGraphText

Using this view allows you to understand all of the types of tokens that are in use and how to traverse the nodes in a code analysis document. The visualizer also provides a graph view of the syntax graph. Simply select any node and click the View Directed Syntax Graph menu item.

ViewGraphMenu

Selecting this option will open a graphical view of the syntax graph. I recommend focusing on a method or block of code when opening this view, as even small chunks of code have a decent sized graph. For example, the two line constructor above is represented by the following graph:

ConstructorGraph

As you can see the visualization tools provided by the Roslyn team give you great insights into the structure of your code and when you go to build analyzers that need to manipulate blocks of code, these tools will be invaluable. Also, don’t forget to install the visualizer into all of your experimental instances or you might get caught scratching your head as to why you can’t find the menu item. In a future post, we will discuss how we can use the visualizer when building a real world analyzer.

Modifying the First Analyzer To Handle Partials

Last time we talked about the basics of creating an analyzer. Now that we have some working code, let’s tweak it a bit. We’re going start by fixing a small issue with the default code to update it to handle partial classes.

Let’s look for a minute at how our current analyzer handles partial classes. I created a file with the partial class Foo declared multiple times. As you see, it only reports the diagnostic on one of the two declared instances.

PartialClass

Looking into the analyzer code, its pretty easy to see why. If you look at the AnalyzeSymbol method, when the diagnostic is created, it uses the first location to report the diagnostic.

private static void AnalyzeSymbol(SymbolAnalysisContext context)
{
    // TODO: Replace the following code with your own analysis, generating Diagnostic objects for any issues you find
    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);
    }
}

Since Locations is an ImmutableArray of Location, we can simply iterate over that and report all of the diagnostics for this symbol.

private static void AnalyzeSymbol(SymbolAnalysisContext context)
{
    // TODO: Replace the following code with your own analysis, generating Diagnostic objects for any issues you find
    var namedTypeSymbol = (INamedTypeSymbol)context.Symbol;

    // Find just those named type symbols with names containing lowercase letters.
    if (namedTypeSymbol.Name.ToCharArray().Any(char.IsLower))
    {
        foreach (var location in namedTypeSymbol.Locations)
        {
            // For all such symbols, produce a diagnostic.
            var diagnostic = Diagnostic.Create(Rule, location, namedTypeSymbol.Name);

            context.ReportDiagnostic(diagnostic);
        }
    }
}

Running the analyzer project again, you will now see that the diagnostic is reported on both of the declarations of the partial class.

PartialClassFixed

The important lesson to take away here is that there are a lot of ways to do things in C# and when writing an analyzer you need to consider that. Partials are a crucial part of the framework and failing to account for them in your analyzer will leave a small, but sometimes annoying, hole in your analyzers coverage.

MagnifyingGlass

Creating your First Code Analyzer

Welcome to the wonderful world of Code Analyzers. With Visual Studio 2015, you now have the ability to create a custom analyzer to provide real time warnings and errors to developers as they code. This type of technology has all sorts of uses. If you are creating a library and want to ensure it is being used properly, you can create an analyzer. If you want to be sure your entire development team is using the same style, you can create analyzers.

To get started, make sure you have the release candidate version of Visual Studio 2015. You will also need to install the .Net Compiler Platform SDK Templates from the Visual Studios extensions dialog. It is important to get the templates that match the version of Visual Studio you are using. Since I am using the release candidate, I am getting the RC templates.

Extensions and Updates Dialog with .Net Compiler Platform SDK Templates for RC selected

Now that you have the latest VS and the compiler platform templates, you can create a new analyzer project. The template is available under the Extensibility tab.

New Project Dialog with Analyzer template selected

Once created, you’ll notice that the solution has 3 projects

  • MyFirstAnalyzer (This is actual analyzer)
  • MyFirstAnalyzer.Test (Unit Tests)
  • MyFirstAnalyzer.Vsix (Visual Studio extension)

The MyFirstAnalyzer project already contains a very simple analyzer to get you started. The analyzer simply checks if a symbol contains lower case letters. It also provides a codefix to change the symbol to all uppercase. If you build and run your project, you will see a new instance of Visual Studio start and your analyzer will be loaded. This instance is referred to as an experimental instance. It has its own settings, so you can customize it without affecting your normal day-to-day environment. I recommend using a different theme in this environment, so you can tell them apart.

In the experimental instance, you can open up an existing project or create a new project. If you browse into a .cs file, you will see that some class names will have a green squiggly line underneath them (provided they contain lowercase letters).

Code Analyzer dialog with analyzer message

If you place your cursor on the symbol and hit the Quick Actions key (Default is Ctrl+.), you will see options for fixing this warning. You have the option to fix or suppress the warning. It also provides a nice preview of the operation, so you know how the resulting code will look.

Code fix window with Make Uppercase code fix shown

Since we have verified that the analyzer works, let’s look at the code that is creating all of this magic. Open the DiagnosticAnalyzer.cs file. The start of the file contains code that sets up the analyzer. The important lines here are the Rule of type DiagnosticDescriptor, which provides Visual Studio the description of your diagnostic and the SupportedDiagnostics property which indicates which diagnostics are supported by this analyzer. This is where VS gets the information to show the user about your diagnostic.

internal static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }

The Initialize method registers an Action in your analyzer to be called whenever a type of symbol is encountered. In this case the AnalyzeSymbol method is registered to be called anytime a SymbolKind.NamedType (e.g. a class) needs to be analyzed. In a future blog post, we will go over the different SymbolKinds and how they are used.

public override void Initialize(AnalysisContext context)
{
    // TODO: Consider registering other actions that act on syntax instead of or in addition to symbols
    context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);
}

Finally, the code for the AnalyzeSymbol method is there. This code simply looks at the Symbol passed in and checks if it has any lowercase characters. If it does, it reports a diagnostics at the location of the symbol.

private static void AnalyzeSymbol(SymbolAnalysisContext context)
{
    // TODO: Replace the following code with your own analysis, generating Diagnostic objects for any issues you find
    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 Diagnostic.Create line creates the diagnostic that is then reported to Visual Studio using the context.ReportDiagnostic method. Diagnostic.Create is a very powerful method and has a lot of overloads. I urge you to explore some of these overloads and see how you can change the behavior of the diagnostic that gets created.

As you can see, creating an analyzer is pretty straightforward. You simply have to register yourself correctly and then you are in the pipeline for analyzing the code in a visual studio project. In the next blog post, we will modify the analyzer to support additional symbol types and cover some of the issues you may run into when working with different SymbolKinds.

Image Credit: Kit using Creative Commons License

1 2 3 6