📜 ⬆️ ⬇️

Creating a Roslyn analyzer using the encapsulation test as an example

What is Roslyn?


Roslyn is a set of open source compilers and an API for analyzing code for C # and VisualBasic .NET from Microsoft.


Roslyn analyzer is a powerful tool for analyzing code, finding errors and fixing them.


Syntax tree and semantic model


To analyze the code, you need to have an idea of ​​the syntactic tree and the semantic model, since these are the two main components for static analysis.


A syntax tree is an element that is built on the basis of the source code of a program, and is necessary for analyzing the code. During the analysis of the code it moves.


Each code has a syntax tree. For the next class object


class A { void Method() { } } 

The syntax tree will look like this:


Tree


An object of type SyntaxTree is a syntax tree. There are three main elements in the tree: SyntaxNodes, SyntaxTokens, SyntaxTrivia.


Syntaxnodes describe syntactic constructions, namely: declarations, operators, expressions, etc. In C #, syntax constructs represent a class of type SyntaxNode.


Syntaxtokens describes such elements as: identifiers, keywords, special characters. In C #, this is the type of the SyntaxToken class.


Syntaxtrivia describes elements that will not be compiled, namely: spaces, newline characters, comments, preprocessor directives. In C #, it is defined by a SyntaxTrivia class.


The semantic model represents information about objects and their types. Thanks to this tool you can conduct a deep and complex analysis. In C #, it is defined by a class of type SemanticModel.


Creating an analyzer


To create a static analyzer, you need to install the following .NETCompilerPlatformSDK component.


The main functions included in any analyzer include:


  1. Registration action.
    Actions are code changes that the analyzer must initiate to check the code for violations. When VisualStudio detects code changes corresponding to a registered action, it calls the registered analyzer method.
  2. Creating a diagnosis.
    When a violation is detected, the analyzer creates a diagnostic object used by VisualStudio to notify the user of the violation.

There are several steps to create and test the analyzer:


  1. Create a solution.
  2. Register the name and description of the analyzer.
  3. Warnings and recommendations of the report analyzer.
  4. Execute correction code to accept recommendations.
  5. Improved analysis with unit tests.

Actions are registered in the override of the method DiagnosticAnalyzer.Initialize (AnalysisContext), where the AnalysisContext method in which the search for the object being analyzed is fixed.


The analyzer may provide one or more code fixes. A code fix identifies changes that address the reported problem. The user chooses the changes from the user interface (lights in the editor), and VisualStudio changes the code. The RegisterCodeFixesAsync method describes a code change.


Example


For example, let's write a public field analyzer. This application should alert the user about public fields and provide the ability to encapsulate a field with a property.


This is what should happen:


work example


Let's look at what you need to do.


First you need to create a solution.


solution creation


After creating the solution, we see that there are already three projects.


decision tree


We need two classes:


1) Class AnalyzerPublicFieldsAnalyzer, in which we specify the criteria for analyzing the code for finding public fields and a description of the warning for the user.


We indicate the following properties:


 public const string DiagnosticId = "PublicField"; private const string Title = "Filed is public"; private const string MessageFormat = "Field '{0}' is public"; private const string Category = "Syntax"; private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true); public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } } 

After that, we will indicate the criteria for analyzing public fields.


 private static void AnalyzeSymbol(SymbolAnalysisContext context) { var fieldSymbol = context.Symbol as IFieldSymbol; if (fieldSymbol != null && fieldSymbol.DeclaredAccessibility == Accessibility.Public && !fieldSymbol.IsConst && !fieldSymbol.IsAbstract && !fieldSymbol.IsStatic && !fieldSymbol.IsVirtual && !fieldSymbol.IsOverride && !fieldSymbol.IsReadOnly && !fieldSymbol.IsSealed && !fieldSymbol.IsExtern) { var diagnostic = Diagnostic.Create(Rule, fieldSymbol.Locations[0], fieldSymbol.Name); context.ReportDiagnostic(diagnostic); } } 

We get an object field of type IFieldSymbol, which has properties for defining field modifiers, its name and location. What we need for diagnosis.


It remains to initialize the analyzer, specifying in the overridden method


 public override void Initialize(AnalysisContext context) { context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.Field); } 

2) Now we proceed to change the proposed code by the user based on code analysis. This happens in the AnalyzerPublicFieldsCodeFixProvider class.


To do this, specify the following:


 private const string title = "Encapsulate field"; public sealed override ImmutableArray<string> FixableDiagnosticIds { get { return ImmutableArray.Create(AnalyzerPublicFieldsAnalyzer.DiagnosticId); } } 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 diagnosticSpan = diagnostic.Location.SourceSpan; var initialToken = root.FindToken(diagnosticSpan.Start); context.RegisterCodeFix( CodeAction.Create(title, c => EncapsulateFieldAsync(context.Document, initialToken, c), AnalyzerPublicFieldsAnalyzer.DiagnosticId), diagnostic); } 

And we define the ability to encapsulate a field with a property in the EncapsulateFieldAsync method.


 private async Task<Document> EncapsulateFieldAsync(Document document, SyntaxToken declaration, CancellationToken cancellationToken) { var field = FindAncestorOfType<FieldDeclarationSyntax>(declaration.Parent); var fieldType = field.Declaration.Type; ChangeNameFieldAndNameProperty(declaration.ValueText, out string fieldName, out string propertyName); var fieldDeclaration = CreateFieldDecaration(fieldName, fieldType); var propertyDeclaration = CreatePropertyDecaration(fieldName, propertyName, fieldType); var root = await document.GetSyntaxRootAsync(); var newRoot = root.ReplaceNode(field, new List<SyntaxNode> { fieldDeclaration, propertyDeclaration }); var newDocument = document.WithSyntaxRoot(newRoot); return newDocument; } 

For this you need to create a private field.


 private FieldDeclarationSyntax CreateFieldDecaration(string fieldName, TypeSyntax fieldType) { var variableDeclarationField = SyntaxFactory.VariableDeclaration(fieldType) .AddVariables(SyntaxFactory.VariableDeclarator(fieldName)); return SyntaxFactory.FieldDeclaration(variableDeclarationField) .AddModifiers(SyntaxFactory.Token(SyntaxKind.PrivateKeyword)); } 

Then create a public property that returns and accepts this private field.


 private PropertyDeclarationSyntax CreatePropertyDecaration(string fieldName, string propertyName, TypeSyntax propertyType) { var syntaxGet = SyntaxFactory.ParseStatement($"return {fieldName};"); var syntaxSet = SyntaxFactory.ParseStatement($"{fieldName} = value;"); return SyntaxFactory.PropertyDeclaration(propertyType, propertyName) .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword)) .AddAccessorListAccessors( SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration).WithBody(SyntaxFactory.Block(syntaxGet)), SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration).WithBody(SyntaxFactory.Block(syntaxSet))); } 

At the same time save the type and name of the source field. The name of the field is constructed as follows “_name”, and the name of the property “Name”.


Links


  1. Sources on GitHub
  2. The .NET Compiler Platform SDK

')

Source: https://habr.com/ru/post/455844/


All Articles