Learn Roslyn Now: Part 10 Introduction to Analyzers

Roslyn analyzers allow companies and individuals to enforce certain rules within a code base. My understanding is that there are two primary uses for analyzers:

  • Broadly enforce coding styles and best practices
  • Specifically guide individuals consuming a library

The first use is largely a replacement for tools like StyleCop and FxCop. We can use analyzers to enforce stylistic choices like “All private variables must start with a lowercase letter” and “Use spaces not tabs”. In fact, you can start using StyleCop.Analyzers today. From a NuGet command line simply use:

Install-Package StyleCop.Analyzers -Pre

The second use is to release library specific analyzers meant to guide consumers of your library. For example, we might want to ensure that no one does the following:


var dateTime = System.DateTime.UtcNow;
dateTime.AddDays(1);

System.DateTime is immutable, so the above code is misleading. Instead the user should have written the following:


var dateTime = System.DateTime.UtcNow;
dateTime = dateTime.AddDays(1);

view raw

NiceFix.cs

hosted with ❤ by GitHub

Analyzers allow library authors to help guide their users. In that sense, I hope that it becomes standard to release a set of analyzers alongside new libraries. It’s difficult to say if this will actually happen, as it requires extra work from library authors.

Download the Roslyn SDK Templates

The templates do not ship with Visual Studio 2015. To install them go to:

Tools > Extensions and Updates > Online.

Search for “Roslyn SDK” and find the templates that correspond to your version. I’m using Visual Studio 2015 RC. I’ve chosen the package selected below:

RoslynSDKTemplates

After installing the templates, you must restart Visual Studio.

Creating your first analyzer

Navigate to:

File > New Project > Extensibility > Analyzer with Code Fix

NewProjectAnalyzer
Give your analyzer a name and click “OK”. I’ve taken the creative liberty of naming mine "Analyzer1". From here we’re presented a README that explains that building our project creates both a .vsix for Visual Studio and a .nupkg for submission to NuGet. There are also instructions on how to properly distribute your analyzer as a NuGet package.

Let’s take a look at what we’re given right out of the box:

StartingProjects

We’re given three projects:

  • Analyzer1 – The brain of our analyzer. This is where all code analysis is done and code fixes are figured out.
  • Anylzer1.Test – A default test project with some helper classes to make testing easier.
  • Analyzer.Vsix – The startup project that will be deployed to Visual Studio. The .vsixmanifest tells Visual Studio that you’d like to export an analyzer and a code fix.

To run the project, simply press F5. A new instance of Visual Studio will launch. This Visual Studio is called the Experimental Hive and has its own set of settings within the Windows Registry. Note: It’s a good practice to choose a different theme for your Experimental Hive so you don’t get them mixed up.

Once you open a solution, you’ll notice Visual Studio complaining about a lot of new warnings. The analyzer we’re running simply creates a warning when it sees any type with lowercase letters in its name. It’s obviously not very useful, but allows us to also demonstrate the code fix included in this sample:

Now that we’ve got a rough idea of what each project is for, we’ll explore Analyzer1 and what we’re given for free.

DiagnosticAnalyzer.cs

The first thing to notice is that our Analyzer inherits from the abstract class DiagnosticAnalyzer. This class expects us to do two things:

Let’s take a look at the properties and fields in the first half of the file:


[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class Analyzer1Analyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "Analyzer1";
// You can change these strings in the Resources.resx file. If you do not want your analyzer to be localize-able, you can use regular strings for Title and MessageFormat.
internal static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.AnalyzerTitle), Resources.ResourceManager, typeof(Resources));
internal static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.AnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources));
internal static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.AnalyzerDescription), Resources.ResourceManager, typeof(Resources));
internal const string Category = "Naming";
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); } }
...
}

It may seem overwhelming at first, but bear with me. First notice the DiagnosticAnalyzer attribute applied to the class. This specifies what language or languages our analyzer will be run on. Today, you can only specify C# and VB .Net.

Looking within the class, the first five properties are simply strings to describe our analyzer and provide messages to users list. By default, the analyzer is set up to encourage localization and allows you define your title, message format and description as localizable strings. However if localization scares you like it does me, you make them simple strings.

Take a moment to look at DiagnosticDescriptor Rule. It defines a DiagnosticSeverity  of “Warning”. I suspect you’ll likely want to stick with Warning, but if you feel like imposing on consumers of your analyzer, you could upgrade the severity to Error and prevent compilation completely. Note: I don’t recommend this. If your analyzer misbehaves and reports errors where there are none, the user will remove it.

Finally, lets take a look at the two generated methods:


[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class Analyzer1Analyzer : DiagnosticAnalyzer
{
...
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);
}
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);
}
}
}

view raw

SecondHalf.cs

hosted with ❤ by GitHub

The Initialize() method sets up the analyzer by registering the AnalyzeSymbol method to fire when semantic analysis has been run on a NamedType symbol. This is only one example out of a handful of ways to trigger an analyzer. We can register our analyzer to run on various triggers including compilation, analysis of codeblocks and analysis of syntax trees. We’ll flush out AnalysisContext in further posts.

The AnalyzeSymbol() method is where we actually do the analysis we’ve been talking about. This is where we would use the Syntax Tree and Symbol APIs to diagnose and report issues. In the case of this analyzer it simply takes the INamedTypSymbol provided and checks whether any of the characters in its name are lowercase. If they are, we report this diagnostic using the Rule we defined earlier.

This may seem like an awful lot of boilerplate for such a simple analyzer. However, once you start building complicated analyzers, you’ll find that the analysis code quickly starts to dominate and that the boilerplate isn’t so bad.

Next time, we’ll explore the CodeFixProvider and how we can offer solutions to problems we find in a user’s code.

5 thoughts on “Learn Roslyn Now: Part 10 Introduction to Analyzers

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s