nrk.no

Improved developer experience with Roslyn Code Analyzers

Kategori: Dev

Tools
Photo by Fleur Treurniet on Unsplash

Lately I have been experimenting with Roslyn Code Analyzers and Code Fixes with Einar W. Høst, and we have found it to be a very nice addition to unit tests and static code analysis with NDepend. What I particularly like is the immediate developer feedback and possibility to add code suggestions for other developers.

tl;dr

In short, Code Analyzers let’s you add errors, warnings or squigglies directly in Visual Studio, with tooltips. You can enhance those with Code Fix providers, which will enable code refactoring using the standard shortcuts (ctrl+b for quick actions).

Real life usage scenario

Our current use case is a HAL NuGet package we are working on. We provide some base classes for enabling HAL in our API contracts, making it easy for the package consumers to follow the HAL specification. However, it is still fully possible to break the HAL standard, or to break deserialization. We could always detect that using static analysis in our CI pipeline, or hope that we would catch such mistakes by code reviews. However, adding our rules as Code Analyzers with Code fixes provides instant and friendly developer feedback.

Let’s look at one specific use case, we have a class HalResource where TLinks : HalLinks. So the developers can inherit from that in their model, and then make another class implementing HalLinks. Having statically typed HalLinks provides us with two benefits:

  • We can guarantee the types of links presented to the consumer
  • We can generate documentation for these links using Swagger

One of the premises for this to work is that all public properties on classes deriving from HalLinks must be of type HalLink or IEnumerable<HalLink>. Enforcing this can be difficult, but this is something which we can quite easily do with Roslyn Code Analyzers and Code Fixes.

Let’s take a look at an example where I add a property of type string instead of HalLink. Notice the squigglies under string.

When I place my marker on the squigglies, I get further help:

And when I press ctrl+. for quick actions I get a preview of how the code will look after I apply the defined code fix.

A really nice thing to remember is that we can distribute our analyzers and fixes within the NuGet package so it is automatically enabled for any developer using our package.

How to create your own

The easiest way to get started is to create a new project from and go from there.

If the Analyzer with Code Fix template is not there, try installing the .NET Compiler Platform SDK from Visual Studio Marketplace – currently found here.

This will create three new projects, an analyzer project, a test project for your analyzers and a VSIX project. We don’t really use the VSIX project except when we wish to try our code analyzer and fix behaves in Visual Studio. For rapid development, the unit testes tends to be sufficient.

In the Analyzer project you’ll find a sample DiagnosticAnalyzer and a CodeFixProvider. The fastest way to get started is to start playing with the DiagnosticsAnalyzer which is responsible for finding code. Unless you wish to create a code action to assist with refactoring, this is all you need to care about.

DiagnosticsAnalyzer

Writing a diagnostics analyzer is a fairly straight forward task, the main steps required are:

  • Inherit from DiagnosticAnalyzer
  • Create a DiagnosticsDescriptor (called Rule in the template)
  • Override SupportedDiagnostics returning the DiagnosticsDescriptor
  • Override Initialize and call RegisterSymbolAction on the context
  • Implement the Symbol action, calling ReportDiagnostics with a Diagnostic create from the DiagnosticDescriptor above.

The Symbol action is where you will need to write your logic for evaluating the source code. Here’s a sample on how you can locate properties without private setters

CodeFixProvider

Writing a codefix provider is not much different, except we’ll be manipulating the code tree instead of just analyzing it.

  • Inherit from CodeFixProvider
  • Override FixableDiagnosticIds, the id’s returned should match the Id in the DiagnosticsDescriptor from your analyzer.
  • Override GetFixAllProvider() which enables fixing multiple instance of this error at once. More details here
  • Override RegisterCodeFixesAsync(CodeFixContext context) and call context.RegisterCodeFix.
  • Implement your fix.

Here’s an example on how to change a property setter to private. Notice that the Roslyn api’s for working with code are immutable (yay).

Executing and testing your provider

When you create an Analyzer + Fix project, you get a Unittest and VSIX project as well. The Vsix can be used to fire up a new solution with your Analyzer project loaded. This is however somewhat tedious and I much prefer the unit test approach. The unit tests are quite simple to write as the Test project mostly contains what you need.

Writing a new unit test can be done by

  • Creating a new class, intherit from CodeFixVerifier
  • Override GetCSharpDiagnosticAnalyzer() and return your own DiagnosticAnalyzer implementation
  • Override GetCSharpCodeFixProvider() and return your own CodeFixProvider implementation
  • For Analyzer a test can be written as such
  • Testing a fix follows the same pattern and can be done using VerifyCSharpFix(string code, string code) which will compare input with output.

Lessons learned

Deployment

We usually distribute our code fixes and analyzers with other libraries, and by adding them to the right place in the nuget package they are automatically picked up by Visual Studio 2017. The C# analyzers should be located in analyzers/dotnet/cs. If all your users use Visual Studio 2017, you can remove the tools folder and powershell scripts created by the Analyzer with Code Fix project template.

Testing

We often scope our tests to a specific namespace or to classes implementing an interface or inheriting from a base class. Running the tests will compile the code on the fly in an empty solution, hence it has no knowledge of our interface or class and it will result in a compilation error and thus not apply our analyzer. To remedy this we have slightly modified the generated test helpers DiagnosticsVerifier.cs with a new method to override, and DiagnosticsVerifierHelper.cs where we include our references as a method parameter to the GetSortedDiagnosticsFromDocuments and then add them to the compilation as shown below.

One will then need to update all calls to the GetSortedDiagnosticsFromDocuments with GetReferences() as the last parameter. This enables us to include dll’s we wish to reference in the compilation of the code in the unit tests by overriding GetReferences() in the test class and adding a reference to the dll’s we need to compile the test code for the Analyzer project.

For more documentation on Roslyn and Code Analyzers/Fixes I can recommend checking out https://github.com/dotnet/roslyn/tree/master/docs

When and where?

Roslyn Code fixes and Analyzers are powerful tools when you have more than a handfull developers or external consumers. And it fits really good with APIs provided as NuGet packages or to protect the domain model.

The first few providers will take some time to write, but as you get more comfortable working with the semantic model and syntax tree, writing new ones are quite simple. We also see that we quickly build extension methods and libraries for common checks and actions.

1 kommentar

  1. Hi Harald

    Great article. Roslyn analyzers and codefixes are an amazing tool that boots productivity and it’s also easy to develop. I even created my own set of codefix and refactoring that simulate Automapper in design time. The project is available at Github github.com/cezarypiatek/MappingGenerator

    In terms of Unit testing for analyzer, I prefer using github.com/phoenix172/RoslynNUnitLight.NetStandard instead of default infrastructure. It’s much easier to use and allows also to test CodeRefactorings.

    Svar på denne kommentaren

Legg igjen en kommentar til Cezary Piątek Avbryt svar

Din e-postadresse vil ikke bli publisert. Obligatoriske felt er merket med *. Les vår personvernserklæring for informasjon om hvilke data vi lagrer om deg som kommenterer.