C# проектах командні стандарти для виконання, покращення якості коду чи автоматичне створення повторюваних блоків коду, Roslyn Compiler API є чудовим вибором. У цій статті ми розглянемо, як можна написати Analyzer та Code Fix за допомогою Roslyn, як створювати новий код під час компіляції з використанням Source Generator, а також як застосовувати все це як у проекті, так і в середовищі Visual Studio.
🤔 Що таке Roslyn?
Roslyn — це сучасна платформа компілятора, розроблена Microsoft для мов C# (та VB). Але називати її лише компілятором було б несправедливо; Roslyn дозволяє не тільки компілювати код, а й перетворювати його на синтаксичні дерева (syntax trees), символи (symbols) та семантичну модель (semantic model), при цьому надаючи вам доступ до цих даних.
Таким чином:
- За допомогою Analyzer ви можете перевіряти код під час компіляції та генерувати попередження.
- Додавши Code Fix, ви можете пропонувати розробникам виправлення одним кліком.
- Завдяки Source Generator ви можете автоматично генерувати код, усуваючи повторювані завдання.
🧱 Приклад сценарію: Імена інтерфейсів мають починатися з "I"
Уявімо такий сценарій: ви хочете, щоб усі ваші інтерфейси починалися з літери I
. У разі, якщо інтерфейс не відповідає цьому правилу:
- Analyzer спрацює і видасть попередження.
- Code Fix дозволить виправити ім'я одним кліком.
- Source Generator створить клас, що перераховує всі інтерфейси в проекті.
Нижче розглянемо структуру з двох проектів:
- InterfaceNamingAnalyzer: Проект, що містить Analyzer, Code Fix та Source Generator.
2.
SampleConsoleApp: Проста консольна програма для того, щоб спробувати Analyzer в реальних умовах.
🔨 Проект InterfaceNamingAnalyzer
Нижче наведено проект, який містить Analyzer для перевірки імен інтерфейсів, Code Fix для автоматичного виправлення цих імен і Source Generator, який під час компіляції знаходить і перераховує всі інтерфейси в проекті.
Analyzer: InterfaceNamingAnalyzer.cs
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace InterfaceNamingAnalyzer
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class InterfaceNamingAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "IFACE001";
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
DiagnosticId,
"Невірне іменування інтерфейсу",
"Інтерфейс '{0}' має починатися з 'I'.",
"Naming",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
public override ImmutableArray SupportedDiagnostics
=> ImmutableArray.Create(Rule);
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeInterface, SyntaxKind.InterfaceDeclaration);
}
private static void AnalyzeInterface(SyntaxNodeAnalysisContext context)
{
var ifaceDecl = (InterfaceDeclarationSyntax)context.Node;
var name = ifaceDecl.Identifier.Text;
if (!name.StartsWith("I"))
{
var diag = Diagnostic.Create(Rule, ifaceDecl.Identifier.GetLocation(), name);
context.ReportDiagnostic(diag);
}
}
}
}
Code Fix: InterfaceNamingCodeFixProvider.cs
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CodeActions;
namespace InterfaceNamingAnalyzer
{
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(InterfaceNamingCodeFixProvider)), Shared]
public class InterfaceNamingCodeFixProvider : CodeFixProvider
{
public sealed override ImmutableArray FixableDiagnosticIds
=> ImmutableArray.Create(InterfaceNamingAnalyzer.DiagnosticId);
public override FixAllProvider GetFixAllProvider()
=> WellKnownFixAllProviders.BatchFixer;
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var diag = context.Diagnostics.First();
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken);
var node = root.FindNode(diag.Location.SourceSpan) as InterfaceDeclarationSyntax;
if (node == null) return;
var currentName = node.Identifier.Text;
var newName = "I" + currentName;
context.RegisterCodeFix(
CodeAction.Create(
"Перейменувати, щоб починалося з 'I'",
c => RenameInterfaceAsync(context.Document, node, newName, c),
equivalenceKey: "RenameToI"),
diag);
}
private async Task RenameInterfaceAsync(
Document document, InterfaceDeclarationSyntax node, string newName, CancellationToken cancellationToken)
{
var identifierToken = SyntaxFactory.Identifier(newName);
var newNode = node.WithIdentifier(identifierToken);
var root = await document.GetSyntaxRootAsync(cancellationToken);
var newRoot = root.ReplaceNode(node, newNode);
return document.WithSyntaxRoot(newRoot);
}
}
}
Генератор Джерела: InterfaceListGenerator.cs
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace InterfaceNamingAnalyzer
{
[Generator]
public class InterfaceListGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context) { }
public void Execute(GeneratorExecutionContext context)
{
var compilation = context.Compilation;
var interfaces = compilation.SyntaxTrees
.SelectMany(t => t.GetRoot().DescendantNodes().OfType<InterfaceDeclarationSyntax>())
.Select(i => i.Identifier.Text)
.Distinct()
.ToList();
var sb = new StringBuilder();
sb.AppendLine("public static class InterfaceRegistry");
sb.AppendLine("{");
sb.AppendLine(" public static readonly string[] Interfaces = new string[] {");
foreach (var iface in interfaces)
{
sb.AppendLine($" \"{iface}\",");
}
sb.AppendLine(" };");
sb.AppendLine("}");
context.AddSource("InterfaceRegistry.g.cs", sb.ToString());
}
}
}
Під час компіляції автоматично створюється файл InterfaceRegistry.g.cs
, який містить масив з усіма іменами інтерфейсів.
📦 Публікація як VSIX
Цей Analyzer та Code Fix можна запакувати та опублікувати як розширення для Visual Studio (VSIX) замість використання на рівні проєкту.
Таким чином, кожен, хто встановить розширення для Visual Studio, автоматично скористується цими правилами при відкритті нового проєкту на C#.
Для створення проєкту VSIX:
- Додайте посилання на проект Analyzer: Додайте файл
InterfaceNamingAnalyzer.csproj
. - У файлі
source.extension.vsixmanifest
визначте ваш DLL файл Analyzer:
- Зібрати і розповсюдити файл .vsix: Легко завантажити ваше розширення через Visual Studio або за допомогою подвійного кліку.
🎉 Результат
Roslyn Compiler API дозволяє виконувати аналіз на етапі компіляції, автоматичні виправлення коду та генерувати код в проєктах C#.
У прикладах цієї статті:
- Ми створили Analyzer та Code Fix, які знаходять інтерфейси, що не починаються з "I".
- Додали Source Generator, який збирає всі імена інтерфейсів під час компіляції.
- І пояснили, як ви можете опублікувати все це як VSIX розширення для Visual Studio.
Ви можете застосувати будь-які правила, від стандартів компанії до правил продуктивності чи архітектурних обмежень, використовуючи цю модель. Створюючи складні сценарії автоматизації та генерації коду, ви зможете зробити ваші проєкти більш доглянутими та послідовними. Чим більше ви досліджуєте можливості, які надає Roslyn, тим легше вам буде підтримувати ваші проєкти. Бажаю приємного кодування!
Перекладено з: 🔥 Roslyn Compiler API ile Özelleştirilmiş C# Analizi ve Kod Üretimi