Using Roslyn Analyzers for static code analysis in software development projects

Using Roslyn Analyzers for static code analysis in software development projects

Richard Brown

30 September 2022 - 10 min read

Code
Using Roslyn Analyzers for static code analysis in software development projects

Code quality is extremely important when developing anything but the simplest software system. Poor quality code can be hard to maintain and, if left unchecked, lead to serious issues with software delivery.

Consistent code review is, therefore, an important part of any software development project. However, a manual code review process can be time consuming. If you're spending all your time in the code checking for correct casing and other syntax errors, then you’ll lack the time to check what's really important about that code.

By using a static code analysis tool you can automate a lot of those more mundane, standard checks so that code reviews can focus less on syntax and more on those higher level areas. All things that are much harder to automate but add a lot more value during the code review process.

At Audacia we use Roslyn Analyzers to automate some of our code review processes and ensure that we are consistently achieving our coding standards. This article will provide an overview of Roslyn Analyzers, including how to configure these analyzers, as well as looking at how we benefit from these tools at Audacia.

What is Static Code Analysis in Software Development?

Static code analysis runs at code compilation, and is carried out by analysing source code against a set (or multiple sets) of rules. ReSharper, for example, will tell you when you’re violating rules that the tool thinks are important.

The use of these tools can help businesses automate the review of code in accordance with coding standards, thereby improving the efficiency of their code review process. Businesses are able to maintain code quality more proactively and test code quality more efficiently.

In a language like C#, there are certain conventions around how classes and methods are named. For example, a class, method or property are named in Pascal case (upper-case first letter), whereas parameters and variables are named in Camel case (lower-case first letter).

Individually checking all of these conventions during pull requests would be highly time consuming.

If you’re spending all of your time during a code review seeing whether they’ve used the right type of case and all of those things, then you’re probably not going to have the time to check what’s really important with that code. Is the behaviour that’s implemented correct? Are there any bugs?

Roslyn

Roslyn is the name of the C# (and VB) compiler, which is developed primarily by Microsoft as an open source project. The platform has been the default C# compiler since Visual Studio 2015 and is actually written in C# itself — in contrast to the first generation of C# compiler which was written in C++.

As well as compiling source code, it exposes a set of APIs that allow custom code to interact with the compiler. This is where Roslyn analyzers come in.

Roslyn is designed with an API surface that allows third parties to write code that hooks into certain compiler events and can carry out custom analysis of C# code.

Roslyn Analyzers

Roslyn Analyzers are a tool that analyses your code with regard to styling, design and other issues. The tool runs analysis on your code as you type, and reports diagnostics in the editor. In practice, this means that the compiler is constantly reaching and evaluating different parts of the syntax tree.

Using their own APIs, Roslyn Analyzers verifies certain conditions about the source code and, if necessary, feeds back into the compiler in the form of compilation warnings and errors. An example would be StyleCop.

Configuring Analyzers

Developers can get granular control over how their code is being analysed by configuring Roslyn analyzers. One way this can be achieved is by setting the severity of rules. As an author of an analyzer, you are able to define the severity of the diagnostic that is created during compilation when something violates a rule, e.g. warning or suggestion.

Analyzer ‘rules’ can be written using the Roslyn SDK, with each rule specifying:

  • A Rule ID - for example StyleCop analyzers all have IDs starting 'SA', e.g. 'SA1201'
  • The actual logic of the rule, i.e. some analysis of source code and checks as to whether the source code meets the requirements of the rule
  • Optionally a code fix, which instructs Roslyn as to what code changes should be made to fix the violation.

All of these events are things that you can configure as a developer. For example, you might want to make sure that methods have been named correctly or that you don’t exceed a certain amount of parameters in your code.

By writing some custom code, you can instruct Roslyn to analyse your code in conjunction with preset parameters.

Developers can also override the analyzer. Overriding an analyzer can be done at various levels and in different ways.

A lot of this configuration is done via .editorconfig.

# Multiple enum values are placed on the same line of code.
dotnet_diagnostic.SA1136.severity = none

# Two sibling elements which each start on their own line have different levels of indentation.
dotnet_diagnostic.SA1137.severity = suggestion

# Use literal suffix notation instead of casting.
dotnet_diagnostic.SA1139.severity = warning

Implementing Analyzers

For authors of Analyzers, there are a few requirements that need to be considered when implementing. Every analyzer has a title and a message, which can be a formatted string that allows the developer to provide dynamic content. Other key elements are:

DiagnosticDescriptor: This is an object that contains the metadata with information about the analyzer.

DiagnosticId: Every analyzer needs an ID, which will show when a rule is violated.

All of these areas will show up in either Visual Studio, Rider, VS Code, or Build Output. These areas will show up to tell the developer what has gone wrong.

Each rule can be associated with a category, such as Naming, Performance and Style.

Then you can specify a severity. A default severity to assign when the rule is violated, with one of the following values:

  • Hidden
  • Info
  • Warning
  • Error

The first part of this process involves overriding the Initialize method. This is what will be called by a compiler when it's starting up and when it's registering all of the analyzers.

public override void Initialize(AnalysisContext context)
{
    context.EnableConcurrentExecution();
    context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
    context.RegisterSyntaxNodeAction(AnalyzeVariable, SyntaxKind.LocalDeclarationStatement);
}

Roslyn then goes on a discovery phase, where it will try to find anything that's decorated with this diagnostic analyzer attribute. Upon finding this code, the compiler will call its initialise method. The method can then tell Roslyn when this analyzer should actually be fired during the compilation process.

During this process there are a few default areas that are added:

  • ConfigureGeneratedCodeAnalysis: Generated code can be treated differently. For example, an entity framework migration would be classified as generated code because it might violate your own coding standards, but you wouldn’t necessarily want this to then break your build because you can't actually do very much to control that.
  • EnableConcurrentExecution: The same analyzer can register multiple actions so you can have it triggering for different areas of the syntax tree. Enabling concurrent execution allows this process to happen in parallel which allows for greater efficiency during the process overall.

How We Use Roslyn Analyzers in software development projects

Audacia has found Roslyn Analyzers to be an invaluable tool for automating routine code checks and, therefore, maintaining our coding standards.

For example, one of our custom analyzers is FieldWithUnderscoreAnalyzer. One of our coding standards is that private fields should be prefixed with an underscore. Given that is one of our standards, we have an Analyzer that automates the checking of this standard.

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class FieldWithUnderscoreAnalyzer : DiagnosticAnalyzer
{
    public const string Id = DiagnosticId.FieldWithUnderscore;
    
    private const string Title = "Private field not prefixed with an underscore";
    private const string MessageFormat = "Field '{0}' is not prefixed with an underscore.";
    private const string Description = "Private fields should be prefixed with an underscore.";
    
    private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(Id, Title, MessageFormat, DiagnosticCategory.Naming, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);
    
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
    
    public override void Initialize(AnalysisContext context)
    {
        context.EnableConcurrentExecution();
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.RegisterSymbolAction(AnalyzeField, SymbolKind.Field);
    }
    
    private static void AnalyzeField(SymbolAnalysisContext context)
    {
        var field = (IFieldSymbol)context.Symbol;
        if (!ShouldAnalyze(field))
        {
            return;
        }
        
        if (!field.Name.StartsWith("_", StringComparison.Ordinal))
        {
            var diagnostic = Diagnostic.Create(Rule, field.Locations[0], field.Name);
            context.ReportDiagnostic(diagnostic);
        }
    }
    
    private static bool ShouldAnalyze(IFieldSymbol field)
    {
        if (field.IsStatic && field.IsReadOnly)
        {
            return false;
        }
        
        if (field.IsConst)
        {
            return false;
        }
        
        if (field.DeclaredAccessibility == Accessibility.Public ||
            field.DeclaredAccessibility == Accessibility.Internal)
        {
            return false;
        }
        
        return true;
    }
}

Here the analyzer registers a symbol action rather than syntax node action. The analyzer is checking whether something is a field symbol and whether it should be analysed accordingly.

We don't want static readonly, const, public or internal fields to be prefixed, so any of those kind of fields are ignored by the analyzer. Otherwise the analyzer will check that the field name starts with an underscore.

We also have a code fix provider that automates the insertion of that underscore. As with the analyzers, you need to inherit from a certain class and decorate it with a certain attribute. Once you've registered this code fix, Roslyn will run your code. From here you’ll receive a syntax token that can be used to interpolate that string and prefix it with an underscore. Then you can feed it back into your source code tree.

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(FieldWithUnderscoreCodeFixProvider)), Shared]
public sealed class FieldWithUnderscoreCodeFixProvider : CodeFixProvider
{
    public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(FieldWithUnderscoreAnalyzer.Id);
    
    public sealed override FixAllProvider GetFixAllProvider()
    {
        return WellKnownFixAllProviders.BatchFixer;
    }
    
    public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
    {
        var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
        var diagnostic = context.Diagnostics.First();
        var token = root.FindToken(diagnostic.Location.SourceSpan.Start);
        context.RegisterCodeFix(
            CodeAction.Create("Prepend '_' to field", c => PrependUnderscore(context.Document, token, c), FieldWithUnderscoreAnalyzer.Id),
            diagnostic);
    }
    
    private async Task<Solution> PrependUnderscore(Document document, SyntaxToken declaration, CancellationToken cancellationToken)
    {
        var newName = $"_{declaration.ValueText}";
        var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
        var symbol = semanticModel.GetDeclaredSymbol(declaration.Parent, cancellationToken);
        var solution = document.Project.Solution;
        
        return await Renamer.RenameSymbolAsync(solution, symbol, newName, solution.Workspace.Options, cancellationToken).ConfigureAwait(false);
    }
}

By writing some code, we're able to automate code fixes. We find this an effective way to efficiently manage specific coding standards.

Consistency in Code Analysis

This article has provided an insight into Roslyn Analyzers, highlighting some of their benefits, and illustrated how this tool can be used to provide consistency in your code anaylsis.

As we have discussed, Roslyn Analyzers are highly useful when it comes to not only flagging code errors, but also automating the process of code fixes. With the ability to also create custom configurations that can be tailored to your coding standards, Roslyn Analyzers have the capacity to significantly improve efficiency and ensure that team’s can focus on what really matters: building robust, well-designed systems.

Audacia is a software development company based in the UK, headquartered in Leeds. View more technical insights from our teams of consultants, business analysts, developers and testers on our technology insights blog.

Technology Insights

Ebook Available

How to maximise the performance of your existing systems

Free download

Richard Brown is the Technical Director at Audacia, where he is responsible for steering the technical direction of the company and maintaining standards across development and testing.