Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Что такое 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.
К основным функциям, входящим в состав любого анализатора, относятся:
- Регистрация действий.
Действия представляют собой изменения кода, которые должны инициировать анализатор для проверки кода на наличие нарушений. Когда VisualStudio обнаруживает изменения кода, соответствующие зарегистрированному действию, она вызывает зарегистрированный метод анализатора. - Создание диагностики.
При обнаружении нарушения анализатор создает диагностический объект, используемый VisualStudio для уведомления пользователя о нарушении.
Существует несколько шагов для создания и проверки анализатора:
- Создайте решение.
- Зарегистрируйте имя и описание анализатора.
- Предупреждения и рекомендации анализатора отчетов.
- Выполните исправление кода, чтобы принять рекомендации.
- Улучшение анализа с помощью модульных тестов.
Действия регистрируются в переопределении метода 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».
Ссылки
- Исходники на GitHub
- The .NET Compiler Platform SDK