.NET Compiler Platform (Roslyn) - Build Analyzer And Code Fixes

Introduction

 
Most programmers treat compilers as black boxes. They write the source code as input, do some processing on it, and get the output as executable binaries. Source code processing consists of three main phases: syntax analysis, semantic analysis, and code generation. Although each phase outputs its own intermediate results and passes them as input to the next phase, these are just internal data structures that are not accessible outside the compiler.
 
In this article, I am going to demonstrate how to write your own compiler, (i.e. type of code analyzer and code fixer using the .NET compile platform called Roslyn), using the latest Visual Studio 2019 and .NET framework, which consists of syntax and semantic analysis. The code fixer will be the code generation. 
 
Background
 
The concept of Roslyn can be found in another article published by Mr. Mahesh.
 
Prerequisites
 
Most Roslyn API syntaxes have changed from VS2014 to VS2019. Hence, let's compile the build using VS 2019 with the latest framework. Before starting the new code analyzer, make sure you have installed the prerequisites of this article,  i. e. “.NET Compiler Platform SDK” and the same from the VS extension. These tools will be your faithful companions as you build your own applications and VS extensions on top of the .NET Compiler Platform APIs. 
 

Build Analyzer and Code Fix

 
Let's begin with creating code analyzers and code fixes step by step and see how to test those in your VS project and create the extension from it.
 
Step 1
 
Open VS 2019 and Select Roslyn or search “Analyzer with Code Fix” project template with C#, create a new project with this option.
 
 
Step 2
 
You will find three projects that are created as part of one solution; i.e. MainAnalyzeProject, UnitTestProject, and VsixProject to install this analyzer as an extension.
 
 
Step 3
 
Go to Your Main Analyzer project and you will find two important classes, one for Analyzer and another for code fix. Analyzer class is used for finding the diagnostic and code fix is used for fixing those diagnostics.
 
Let's understand these two main steps one by one in detail.
 

Code Analyzer

 
Step 1
 
Let's start with writing two new Diagnostic Rules.
  • Finding all the fields which start with an underscore.
  • Finding all blocks like if, for each, or those that don't have curly braces.
Step 2
 
Open the analyzer class which is derived from DiagnosticAnalyzer and define DiagnosticDescriptor which contains “Unique Id, title to show during analyze, message, category, severity, enable this rule by default description”.
  1. private static DiagnosticDescriptor StartUnderScoreRule = new DiagnosticDescriptor  
  2. (StartUnderScoreDiagnosticId, "name starts with underscore",   
  3. "the name of field {0} start with an underscore""naming", DiagnosticSeverity.Warning, true);  
Step 3
 
The next step is to register the type of Action with the kind of property/syntax (Symbol, Syntax, compilation, etc.) in the Initialize method.
  1. context.RegisterSymbolAction(AnalyzeField, SymbolKind.Field);  
Step 4
 
The next step is to implement a delegate that was associated with registration of the type. This delegate will be triggered during the analysis of each class. 
  1. private static void AnalyzeField(SymbolAnalysisContext context)  
  2.        {  
  3.            var namedTypeSymbol = (IFieldSymbol)context.Symbol;  
  4.   
  5.            // Find just those named type symbols with names starting with _.  
  6.            if (namedTypeSymbol.Name.StartsWith("_") && namedTypeSymbol.Name.Length > 1)  
  7.            {  
  8.                // For all such symbols, produce a diagnostic.  
  9.                var diagnostic = Diagnostic.Create(StartUnderScoreRule, namedTypeSymbol.Locations[0], namedTypeSymbol.Name);  
  10.                context.ReportDiagnostic(diagnostic);  
  11.            }  
  12.        }  
Step 5
 
Define all other rules in this class with Unique RuleID, DiagnosticDescriptor and register it. Here is the full implementation of all rules defined in CodeAnalyzer to find all diagnostics and provide to codeFixProvider. 
  1. [DiagnosticAnalyzer(LanguageNames.CSharp)]  
  2.     public class CSharpeStyleCopAnalyzer : DiagnosticAnalyzer  
  3.     {  
  4.         public const string LowerCaseDiagnosticId = "LowerCase";  
  5.         public const string StartUnderScoreDiagnosticId = "StartUnderScore";  
  6.         public const string CurlyBraceDiagnosticId = "CurlyBrace";  
  7.   
  8.         private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.AnalyzerTitle), Resources.ResourceManager, typeof(Resources));  
  9.         private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.AnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources));  
  10.         private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.AnalyzerDescription), Resources.ResourceManager, typeof(Resources));  
  11.           
  12.         private static DiagnosticDescriptor LowerCaseNameRule = new DiagnosticDescriptor(LowerCaseDiagnosticId, Title, MessageFormat, "naming", DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);  
  13.         private static DiagnosticDescriptor StartUnderScoreRule = new DiagnosticDescriptor(StartUnderScoreDiagnosticId, "name starts with underscore""the name of field {0} start with an underscore""naming", DiagnosticSeverity.Warning, true);  
  14.         private static DiagnosticDescriptor SyntaxRule = new DiagnosticDescriptor(CurlyBraceDiagnosticId, "Add curly braces""the block {0} does not contains curly braces""syntax", DiagnosticSeverity.Warning, true);  
  15.   
  16.         public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(LowerCaseNameRule, StartUnderScoreRule, SyntaxRule); } }  
  17.   
  18.         public override void Initialize(AnalysisContext context)  
  19.         {  
  20.             context.RegisterSymbolAction(AnalyzeField, SymbolKind.Field);    
  21.             context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.IfStatement, SyntaxKind.ForEachStatement);  
  22.         }  
  23.   
  24.         private static void AnalyzeField(SymbolAnalysisContext context)  
  25.         {  
  26.             var namedTypeSymbol = (IFieldSymbol)context.Symbol;  
  27.   
  28.             // Find just those named type symbols with names starting with _.  
  29.             if (namedTypeSymbol.Name.StartsWith("_") && namedTypeSymbol.Name.Length > 1)  
  30.             {  
  31.                 // For all such symbols, produce a diagnostic.  
  32.                 var diagnostic = Diagnostic.Create(StartUnderScoreRule, namedTypeSymbol.Locations[0], namedTypeSymbol.Name);  
  33.                 context.ReportDiagnostic(diagnostic);  
  34.             }  
  35.         }  
  36.   
  37.         private static void AnalyzeSyntax(SyntaxNodeAnalysisContext context)  
  38.         {  
  39.             var body = default(StatementSyntax);  
  40.             var token = default(SyntaxToken);  
  41.             switch (context.Node)  
  42.             {  
  43.                 case IfStatementSyntax ifstate:  
  44.                     body = ifstate.Statement;  
  45.                     token = ifstate.IfKeyword;  
  46.                     break;  
  47.                 case ForEachStatementSyntax foreachstate:  
  48.                     body = foreachstate.Statement;  
  49.                     token = foreachstate.ForEachKeyword;  
  50.                     break;  
  51.                 case ForStatementSyntax forstate:  
  52.                     body = forstate.Statement;  
  53.                     token = forstate.ForKeyword;  
  54.                     break;  
  55.                 case WhileStatementSyntax whilestate:  
  56.                     body = whilestate.Statement;  
  57.                     token = whilestate.WhileKeyword;  
  58.                     break;  
  59.                  // Add more blocks type  
  60.             }  
  61.   
  62.             if (!body.IsKind(SyntaxKind.Block))  
  63.             {  
  64.                 var diagnostic = Diagnostic.Create(SyntaxRule, token.GetLocation(), context.Node.Kind().ToString());  
  65.                 context.ReportDiagnostic(diagnostic);  
  66.             }  
  67.         }  
  68.     }  

Code Fixes

 
Step 1
 
Open the CodeFixProvider Class and provide all unique ids to FixableDiagnosticIds list. 
  1. public sealed override ImmutableArray<string> FixableDiagnosticIds  
  2.        {  
  3.            get { return ImmutableArray.Create(CSharpeStyleCopAnalyzer.LowerCaseDiagnosticId, CSharpeStyleCopAnalyzer.StartUnderScoreDiagnosticId,  
  4.                CSharpeStyleCopAnalyzer.CurlyBraceDiagnosticId); }  
  5.        }  
Step 2
 
The next step is to add your code fix implementation into override method “RegisterCodeFixesAsync”.
 
Step 3
 
Get the list of diagnostics and fix them one by one by checking the type of ruleId which was uniquely defined in analyzer class. Here is the full implementation of each code fix for respective rules defined in Analyzer class. 
  1. [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CSharpeStyleCopCodeFixProvider)), Shared]  
  2.     public class CSharpeStyleCopCodeFixProvider : CodeFixProvider  
  3.     {  
  4.         public sealed override ImmutableArray<string> FixableDiagnosticIds  
  5.         {  
  6.             get { return ImmutableArray.Create(CSharpeStyleCopAnalyzer.LowerCaseDiagnosticId, CSharpeStyleCopAnalyzer.StartUnderScoreDiagnosticId,  
  7.                 CSharpeStyleCopAnalyzer.CurlyBraceDiagnosticId); }  
  8.         }  
  9.   
  10.         public sealed override FixAllProvider GetFixAllProvider()  
  11.         {  
  12.             // See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/FixAllProvider.md for more information on Fix All Providers  
  13.             return WellKnownFixAllProviders.BatchFixer;  
  14.         }  
  15.   
  16.         public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)  
  17.         {  
  18.             var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);  
  19.   
  20.             foreach (Diagnostic diagnostic in context.Diagnostics)  
  21.             {  
  22.                 TextSpan diagnosticSpan = diagnostic.Location.SourceSpan;  
  23.   
  24.                 switch (diagnostic.Id)  
  25.                 {  
  26.                     case CSharpeStyleCopAnalyzer.CurlyBraceDiagnosticId:  
  27.                         context.RegisterCodeFix(  
  28.                           CodeAction.Create(  
  29.                                "Insert curly braces",  
  30.                                c => InsertCurlyBraces(context.Document, root.FindToken(diagnosticSpan.Start).Parent, c),  
  31.                                "Insert curly braces"),  
  32.                            diagnostic);  
  33.                         break;  
  34.                     case CSharpeStyleCopAnalyzer.StartUnderScoreDiagnosticId:  
  35.                         // Find the type declaration identified by the diagnostic.  
  36.                         var declaration1 = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<VariableDeclaratorSyntax>().First();  
  37.   
  38.                         context.RegisterCodeFix(  
  39.                            CodeAction.Create(  
  40.                                title: "Remove underscore",  
  41.                                 c => RemoveUnderscoreAsync(context.Document, declaration1, c),  
  42.                                 "Remove underscore"),  
  43.                             diagnostic);  
  44.   
  45.                         break;  
  46.                 }  
  47.             }  
  48.         }  
  49.   
  50.         private async Task<Document> InsertCurlyBraces(Document document, SyntaxNode oldStatemnt, CancellationToken c)  
  51.         {  
  52.             var newStatement = oldStatemnt;  
  53.   
  54.             switch (oldStatemnt)  
  55.             {  
  56.                 case IfStatementSyntax ifstate:  
  57.                     //var ifstate = oldStatemnt as IfStatementSyntax;  
  58.                     newStatement = ifstate.WithStatement(SyntaxFactory.Block(ifstate.Statement));  
  59.                     break;  
  60.                 case ForEachStatementSyntax foreachstate:  
  61.                     newStatement = foreachstate.WithStatement(SyntaxFactory.Block(foreachstate.Statement));  
  62.                     break;  
  63.             }  
  64.   
  65.             newStatement = newStatement.WithAdditionalAnnotations();  
  66.             var root = await document.GetSyntaxRootAsync(c).ConfigureAwait(false);  
  67.             var newRoot = root.ReplaceNode(oldStatemnt, newStatement);  
  68.             var newdoc = document.WithSyntaxRoot(newRoot);  
  69.   
  70.             return newdoc;  
  71.         }  
  72.   
  73.         private async Task<Solution> RemoveUnderscoreAsync(Document document, VariableDeclaratorSyntax declaration1, CancellationToken c)  
  74.         {  
  75.             var identifierToken = declaration1.Identifier;  
  76.             var newName = identifierToken.Text.Substring(1);  
  77.   
  78.             var semanticaModel = await document.GetSemanticModelAsync(c);  
  79.             var fieldSymbol = semanticaModel.GetDeclaredSymbol(declaration1, c);  
  80.   
  81.             var originalSolution = document.Project.Solution;  
  82.             var optionset = originalSolution.Workspace.Options;  
  83.             var newSolution = await Renamer.RenameSymbolAsync(document.Project.Solution, fieldSymbol, newName, optionset, c);  
  84.   
  85.             return newSolution;  
  86.         }  
  87.     }  

Build Vsix and Test

 
Step 1
 
Select your third project solution and build it. You may find the respective Vsix file in your bin/debug folder to install it.
 
Step 2
 
You can test your analyzer directly with VS without installing Vsix, select your VSix as a startup project and Run the current VS instance. This will open the new VS instance where you can open your existing VS project to test your analyzer rules as shown below in the screenshot.
 
Rule 1
 
Where we defined the field name starts with an underscore. The diagnostic finds such fields and shows the title and message. When you click on that message you may notice the icon visible, which is nothing but your code fixer that displays the fix to remove the underscore.
 
.NET Compiler Platform(Roslyn) - Build Analyzer And Code fixes
 
Rule 2
 
Where we defined Blocks like If, Foreach, For, etc does not have curly braces, diagnostic find such blocks and shows the title and message defined in your Rule DiagnosticDescriptor. When you click on that message you may notice the blub icon visible which is nothing but your code fixer which displays the fix to add curly braces.
 
.NET Compiler Platform(Roslyn) - Build Analyzer And Code fixes
 
Here, we could notice that how that analyzer extension is applied in this new project, and both the rules are trigged in class shows as error in green color with a user-defined title and then clicking on that opens up the yellow bulb icon to get a preview of the fix. 
 

Conclusion

 
Here, we learned to develop our own analyzer with help of .Net Compiler Platform (Roslyn) using the latest framework and VS2019.