Writing a simple analyzer using Roslyn – part 1
But before proceeding to the discussion of the examples, let's look at what we may need them for.
What Do We Need Our Own Analyzers For?
The question is quite reasonable. There are "jet brains", there is DevExpress, there are also microsoft guys from DevDiv who create tools for developers. Why should I deal with all sorts of immutable syntax trees and control flow analysis? It's pretty fun, but is this really enough to spend my valuable time on it?
Any general purpose tool is designed to solve the most typical problems. Just open the list of Resharper analyzers and you will understand what I mean. These analyzers will cope perfectly with the search of unreachable code, or with warning about incorrect singleton implementation, but they will not "tell" you about the rules specific to your project and/or library.
For example, you may want to respond more rigidly to incorrect exception logging (detect it and "punish" if your logging method receives ex.Message instead of ex.ToString()), or you may want to create a custom rule prohibiting the use of LINQ in certain assemblies in order to avoid performance loss. Or maybe your team has a rule or a set of rules to be followed by all members of the team, which can not be expressed in the form of FxCop/StyleCop rules. All these problems will be solved perfectly with the help of your own analyzers.
Methods of Analyzer Distribution
Once you decide what exactly you want to analyze in the development environment, you have to solve the problem of analyzer distribution. There are three methods:
- By installing VSIX
- By using NuGet packages
- By explicitly adding the analyzer through Analyzers -> Add Analyzer
The first method will allow you to define a "global" analyzer that will work for all projects, and the last two will allow the use of analyzers that are specific to a particular project.
The first method is most suitable for general purpose analyzers similar to Resharper rules. Analyzers based on NuGet packages allow the use of the same set of rules by all members of the team (including the build server). Since custom analyzers are no different from compile errors, using them on a build server allows you to "break" a build if the code suddenly ceases to follow certain rules.
The good news is that the first two approaches are not mutually exclusive, and you can put your favorite set of analyzers on your machine, and assign them to specific projects.
The First Analyzer. Task Formulation
As an exercise, let's write the following analyzer. Suppose we have a library MvvmUltraLight with only one structure RelayCommand:
Structures in C# do not play along nicely with OOP, but they can help in terms of performance, because they do not cause memory allocation on the heap. Our task is to write an analyzer which will issue a warning about the use of a default constructor for a given structure.
NOTE
This example, like many synthetic examples, is somewhat flimsy. But if it will be easier for you, instead of the RelayCommand structure it could be a List<T>.Enumerator, or another custom structure, the use of a default constructor for which it does not make sense, or leads to a runtime error. For example, the following results in NullReferenceException: new List .Enumerator().MoveNext().
Basic Structure of Analyzers
To create your own analyzer it is enough to create a new project: File -> New -> Project -> Analyzer with Code Fix (NuGet + VSIX), after which an empty analyzer project (plus a NuGet package), a project with tests, and a project with VSIX will be created. "Empty" project will contain a simple analyzer and a fixer (a class that fixes the problem detected by the analyzer).
Key classes are shown in the following figure (given that our analyzer is called DoNotUseDefaultCtorAnalyzer, and fixer - UseNonDefaultCtorCodeFixProvider).
Each analyzer must contain an identifier, DiagnosticDescriptor (which in turn consists of ID, name, formatted message, and message level - Warning/Error). And a fixer should return a list of analyzers, the issues of which it is ready to solve.
Constructor Call Analysis
Let’s start developing our own analyzer. For starters, we need to create DiagnosticDescriptor with the necessary information:
Now we need to redefine the Initializer method and register a specific callback method for handling certain nodes of the syntax tree (you can register a lot of things there, but let’s talk about them some other time). To understand which node of the syntax tree is the one we need in this case, you can use Roslyn Syntax Tree Visualizer or the 5th version of LINQ Pad, in which this functionality is built in. To search for a correct type of node, simply open LINQ Pad and punch in the expression var cmd = new RelayCommand():
Ok, we need to handle ObjectCreationExpression. Let’s register the required handler and create the first implementation:
This implementation is very primitive. We just check that an instance of the desired command is created, and that the number of constructor arguments equals 0. Here a type check is carried out at the "syntax" level - by checking whether the type of the created instance contains the text "RelayCommand". Next, we will see how to create this check in a more adequate way.
That’s all; our analyzer is ready and you can start testing. Since our solution already contains a project with tests (and a class with tests of "embedded" analyzer), writing the test will be simple enough:
VerifyCSharpDiagnostic methods are already in the created project with unit tests, and our main task is to choose the correct coordinates for the object DiagnosticResultLocation.
In the second part of the article we will look at how to write a Fixer and about Semantic Information and Correct Type Search.
Looking to upgrade your IT&C skills? Check out our trainings.
Sergey Teplyakov
Expert in .Net, С++ and Application Architecture