Проверяем инкапсуляцию с помощью Roslyn

в 12:24, , рубрики: .net, api, C#, open source, roslyn, static code analysis, Visual Studio, статический анализ кода

Что такое 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

Автор: illusionist1nemo

Источник

* - обязательные к заполнению поля