Хабрахабр

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

Что такое Roslyn?

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

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

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

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

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

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

class A

}

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

Дерево

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

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

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

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

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

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

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

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

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

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

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

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

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

Пример

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

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

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

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

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

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

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

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

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

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

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

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

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
Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть