Creating a Roslyn analyzer using the encapsulation test as an example

Creating a Roslyn analyzer using the encapsulation test as an example


What is Roslyn?


Roslyn is a set of open source compilers and APIs for analyzing code for Microsoft’s C # and VisualBasic .NET languages.


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 of actions.
    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. Create diagnostics.
    When a violation is detected, the analyzer creates a diagnostic object that VisualStudio uses 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. Run the fix code to accept the recommendations.
  5. Improved analysis with unit tests.

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


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 code changes.


Example


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


Here's what should happen:


example of work


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 code analysis criteria for finding public fields and a description of the warning for the user.


Specify 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 & lt; DiagnosticDescriptor & gt;  SupportedDiagnostics
 {
  get
  {
  Return ImmutableArray.Create (Rule);
  }
 }  

After that, let us indicate the criteria for analyzing public fields.


  private static void AnalyzeSymbol (SymbolAnalysisContext context)
 {
  var fieldSymbol = context.Symbol as IFieldSymbol;

  if (fieldSymbol! = null & amp; & amp; fieldSymbol.DeclaredAccessibility == Accessibility.Public
  & amp; & amp;  ! fieldSymbol.IsConst & amp; & amp;  ! fieldSymbol.IsAbstract & amp; & amp;  ! fieldSymbol.IsStatic
  & amp; & amp;  ! fieldSymbol.IsVirtual & amp; & amp;  ! fieldSymbol.IsOverride & amp; & amp;  ! fieldSymbol.IsReadOnly
  & amp; & amp;  ! fieldSymbol.IsSealed & amp; & amp;  ! 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 the 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 let's move on to changing the proposed code by the user based on code analysis. This happens in the AnalyzerPublicFieldsCodeFixProvider class.


To do this, we specify the following:


  private const string title = "Encapsulate field";

 public sealed override ImmutableArray & lt; string & gt;  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 = & gt;  EncapsulateFieldAsync (context.Document, initialToken, c),
  AnalyzerPublicFieldsAnalyzer.DiagnosticId),
  diagnostic);
 }  

And define the ability to encapsulate the field property in the method of EncapsulateFieldAsync.


  private async Task & lt; Document & gt;  EncapsulateFieldAsync (Document Document, SyntaxToken declaration, CancellationToken cancellationToken)
 {
  var field = FindAncestorOfType & lt; FieldDeclarationSyntax & gt; (declaration.Parent);

  var fieldType = field.Declaration.Type;

  ChangeNameFieldAndNameProperty (declaration.ValueText, out string fieldName, out string propertyName);

  var fieldDeclaration = CreateFieldDecparation (fieldName, fieldType);

  var propertyDeclaration = CreatePropertyDecaration (fieldName, propertyName, fieldType);

  var root = await document.GetSyntaxRootAsync ();
  var newRoot = root.ReplaceNode (field, new List & lt; SyntaxNode & gt; {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, we save the type and name of the source field. The field name is constructed as follows “_name”, and the property name is “Name”.


References


  1. GitHub sources
  2. The .NET Compiler Platform SDK

Source text: Creating a Roslyn analyzer using the encapsulation test as an example