Creating a Roslyn analyzer using encapsulation testing as an example

    What is Roslyn?


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


    The 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 understanding of the syntax 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 the program, and is necessary for code analysis. During the analysis of the code, it moves along it.


    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. Three main elements can be distinguished in the tree: SyntaxNodes, SyntaxTokens, SyntaxTrivia.


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


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


    Syntaxtrivia describes elements that will not be compiled, namely spaces, line feeds, comments, preprocessor directives. In C #, it is defined by a class of type SyntaxTrivia.


    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 that make up any analyzer include:


    1. Registration of actions.
      Actions are code changes that the analyzer must initiate in order to check the code for violations. When VisualStudio detects code changes that correspond to the registered action, it calls the registered analyzer method.
    2. Create diagnostics.
      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 for creating and testing an analyzer:


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

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


    The analyzer can provide one or more code corrections. A code patch identifies changes that address the reported problem. The user chooses the changes from the user interface (light bulbs in the editor), and VisualStudio changes the code. The RegisterCodeFixesAsync method describes how to change the code.


    Example


    For an example we will write the analyzer of public fields. This application should warn the user about public fields and provide the ability to encapsulate the field with a property.


    Here's what you should get:


    work example


    Let’s figure out what needs to be done for this.


    First you need to create a solution.


    decision making


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


    decision tree


    We need two classes:


    1) The AnalyzerPublicFieldsAnalyzer class, in which we specify the code analysis criteria 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 SupportedDiagnostics
    {
        get
        {
            return ImmutableArray.Create(Rule);
        }
    }

    After that, we indicate by what criteria the analysis of public fields will take place.


    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 a field of an object 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 by 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 the analysis of the code. This happens in the class AnalyzerPublicFieldsCodeFixProvider.


    To do this, indicate the following:


    private const string title = "Encapsulate field";
    public sealed override ImmutableArray 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 determine the ability to encapsulate the field with a property in the EncapsulateFieldAsync method.


    private async Task EncapsulateFieldAsync(Document document, SyntaxToken declaration, CancellationToken cancellationToken)
    {
        var field = FindAncestorOfType(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 { fieldDeclaration, propertyDeclaration });
        var newDocument = document.WithSyntaxRoot(newRoot);
        return newDocument;
    }

    To do this, 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 receives 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 name of the field is constructed as follows "_name", and the name of the property "Name".


    References


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

    Also popular now: