Writing and distributing Roslyn analyzers with MyGet

Pretty sweet: MyGet just announced Vsix support has been enabled for all MyGet customers! I wanted to work on a fun example for this new feature and came up with this: how can we use MyGet to build and distribute a Roslyn analyzer and code fix? Let’s see.

Developing a Roslyn analyzer and code fix

Roslyn analyzers and code fixes allow development teams and individuals to enforce certain rules within a code base. Using code fixes, it’s also possible to provide automated “fixes” for issues found in code. When writing code that utilizes DateTime, it’s often best to use DateTime.UtcNow instead of DateTime.Now. The first uses UTC timezone, while the latter uses the local time zone of the computer the code runs on, often introducing nasty time-related bugs. Let’s write an analyzer that detects usage of DateTime.Now!

You will need Visual Studio 2015 RC and the Visual Studio 2015 RC SDK installed. You’ll also need the SDK Templates VSIX package to get the Visual Studio project templates. Once you have those, we can create a new Analyzer with Code Fix.

image_thumb[2]

A solution with 3 projects will be created: the analyzer and code fix, unit tests and a Vsix project. Let’s start with the first: detecting DateTime.Now in code an showing a diagnostic for it. It’s actually quite easy to do: we tell Roslyn we want to analyze IdentifierName nodes and it will pass them to our code. We can then see if the identifier is “Now” and the parent node is “System.DateTime”. If that’s the case, return a diagnostic:

private void AnalyzeIdentifierName(SyntaxNodeAnalysisContext context) { var identifierName = context.Node as IdentifierNameSyntax; if (identifierName != null) { // Find usages of "DateTime.Now" if (identifierName.Identifier.ValueText == "Now") { var expression = ((MemberAccessExpressionSyntax)identifierName.Parent).Expression; var memberSymbol = context.SemanticModel.GetSymbolInfo(expression).Symbol; if (!memberSymbol?.ToString().StartsWith("System.DateTime") ?? true) { return; } else { // Produce a diagnostic. var diagnostic = Diagnostic.Create(Rule, identifierName.Identifier.GetLocation(), identifierName); context.ReportDiagnostic(diagnostic); } } } }

If we compile our solution and add the generated NuGet package to another project, DateTime.Now code will be flagged. But let’s implement the code fix first as well. We want to provide a code fix for the syntax node we just detected. And when we invoke it, we want to replace the “Now” node with “UtcNow”. A bit of Roslyn syntax tree fiddling:

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 "Now" var identifierNode = root.FindNode(diagnosticSpan); // Register a code action that will invoke the fix. context.RegisterCodeFix( CodeAction.Create("Replace with DateTime.UtcNow", c => ReplaceWithDateTimeUtcNow(context.Document, identifierNode, c)), diagnostic); } private async Task<Document> ReplaceWithDateTimeUtcNow(Document document, SyntaxNode identifierNode, CancellationToken cancellationToken) { var root = await document.GetSyntaxRootAsync(cancellationToken); var newRoot = root.ReplaceNode(identifierNode, SyntaxFactory.IdentifierName("UtcNow")); return document.WithSyntaxRoot(newRoot); }

That’s it. We now have an analyzer and a code fix. If we try it (again, by adding the generated NuGet package to another project), we can see both in action:

image_thumb[6]

Now let’s distribute it to our team!

Distributing a Roslyn analyzer and code fix using MyGet

Roslyn analyzers can be distributed in two formats: as NuGet packages, so they can be enabled for individual project, and as a Visual Studio extension so that all projects we work with have the analyzer and code fix enabled. You can build on a developer machine, a CI server or using MyGet Build Services. Let’s pick the latter as it’s the easiest way to achieve our goal: compile and distribute.

Create a new feed on www.myget.org. Next, from the Build Services tab, we can add a GitHub repository as the source. We’ve open-sourced our example at https://github.com/myget/sample-roslyn-with-vsix so feel free to add it to your feed as a test. Once added, you can start a build. Just like that. MyGet will figure out it’s a Roslyn analyzer and build both the NuGet package as well as the Visual Studio extension.

image_thumb[9]

Sweet! You can now add the Roslyn analyzer and code fix per-project, by installing the NuGet package from the feed (https://www.myget.org/F/datetime-analyzer/api/v2). ANd when registering it in Visual Studio (https://www.myget.org/F/datetime-analyzer/vsix/) by opening the Tools | Options... menu and the Environment | Extensions and Updates pane, you can also install the full extension.

image_thumb[12]

This is an imported post. It was imported from my old blog using an automated tool and may contain formatting errors and/or broken images.

Leave a Comment

avatar

8 responses

  1. Avatar for David De Sloovere
    David De Sloovere May 8th, 2015

    I had seen the analyzers in a demo, but didn't know that these could also be installed as extension to Studio instead of nuget package per project.
    Great post.

  2. Avatar for Maarten Balliauw
    Maarten Balliauw May 8th, 2015

    You're welcome!

  3. Avatar for Lior Tal
    Lior Tal May 14th, 2015

    I realize this is just an example, but is it possible that you have another DateTime type with a Now identifier on it, and that will be flagged as an error? Also, are there any "built-in" analyzers and fixes that are available as part of VS2015 ? (is their source available to learn from?)

  4. Avatar for Maarten Balliauw
    Maarten Balliauw May 14th, 2015

    There are lots of examples available, check these:

    https://msdn.microsoft.com/...

    https://github.com/Wintelle...

  5. Avatar for Michael Lund
    Michael Lund May 21st, 2015

    If you write System.DateTime.Now the analyzer crashes due to an invalid cast. Is there a way to know that the "Now" identifier is a property on the type System.DateTime no matter if you write DateTime or System.DateTime?
    It seems wrong to have to handle these two scenarios separately.
    Further: We really only want to apply the fix if the type is System.DateTime and not SomeOtherNamespace.DateTime.

    What to do?

  6. Avatar for Michael Lund
    Michael Lund May 21st, 2015

    Ok - I looked at https://msdn.microsoft.com/... - It seems I want to do something like

    var memberSymbol = context.SemanticModel.GetSymbolInfo(expression).Symbol as IMethodSymbol;

    and

    if (!memberSymbol?.ToString().StartsWith("System.DateTime.Now") ?? true) return;

  7. Avatar for Maarten Balliauw
    Maarten Balliauw May 21st, 2015

    Correct. I was about to send the same link. Also updated code on GitHub+ this blog post.

  8. Avatar for Rashi Thakkar
    Rashi Thakkar September 3rd, 2015