Создание анализатора Roslyn на примере проверки инкапсуляции

    Что такое Roslyn?


    Roslyn – это набор компиляторов с открытым исходным кодом и API для анализа кода для языков C# и VisualBasic .NET от Microsoft.


    Анализатор Roslyn – мощный инструмент для анализа кода, нахождения ошибок и их исправления.


    Синтаксическое дерево и семантическая модель


    Для анализа кода, нужно иметь представление о синтаксическом дереве и семантической модели, так как это два основных компонента для статического анализа.


    Синтаксическое дерево — это элемент, который строится на основании исходного кода программы, и необходимый для анализа кода. В ходе анализа кода по нему происходит перемещение.


    Каждый код обладает синтаксическим деревом. Для следующего объекта класса


    class A
    {
        void Method()
        {
        }
    }

    синтаксическое дерево будет выглядеть так:


    Дерево


    Объект типа SyntaxTree представляет собой синтаксическое дерево. В дереве можно выделить три основных элемента: SyntaxNodes, SyntaxTokens, SyntaxTrivia.


    Syntaxnodes описывают синтаксические конструкции, а именно: объявления, операторы, выражения и т.п. В C# синтаксические конструкции представляют класс типа SyntaxNode.


    Syntaxtokens описывает такие элементы, как: идентификаторы, ключевые слова, специальные символы. В C# является типом класса SyntaxToken.


    Syntaxtrivia описывает элементы, которые не будут скомпилированы, а именно: пробелы, символы перевода строки, комментарии, директивы препроцессора. В C# определяется классом типа SyntaxTrivia.


    Семантическая модель представляет информацию об объектах и об их типах. Благодаря этому инструменту можно проводить глубокий и сложный анализ. В C# определяется классом типа SemanticModel.


    Создание анализатора


    Для создания статического анализатора требуется установить следующий компонент .NETCompilerPlatformSDK.


    К основным функциям, входящим в состав любого анализатора, относятся:


    1. Регистрация действий.
      Действия представляют собой изменения кода, которые должны инициировать анализатор для проверки кода на наличие нарушений. Когда VisualStudio обнаруживает изменения кода, соответствующие зарегистрированному действию, она вызывает зарегистрированный метод анализатора.
    2. Создание диагностики.
      При обнаружении нарушения анализатор создает диагностический объект, используемый VisualStudio для уведомления пользователя о нарушении.

    Существует несколько шагов для создания и проверки анализатора:


    1. Создайте решение.
    2. Зарегистрируйте имя и описание анализатора.
    3. Предупреждения и рекомендации анализатора отчетов.
    4. Выполните исправление кода, чтобы принять рекомендации.
    5. Улучшение анализа с помощью модульных тестов.

    Действия регистрируются в переопределении метода DiagnosticAnalyzer.Initialize (AnalysisContext), где AnalysisContext метод в котором фиксируется поиск анализируемого объекта.


    Анализатор может предоставить одно или несколько исправлений кода. Исправление кода определяет изменения, которые обращаются к сообщенной проблеме. Пользователь сам выбирает изменения из пользовательского интерфейса (лампочки в редакторе), а VisualStudio изменяет код. В методе RegisterCodeFixesAsync описывается изменение кода.


    Пример


    Для примера напишем анализатор публичных полей. Это приложение должно предупредить пользователя о публичных полях и предоставить возможность инкапсулировать поле свойством.


    Вот что должно получиться:


    пример работы


    Разберем, что для этого нужно сделать


    Для начала следует создать решение.


    создание решения


    После создания решение видим, что уже есть три проекта.


    дерево решения


    Нам потребуется два класса:


    1) Класс AnalyzerPublicFieldsAnalyzer, в котором указываем критерии анализа кода для нахождения публичных полей и описание предупреждения для пользователя.


    Укажем следующие свойства:


    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);
        }
    }

    После этого укажем, по каким критериям будет происходить анализ публичных полей.


    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);
        }
    }

    Мы получаем поле объекта типа IFieldSymbol, который обладает свойствами для определения модификаторов поля, его имени и локации. Что нам и нужно для диагностики.


    Остается инициализировать анализатор, указав в переопределённом методе


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

    2) Теперь перейдем к изменению предлагаемого кода пользователем на основе анализа кода. Это происходит в классе AnalyzerPublicFieldsCodeFixProvider.


    Для этого указываем следующее:


    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);
    }

    И определяем возможность инкапсулировать поле свойством в методе EncapsulateFieldAsync.


    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;
    }

    Для этого необходимо создать приватное поле.


    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));
    }

    Затем создать публичное свойство, возвращающее и принимающее это приватное поле.


    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)));
    }

    При этом сохраняем тип и имя исходного поля. Имя поля строится следующим образом «_name», а имя свойства «Name».


    Ссылки


    1. Исходники на GitHub
    2. The .NET Compiler Platform SDK
    • +10
    • 2,1k
    • 7
    Поделиться публикацией

    Комментарии 7

      0
      Проверяем инкапсуляцию с помощью Roslyn
      Мне кажется было бы лучше назвать статью — «Создание анализатора Roslyn на примере проверки инкапсуляции»

      Большую аудиторию привлечёте. Ещё было бы неплохо предоставить ссылки на репозитории с хорошими примерами анализаторов, в своё время потратил много времени пытаясь найти что-то приемлемое.

      Ну и если добавить пример как анализатор использовать с CI, то вообще бы было замечательно.
        0
        Плюсую к ci. Можно ли такую штуку сделать подключаемой через nuget (без VS) и кидать предупреждения /ошибки при сборке?
          0
          Можно.
          Добавляете к солюшну новый проект (Visual C# -> Extensibility -> Analyzer with Code Fix (.NET Standard)), потом через AddReference добавляете анализатор к вашему проекту. После билда .DLL анализатора падает в Output проекта.
            0
            dll то попадёт, а будет ли запускаться анализатор при каждой сборке?
              +1
              Анализатор будет запускаться при каждой сборке. Сейчас специально проверил, авто-билд падает при срабатывании анализатора.
                0
                Супер, спасибо.
              0
              А для того, чтобы проверить правильно ли написан ваш анализатор, есть пакет Microsoft.CodeAnalysis.Analyzers :) Побуду капитаном — правда ведь все знают про StyleCop.Analyzers и прочие Microsoft.CodeAnalysis.FxCopAnalyzers? Это такие же подключаемые анализаторы, возможность их использования появилась еще в VS 2015, но почему-то они мало где упоминаются. А между тем статический анализ кода (о чем неустанно нам напоминает PVS-Studio :)) — весьма важная вещь, тем более бесплатно (разве что время сборки увеличивается).

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое