TestTubes

Testing our Comment Analyzer

In previous posts we covered the basics of creating a comment analyzer and creating a code fix for that analyzer. The final thing that is needed for any analyzer is a good set of unit tests.

To start we need to create a TestClass that inherits from CodeFixVerifier.

[TestClass]
public class EmptyCommentCodeFix : CodeFixVerifier
{
}

The CodeFixVerifier base class provides a few helper methods that make testing your analyzer much easier. To start using it we need to override a few methods. We’ll start by overriding theGetCSharpCodeFixProviderand return an instance of ourEmptyCommentCodeFix

protected override CodeFixProvider GetCSharpCodeFixProvider()
{
    return new AnalyzerSamples.Comments.EmptyCommentCodeFix();
}

Next we will override the GetCharpDiagnosticAnalyzer method and return to it an instance of our EmptyCommentAnalyzer

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

These two overrides provide the underlying base class with the instances it needs to work with all of the helper methods it provides. Obviously, you could do all of this wiring up yourself, but this base class just makes it easier.

At this point, we can write our first test. We’ll start with a very simple test that exercises the most basic of uses of the diagnostic. We can start this by creating some basic test code:

[TestMethod]
public void TestBasicEmptyComment()
{
    var testCode = @"
class Foo
{
    public void Bar()
    {
        //
    }
}";

Given this code, we expect a diagnostic to appear at line 6 column 9. We now need to declare what we expect to happen and verify it. To do this we can create a diagnostic result and use the VerifyCSharpDiagnostic method.

var expected = new DiagnosticResult
{
    Id = EmptyCommentAnalyzer.DiagnosticId,
    Message = EmptyCommentAnalyzer.MessageFormat.ToString(),
    Severity = DiagnosticSeverity.Warning,
    Locations =
        new[] {
                new DiagnosticResultLocation("Test0.cs", 6, 9)
            }
};
this.VerifyCSharpDiagnostic(testCode, expected);

This code will pass and verify that the diagnostic is raised at the correct location. Try playing with the line or column numbers. Notice that if you get them wrong, the error message will give you the correct location to make it easier to get your locations correct when first creating the unit tests. For example, if I change the column parameter to 10, then I get the following error:

Assert.IsTrue failed. Expected diagnostic to start at column “10” was actually at column “9”

At this point we can create the expected code for the fix and verify that the code gets fixed correctly. To do this, we use the VerifyCSharpFix method sending it the original code and the fixed code.

var expectedFixedCode = @"
class Foo
{
    public void Bar()
    {
    }
}";
this.VerifyCSharpFix(testCode, expectedFixedCode);

With that, we have a test that validates our analyzer and our code fix. The full code for the test class is:

 [TestClass]
    public class EmptyCommentCodeFix : CodeFixVerifier
    {

        [TestMethod]
        public void TestBasicEmptyComment()
        {
            var testCode = @"
class Foo
{
    public void Bar()
    {
        //
    }
}";
            var expected = new DiagnosticResult
            {
                Id = EmptyCommentAnalyzer.DiagnosticId,
                Message = EmptyCommentAnalyzer.MessageFormat.ToString(),
                Severity = DiagnosticSeverity.Warning,
                Locations =
                    new[] {
                            new DiagnosticResultLocation("Test0.cs", 6, 9)
                        }
            };
            this.VerifyCSharpDiagnostic(testCode, expected);

            var expectedFixedCode = @"
class Foo
{
    public void Bar()
    {
    }
}";
            this.VerifyCSharpFix(testCode, expectedFixedCode);
        }

        protected override CodeFixProvider GetCSharpCodeFixProvider()
        {
            return new AnalyzerSamples.Comments.EmptyCommentCodeFix();
        }

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

With all of the helper methods and classes provided by the Roslyn team, there is no excuse to not test your analyzers. For a more exhaustive lists of tests that could be run against this analyzer, you can look at the SA1120 unit tests in the StyleCopAnalyzers project on GitHub.

*Image Credit: r. nial bradshaw via Creative Commons

FixAFlat

Creating a code fix that fixes comments

In a previous post we created an analyzer that looked for blank comments. Since empty comments are a very fixable problem (just remove them), I figured tackling the code fix for this would be a useful exercise.

To start, we need to register our code fix in the RegisterCodeFixesAsync method. In this method we simply loop over the diagnostics that have an ID we care about and register a method to perform the code fix.

public sealed override Task RegisterCodeFixesAsync(CodeFixContext context)
{
    foreach (var diagnostic in context.Diagnostics.Where(d => FixableDiagnosticIds.Contains(d.Id)))
    {
        context.RegisterCodeFix(CodeAction.Create("Remove the empty comment", 
                token => GetTransformedDocumentAsync(context.Document, diagnostic, token)), diagnostic);
    }

    return Task.FromResult<object>(null);
}

Now that the code fix is registered we can implement the GetTransformedDocumentAsync method to get a new document that represents the fixed code. To start, we can simply find the node in the document and replace it with a blank node.

private static async Task<Document> GetTransformedDocumentAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
{
    var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
    var node = root.FindTrivia(diagnostic.Location.SourceSpan.Start, true);

    var newDoc = document.WithSyntaxRoot(root.ReplaceTrivia(node, new SyntaxTrivia()));
    return newDoc;
}

If you run this analyzer as is, it correctly removes the comment node. So, we are done, right? Not exactly. While it removes the comment node, it leaves the blank line there, which you would expect to also go away. You might be tempted to just find the node after the comment and if it is a SyntaxKind.EndOfLineTrivia then remove it. However, if you do this, you will run into scenarios where this doesn’t work well. For example if there was content on the same line prior to the empty comment, you don’t want to remove the blank line.

public void Bar()
{
    System.Console.WriteLine(""A""); //
}

If you removed the blank line in this case, the resulting code would be:

public void Bar()
{
    System.Console.WriteLine(""A"");  }

So, when it comes down to it, we need to have a few basic rules in place for determining what to do next.

  • We will remove the whitespace prior to the comment node if there is any and there is no trailing content on the same line.
  • We will remove an end of line node following the comment node if there is one and there is no leading content on the same line.

In order to determine if there is leading content, we need to move to the node prior to the list of trivia in which our diagnostic node is contained and see if it is on the same line. The triviaList is calculated by a helper method which combines all of the leading and trailing trivia around the node we care about.

private static bool TriviaHasLeadingContentOnLine(SyntaxNode root, IReadOnlyList<SyntaxTrivia> triviaList)
{
    var nodeBeforeStart = triviaList[0].SpanStart - 1;
    var nodeBefore = root.FindNode(new Microsoft.CodeAnalysis.Text.TextSpan(nodeBeforeStart, 1));

    if (GetLineSpan(nodeBefore).EndLinePosition.Line == GetLineSpan(triviaList[0]).StartLinePosition.Line)
    {
        return true;
    }

    return false;
}

To determine if there is trailing content, we do the same, just looking at the node after the last node in the list of trivia.

private static bool TriviaHasTrailingContentOnLine(SyntaxNode root, IReadOnlyList<SyntaxTrivia> triviaList)
{
    var nodeAfterTriviaStart = triviaList[triviaList.Count - 1].SpanStart - 1;
    var nodeAfterTrivia = root.FindNode(new Microsoft.CodeAnalysis.Text.TextSpan(nodeAfterTriviaStart, 1));

    if (GetLineSpan(nodeAfterTrivia).StartLinePosition.Line == GetLineSpan(triviaList[triviaList.Count - 1]).EndLinePosition.Line)
    {
        return true;
    }

    return false;
}

Now that we have those helper methods, we can use them inside our GetTransformedDocumentAnsync method and follow the rules we laid out above:

private static async Task<Document> GetTransformedDocumentAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
{
    var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
    var node = root.FindTrivia(diagnostic.Location.SourceSpan.Start, true);

    int diagnosticIndex = 0;
    var triviaList = TriviaHelper.GetContainingTriviaList(node, out diagnosticIndex);

    var nodesToRemove = new List<SyntaxTrivia>();
    nodesToRemove.Add(node);

    // If there is trialing content on the line, we don't want to remove the leading whitespace
    bool hasTrailingContent = TriviaHasTrailingContentOnLine(root, triviaList);

    if (diagnosticIndex > 0 && !hasTrailingContent)
    {
        var previousStart = triviaList[diagnosticIndex - 1].SpanStart;
        var previousNode = root.FindTrivia(previousStart, true);
        nodesToRemove.Add(previousNode);
    }

    // If there is leading content on the line, then we don't want to remove the trailing end of lines
    bool hasLeadingContent = TriviaHasLeadingContentOnLine(root, triviaList);

    if (diagnosticIndex < triviaList.Count - 1)
    {
        var nextStart = triviaList[diagnosticIndex + 1].SpanStart;
        var nextNode = root.FindTrivia(nextStart, true);

        if (nextNode.IsKind(SyntaxKind.EndOfLineTrivia) && !hasLeadingContent)
        {
            nodesToRemove.Add(nextNode);
        }
    }

    // Replace all roots with an empty node
    var newRoot = root.ReplaceTrivia(nodesToRemove, (original, rewritten) =>
    {
        return new SyntaxTrivia();
    });

    Document updatedDocument = document.WithSyntaxRoot(newRoot);
    return updatedDocument;
}

Notice that we now have a list of nodes that we will remove from the SyntaxRoot and we use the ReplaceTrivia overload that allows us to specify an IEnumerable<SyntaxTrivia>.

As I mentioned in the previous article about this analyzer, it is diagnostic SA1120 in the StyleCopAnalyzers project. The full code fix is named SA120CodeFixProvider.The exact code sample I worked on here is available in my AnalyzersSamples project on Github under the EmptyCommentsCodeFix.

Now we have an analyzer and a code fix. What’s missing? Unit tests. The next time we delve into this analyzer we will go through how we can unit test this to make sure we are handling all of the cases we want to handle correctly.

Image Credit: Mike Mozart via Creative Commons

MagnifyingGlass

Using Analyzers in Your Projects

While many of my posts cover the basics of creating analyzers and digging into the syntax, this post is going to focus on how to add existing analyzers to your projects. There has already been a great deal of work done by many people to create a wide array of analyzers and you should take advantage of those on top of creating your own analyzers.

The easiest way to add analyzers to your project is to obtain them via Nuget. A few popular analyzers include the Microsoft.AnalyzerPowerPack and StyleCop.Analyzers. Simply open up the Nuget package manager and search for the analyzer that you want to install.

NugetPackageManager

Once installed, you will see the default settings that the analyzer developer decided upon for severity of the violation. Your code may end up looking something like this:

AnalyzerIssues

Luckily, you have full control over each and every analyzer that is provided to you and you can decide what should be reported and the severity of the violation. Under your project references, you will see a new node named Analyzers.

ProjectHierarchy

You can right click on this node and select the Open Active Rule Set option. This will bring up a dialog that lets you configure the rules.

RulesetWindow

You’ll notice an information bar at the top indicating that you are viewing the default ruleset. If you make any modifications, then it those changes will be persisted to a new file named <projectname>.ruleset. From this window, you can change the behavior of rules. You have the following options for each rule:

Value Definition
Warning Will appear as a warning in the Visual Studio Error List window.
Error Will appear as an error in the Visual Studio Error List window.
Info Will appear as an information message in the Visual Studio Error List window.
Hidden Analysis is still performed, but it is not reported by any Visual Studio UI. A custom UI could be created to report these diagnostics.
None The rule is disabled and the diagnostic will not be run against your code.
Inherit This will inherit the behavior from the rule group. There is currently no UI to set this.

I recommend getting as many analyzers as you can get your hands on to see what value they add. After you see what analyzers are available, you can trim back the analyzers you use to match your business needs. If you have a favorite analyzer that you think other developers needs to know about, leave a comment below.

Image Credit: Kit using Creative Commons License

Yolo

Code Analyzers: They aren’t just for C#

Visual Basic has always had a little place in my heart. VB5 was the language I used at my first job and VB.Net was the language I used to transition into managed code. Luckily for us, Microsoft has kept improving VB.Net and the Roslyn compiler supports it as a first class citizen, just like C#.

So I am going to show you how to write an analyzer that analyzes Visual Basic code, but I will write the analyzer in C#, as that is my preferred language. To start analyzing Visual Basic code, you need to add a the Microsoft.CodeAnalysis.VisualBasic nuget package to your project.

Now, when creating a new analyzer, you need to make sure you indicate it is a Visual Basic analyzer by setting the DiagnosticAnalyzer attribute to include LanguageNames.VisualBasic. You should also add usings for the Microsoft.CodeAnalysis.VisualBasic and Microsoft.CodeAnalysis.VisualBasic.Syntax to ensure that the type you need to analyze for Visual Basic code are available.

using Microsoft.CodeAnalysis.VisualBasic;
using Microsoft.CodeAnalysis.VisualBasic.Syntax;

namespace AnalyzerSamples.VB
{
    [DiagnosticAnalyzer(LanguageNames.VisualBasic)]
    public class OptionExplicitAndOptionStrictShouldNotBeTurnedOff : DiagnosticAnalyzer
    {
      //...
    }
}

In this analyzer, we are going to analyze Option Statements and ensure that Option Explicit and Option Strict are not set to Off. To do this, we will register a syntax node action for SyntaxKind.OptionStatement. Note that this SyntaxKind is in the Microsoft.CodeAnalysis.VisualBasic.Syntax, instead of the CSharp namespaces we have used in past analyzers.

public override void Initialize(AnalysisContext context)
{
    context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.OptionStatement);
}

Our AnalyzeSyntax method will run a few checks to make sure that we are working with a valid option statement (remember analyzers can run against code that is not compilable) and that it is a type that we care about. To start, our method simply gets the option statement and makes sure it has both the Name and Value keywords. If not, we stop analyzing.

private static void AnalyzeSyntax(SyntaxNodeAnalysisContext context)
{
    // TODO: Replace the following code with your own analysis, generating Diagnostic objects for any issues you find
    var optionStatement = context.Node as OptionStatementSyntax;

    if (optionStatement == null)
        return;

    if (optionStatement.NameKeyword.IsMissing || optionStatement.ValueKeyword.IsMissing)
        return;

Next we are going to look at the name keyword and check it’s kind. Since we are checking Explicit and Strict, we will look to see if it is a SyntaxKind.Explicit or SyntaxKind.Strict:

// Look to see if the keyword is Strict or Explicit.
if (!(optionStatement.NameKeyword.IsKind(SyntaxKind.StrictKeyword) || optionStatement.NameKeyword.IsKind(SyntaxKind.ExplicitKeyword)))
    return;

After we have validated that it is an option we care about, we can then check the value to see if it is on or off. This is as easy as checking the ValueKeyword to see if its SyntaxKind is SyntaxKind.OffKeyword:

// We only care if it is set to Off
if (!optionStatement.ValueKeyword.IsKind(SyntaxKind.OffKeyword))
    return;

If we have then reached this point in the code, we know that one of the two keywords is set to off and we can raise a diagnostic at the correct location with the correct message. Notice that we use the ValueText of the NameKeyword to allow the message to correctly show the name of the keyword that is causing the violation.

// For all such symbols, produce a diagnostic.
var diagnostic = Diagnostic.Create(Rule, context.Node.GetLocation(), optionStatement.NameKeyword.ValueText);
context.ReportDiagnostic(diagnostic);

The full code for the analyzer is:

using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.VisualBasic;
using Microsoft.CodeAnalysis.VisualBasic.Syntax;

namespace AnalyzerSamples.VB
{
    [DiagnosticAnalyzer(LanguageNames.VisualBasic)]
    public class OptionExplicitAndOptionStrictShouldNotBeTurnedOff : DiagnosticAnalyzer
    {
        public const string DiagnosticId = "OptionExplicitAndOptionStrictShouldNotBeTurnedOff";
        internal static readonly LocalizableString Title = "Option strict and option explicit should not be turned off";
        internal static readonly LocalizableString MessageFormat = "Option {0} should not be turned off.";
        internal const string Category = "Visual Basic";

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

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

        public override void Initialize(AnalysisContext context)
        {
            context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.OptionStatement);
        }

        private static void AnalyzeSyntax(SyntaxNodeAnalysisContext context)
        {
            var optionStatement = context.Node as OptionStatementSyntax;

            if (optionStatement == null)
                return;

            if (optionStatement.NameKeyword.IsMissing || optionStatement.ValueKeyword.IsMissing)
                return;

            // Look to see if the keyword is Strict or Explicit.
            if (!(optionStatement.NameKeyword.IsKind(SyntaxKind.StrictKeyword) || optionStatement.NameKeyword.IsKind(SyntaxKind.ExplicitKeyword)))
                return;

            // We only care if it is set to Off
            if (!optionStatement.ValueKeyword.IsKind(SyntaxKind.OffKeyword))
                return;

            // For all such symbols, produce a diagnostic.
            var diagnostic = Diagnostic.Create(Rule, context.Node.GetLocation(), optionStatement.NameKeyword.ValueText);

            context.ReportDiagnostic(diagnostic);

        }
    }
}

That’s it. Writing analyzers for Visual Basic specific code is just as easy as writing analyzers for C# code. Many analyzers may be able to share a good majority, if not all, of their code to analyze both C# and Visual Basic. So, next time you are writing an analyzer, see how hard it would be to make it work for both C# and Visual Basic. A VB programmer may thank you for it.

LikeAGlove

Working With Catch Blocks in your Roslyn Analyzer

Exceptions. Most developers have a love/hate relationship with them. And catching general exceptions is a topic that gets discussed over and over and over. Regardless of which side of the fence you are on, you may run into a scenario when you want to write an analyzer that can enforce your team’s standards when working with exceptions. Roslyn offers a few different SyntaxKinds which you can register for that give you access to various parts of a catch statement.

Syntax Kind Description
SyntaxKind.CatchClause This represents the entirety of the catch statement and the body of the work to be done inside the catch clause.
SyntaxKind.CatchDeclaration This represents the variable declaration portion of the catch clause. So in the catch clause catch (Exception ex) when (i==1), this node would represent Exception ex
SyntaxKind.CatchFilterClause This represents the filter provided as part of the catch clause. So in the catch clause catch (Exception ex) when (i==1), this node would represent i==1
SyntaxKind.CatchKeyword This represents the catch keyword that is part of a catch clause.

So, now that we know the different types of syntax nodes we can work with, we can put them to use in a custom analyzer. For example, say you wanted to raise a diagnostic anytime a System.Exception was caught. As we covered in the Working with Types post, you need to capture the type you want to check from the Compilation engine. Since in this case we care about System.Exception, we will capture that from a CompilationStartAction.

context.RegisterCompilationStartAction((compileContext) =>
{
    var exceptionType = compileContext.Compilation.GetTypeByMetadataName("System.Exception");

Next we need to register our code to run on a SyntaxKind.CatchDeclaration.

compileContext.RegisterSyntaxNodeAction((symbolContext) =>
{
}, SyntaxKind.CatchDeclaration);

Inside the body of this action, we can check a few things. First, we should check that the declaration has a type. The main reasons it may not have a type is that the user is currently typing and they haven’t gotten there yet, or just that the document is in a non-compilable state. If we run into this scenario, we will just stop analyzing for now.

var errorSymbol = symbolContext.Node as CatchDeclarationSyntax;

if (errorSymbol.Type.IsMissing)
{
    return;
}

After that, we simply need to get the type of the variable from the semantic model and check it against the exceptionType we stashed away earlier.

var variableTypeInfo = symbolContext.SemanticModel.GetTypeInfo(errorSymbol.Type).ConvertedType as INamedTypeSymbol;

if (variableTypeInfo == null)
    return;

if (variableTypeInfo.Equals(exceptionType))
{
    symbolContext.ReportDiagnostic(Diagnostic.Create(Rule, errorSymbol.GetLocation()));
}

So, the full diagnostic is now:

context.RegisterCompilationStartAction((compileContext) =>
{
    var exceptionType = compileContext.Compilation.GetTypeByMetadataName("System.Exception");
    compileContext.RegisterSyntaxNodeAction((symbolContext) =>
    {
        var errorSymbol = symbolContext.Node as CatchDeclarationSyntax;

        if (errorSymbol.Type.IsMissing)
        {
            return;
        }

        var variableTypeInfo = symbolContext.SemanticModel.GetTypeInfo(errorSymbol.Type).ConvertedType as INamedTypeSymbol;

        if (variableTypeInfo == null)
            return;

        if (variableTypeInfo.Equals(exceptionType))
        {
            symbolContext.ReportDiagnostic(Diagnostic.Create(Rule, errorSymbol.GetLocation()));
        }
    }, SyntaxKind.CatchDeclaration);
});

At this point, we have a fully functioning analyzer that raises a diagnostic anytime a general exception is caught. While this is nice, there is also the scenario where there is no CatchDeclaration specified, i.e. the user just has a catch keyword on a line. For these scenarios, we can register a SyntaxNodeAction for a SyntaxKind.CatchClause and check to see if a declaration exists. If no declaration exists, then we can raise the diagnostic.

compileContext.RegisterSyntaxNodeAction((symbolContext) =>
{
    var errorSymbol = symbolContext.Node as CatchClauseSyntax;

    if (errorSymbol.Declaration == null)
    {
        symbolContext.ReportDiagnostic(Diagnostic.Create(Rule, errorSymbol.CatchKeyword.GetLocation()));
        return;
    }
}, SyntaxKind.CatchClause);

As you can see, the code analysis tools in Roslyn make it easy to write an analyzer that will help nudge your team along to use exceptions correctly (for various definitions of correct :) ).

Image Credit: Andrei Niemimäki via Creative Commons

1 2 3 7