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