Что такое Roslyn: читаем, исправляем и генерируем код C# с точки зрения компилятора

· · .NET, C#, Roslyn, Анализатор, Генератор кода, Компилятор, Статический анализ, Генерация кода, Использование существующих активов

1. Что нужно понять в первую очередь

Ситуаций, когда нужно обрабатывать исходный код C#, на удивление много. Например, такие задачи.

Запретить определённый способ использования API
Механически находить устаревший стиль кода
Собрать список методов и классов
Изучить зависимости по всему проекту
Генерировать шаблонный код на этапе компиляции
Предупреждать о неправильном использовании собственной библиотеки во время сборки
Безопасно провести масштабную замену или миграцию

В таких случаях порой хочется просто открыть файлы *.cs и обработать их поиском по строке или регулярными выражениями.

Но C# - это не строка. Эти два фрагмента похожи внешне, но по смыслу они разные вещи.

Console.WriteLine("Hello");
MyCompany.Logging.Console.WriteLine("Hello");

Кроме того, имя Console иногда может указывать на другой тип.

using Console = MyCompany.Logging.Console;

Console.WriteLine("Hello");

Если смотреть как на строку, всё это выглядит как Console.WriteLine. Но с точки зрения компилятора нельзя понять, идёт ли речь о System.Console.WriteLine или о другом типе, не выполнив разрешение имён (name resolution).

Именно здесь на сцену выходит Roslyn. Roslyn - это платформа, которая делает информацию, которой располагает компилятор C# и Visual Basic, доступной приложениям и инструментам в виде API.

Проще говоря, благодаря Roslyn код на C# можно обрабатывать так:

Читать не как строку, а как синтаксис
Читать не по внешнему виду, а по смыслу
Читать не отдельный файл, а весь проект или решение (solution)
На основе прочитанного выдавать предупреждения, варианты исправлений и сгенерированный код

В этой статье мы разберём общую картину Roslyn: Syntax Tree, SemanticModel, Workspace, Analyzer, Source Generator, а также то, где всё это применяется на практике.

Заметим, что весь код, встречающийся в этой статье, опубликован на GitHub в виде полноценного набора примеров, которые можно собрать и запустить (библиотека для работы с Syntax Tree / SemanticModel, Analyzer, предупреждающий об использовании DateTime.Now, Source Generator, демонстрация анализа целого решения, а также юнит-тесты для проверки ложных срабатываний и пропусков).

roslyn-dotnet-compiler-platform - komurasoft-blog-samples (GitHub)

2. Что такое Roslyn

Официально Roslyn называется .NET Compiler Platform. Это реализация компилятора C# и Visual Basic, а одновременно - набор API для создания инструментов анализа кода.

Традиционно компилятор часто воспринимали как «чёрный ящик» вида:

Подаём исходный код на вход
Компилятор его обрабатывает
На выходе получаем DLL / EXE

Обычно у разработчиков не было доступа к информации, которую компилятор создавал внутри себя.

На самом же деле компилятор не просто преобразует текст в машинный код или IL - в процессе компиляции он создаёт информацию вроде следующей.

Эта последовательность символов - объявление класса
Этот идентификатор - локальная переменная
Этот вызов метода указывает именно на такой метод такого типа
Тип возвращаемого значения этого выражения - string
В этом коде есть синтаксическая ошибка
Эта ссылка указывает на тип из сборки A
Эта директива using фактически не используется

Roslyn делает эту информацию доступной для разработчиков. Поэтому Roslyn - не просто компилятор, а платформа для понимания кода.

3. Что можно делать с помощью Roslyn

С помощью Roslyn в основном можно делать следующее.

Синтаксический анализ C# / VB
Семантический анализ типов и методов
Получение информации о компиляции
Анализ всего проекта или решения
Создание собственных Analyzer
Создание Code Fix
Создание Source Generator
Создание инструментов рефакторинга
Генерация кода
Преобразование кода

Если сформулировать чуть более практично, использование выглядит так.

Выдавать предупреждение сборки при использовании запрещённого API
Составить список мест, использующих устаревший API
Проверять правила именования async-методов
Обнаруживать пропущенную обработку IDisposable
Направлять к правильному использованию собственного фреймворка
Генерировать DTO и код маппинга на этапе компиляции
Генерировать шаблонный код из конфигурационных файлов или атрибутов
Помогать в исследовании миграции с .NET Framework на .NET

Важность Roslyn в том, что «обработку исходного кода C#» можно писать на той же основе, что и сам компилятор.

Если читать C# с помощью регулярных выражений или собственного парсера, ограничения проявляются очень быстро. Например, корректно обработать следующие элементы непросто.

using alias
расширяющие методы (extension methods)
partial class
partial method
global using
nullable-аннотации
обобщённые (generic) типы
разрешение перегрузок (overload resolution)
условная компиляция
директивы препроцессора
переписывание кода с сохранением комментариев и пробелов

Roslyn предоставляет API, позволяющие обрабатывать всё это в соответствии со спецификацией языка C#.

4. Roslyn разделяет «синтаксис» и «смысл»

При изучении Roslyn в первую очередь стоит разделить для себя эти два понятия.

Синтаксис: как написан код
Смысл: на что этот код указывает

Рассмотрим, например, такой код.

Console.WriteLine(message);

Если смотреть на него как на синтаксис, получится такая структура.

Выражение-инструкция
  Выражение вызова
    Выражение доступа к члену
      Идентификатор Console
      Идентификатор WriteLine
    Аргумент message

Но одного этого недостаточно, чтобы понять смысл. Какому типу принадлежит Console, какая перегрузка имеется в виду под WriteLine, каков тип message - по одному синтаксису этого не определить.

Чтобы увидеть смысл, требуется вся эта информация.

Состояние директив using
Ссылающиеся сборки
Определения типов в том же проекте
Ссылки на другие проекты
Вывод типов
Разрешение перегрузок
Версия языка
nullable-контекст

В Roslyn это различие отражено и на уровне API.

Syntax Tree     : представляет синтаксис кода
SemanticModel   : представляет, что означает синтаксис
Compilation     : представляет всю информацию, необходимую для компиляции
Workspace       : работает с решением, проектами и документами

Если усвоить это разделение, ориентироваться в Roslyn становится намного легче.

5. Что такое Syntax Tree

Syntax Tree - это дерево, представляющее синтаксическую структуру исходного кода. Предположим, есть такой код.

class User
{
    public string Name { get; set; }

    public void Rename(string name)
    {
        Name = name;
    }
}

С точки зрения Roslyn этот код имеет примерно такую структуру.

CompilationUnit
  ClassDeclaration: User
    PropertyDeclaration: Name
    MethodDeclaration: Rename
      Parameter: name
      Block
        ExpressionStatement
          AssignmentExpression

Syntax Tree - это не текст, разбитый построчно, а структура, упорядоченная по синтаксическим элементам C#: классы, методы, свойства, выражения, инструкции, аргументы, операторы и так далее.

Рассмотрим простой пример.

using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

var source = """
class User
{
    public string Name { get; set; }

    public void Rename(string name)
    {
        Name = name;
    }
}
""";

var tree = CSharpSyntaxTree.ParseText(source);
var root = tree.GetCompilationUnitRoot();

var methods = root
    .DescendantNodes()
    .OfType<MethodDeclarationSyntax>();

foreach (var method in methods)
{
    Console.WriteLine(method.Identifier.Text);
}

Этот код ищет в исходном коде объявления методов и выводит их имена. В данном примере будет получено Rename.

Важно, что здесь ищется не строка void, а «объявление метода» именно как синтаксическая конструкция C#.

6. Node, Token и Trivia

При работе с Syntax Tree часто встречаются эти три термина.

SyntaxNode
SyntaxToken
SyntaxTrivia

SyntaxNode

SyntaxNode - это синтаксическая единица. Например, такие вещи.

Объявление класса
Объявление метода
Объявление свойства
Инструкция if
Инструкция for
Выражение присваивания
Выражение вызова
Лямбда-выражение

В синтаксисе C# Node - это то, что может иметь дочерние элементы.

SyntaxToken

SyntaxToken - это минимальная единица, из которой строится синтаксис. Например, вот такие.

ключевое слово class
ключевое слово public
идентификатор User
идентификатор Rename
{ и }
; и ,
строковый литерал
числовой литерал

Token - это листовые (концевые) элементы синтаксического дерева.

SyntaxTrivia

SyntaxTrivia - это информация, которая напрямую не участвует в обычном семантическом анализе, но нужна для точного воспроизведения исходного кода. Например, вот такие вещи.

пробелы
переводы строк
комментарии
директивы препроцессора

Именно благодаря Trivia Roslyn может работать с исходным кодом с высокой точностью, включая комментарии и пробелы.

Trivia крайне важна при форматировании кода, рефакторинге и механическом переписывании.

Если требуется только построить AST, комментарии можно было бы и отбросить. Но в практических преобразованиях кода важно не ломать комментарии и переводы строк.

7. Syntax Tree неизменяем

Syntax Tree в Roslyn неизменяем (immutable). То есть вместо того, чтобы напрямую изменять полученное синтаксическое дерево, создаётся новое дерево с внесёнными изменениями.

Например, даже когда нужно изменить имя метода, существующий MethodDeclarationSyntax не модифицируется на месте.

var newMethod = oldMethod.WithIdentifier(
    SyntaxFactory.Identifier("NewName"));

Так создаётся новый узел.

У неизменяемости есть ряд преимуществ.

Удобнее работать из нескольких потоков
Можно безопасно работать со снимками кода, редактируемого в IDE
Легче формировать диффы
Легче сравнивать состояние до и после изменения

Поначалу это может показаться немного неудобным. Но в мире, где один и тот же код одновременно используют несколько процессов - IDE, сборка, Analyzer, Source Generator, - неизменяемость становится большим преимуществом.

8. Что такое SemanticModel

Только по Syntax Tree можно узнать лишь то, как код выглядит. Чтобы узнать его смысл, используется SemanticModel.

Рассмотрим, например, такой код.

Console.WriteLine("Hello");

Из Syntax Tree понятно, что есть идентификаторы Console и WriteLine. Но неизвестно, указывают ли они на System.Console.WriteLine(string?) или на метод какого-то другого типа.

С помощью SemanticModel можно узнать, «в какой символ разрешился данный синтаксический узел».

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

var source = """
using System;

class Program
{
    static void Main()
    {
        Console.WriteLine("Hello");
    }
}
""";

var tree = CSharpSyntaxTree.ParseText(source);

var compilation = CSharpCompilation.Create(
    assemblyName: "Sample",
    syntaxTrees: new[] { tree },
    references: new[]
    {
        MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
        MetadataReference.CreateFromFile(typeof(Console).Assembly.Location)
    });

var semanticModel = compilation.GetSemanticModel(tree);
var root = tree.GetCompilationUnitRoot();

var invocation = root
    .DescendantNodes()
    .OfType<InvocationExpressionSyntax>()
    .First();

var symbolInfo = semanticModel.GetSymbolInfo(invocation);
var method = (IMethodSymbol?)symbolInfo.Symbol;

Console.WriteLine(method?.ContainingType.ToDisplayString());
Console.WriteLine(method?.Name);

Так можно получить сведения о том, в какой именно метод фактически разрешился вызов Console.WriteLine.

Сила Roslyn в том, что можно опираться не только на синтаксис, но и на результаты разрешения имён, полученные самим компилятором.

9. Что такое Symbol

В Roslyn типы, методы, свойства, поля, параметры, локальные переменные и тому подобное представлены как Symbol.

Вот основные интерфейсы.

INamedTypeSymbol  : классы, структуры, интерфейсы и т. п.
IMethodSymbol     : методы
IPropertySymbol   : свойства
IFieldSymbol      : поля
IParameterSymbol  : параметры
ILocalSymbol      : локальные переменные
INamespaceSymbol  : пространства имён

Symbol представляет не внешний вид кода в исходнике, а смысл, разрешённый компилятором.

Например, эти два фрагмента кода выглядят по-разному.

System.Console.WriteLine("Hello");
using System;

Console.WriteLine("Hello");

Но если оба указывают на один и тот же System.Console.WriteLine, то при семантическом анализе Roslyn они будут представлены одним и тем же символом метода.

Благодаря этому свойству становятся возможны такие проверки.

Действительно ли этот вызов относится к API, запрещённому в компании
Реализует ли этот тип определённый интерфейс
Является ли этот метод async
Является ли это возвращаемое значение nullable
Действительно ли применён этот атрибут
Наследует ли этот класс определённый базовый класс

Это анализ, основанный на суждении самого компилятора, а не на простом поиске по строке.

10. Что такое Compilation

Compilation объединяет всю информацию, необходимую для компиляции программы на C# или Visual Basic.

Конкретно, она содержит такую информацию.

Набор SyntaxTree
Ссылающиеся сборки
Параметры компиляции
Версия языка
Предопределённые символы
Информация о типах и членах
Диагностическая информация

Если требуется читать один файл только как синтаксис, достаточно SyntaxTree. Но для разрешения типов и ссылок нужен Compilation.

Например, когда нужно сделать следующее.

Узнать, методу какого типа принадлежит этот вызов
Узнать, реализует ли этот класс IDisposable
Узнать, к какому именно типу атрибутов относится этот атрибут
Узнать тип возвращаемого значения этого выражения
Получить ошибки и предупреждения компиляции

Всё это невозможно определить по одному синтаксису. Нужно учитывать ещё и ссылки проекта, и параметры компиляции.

11. Что такое Workspace

Когда нужно работать не с отдельным файлом, а с целым решением или проектом, используется Workspace.

Workspace оперирует такими единицами.

Solution
Project
Document

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

using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis.MSBuild;

MSBuildLocator.RegisterDefaults();

using var workspace = MSBuildWorkspace.Create();
var solution = await workspace.OpenSolutionAsync("Sample.sln");

foreach (var project in solution.Projects)
{
    Console.WriteLine(project.Name);

    foreach (var document in project.Documents)
    {
        var root = await document.GetSyntaxRootAsync();
        Console.WriteLine($"  {document.Name}: {root?.DescendantNodes().Count()} nodes");
    }
}

Такой инструмент можно использовать для исследования существующей кодовой базы и помощи в миграции.

Например, для таких задач.

Выгрузить список вызовов определённого API в CSV
Составить список мест, использующих устаревшее пространство имён
Составить список публичных API
Изучить зависимости между проектами
Проверить огромное решение на нарушения соглашений по коду
Выполнить механическое преобразование кода

Analyzer - это механизм, работающий в связке с IDE и сборкой. А консольные инструменты на основе Workspace, напротив, хорошо подходят для исследований и массовой миграции.

Эти два подхода похожи, но стоит разделять сферы их применения.

12. Основные формы использования Roslyn

Способы использования Roslyn можно условно разделить на четыре.

1. Использовать как библиотеку
2. Создать Analyzer
3. Создать Code Fix
4. Создать Source Generator

У каждого из них своя цель.

Использование как библиотеки

Вы вызываете API Roslyn из собственного консольного приложения или внутреннего инструмента.

Подходящие сценарии использования - примерно такие.

Исследование кодовой базы
Массовое преобразование
Сбор метрик
Помощь в миграции
Составление отчётов

В этом варианте инструмент можно запускать в любой удобный момент. Поскольку ему не нужно работать во время набора текста в IDE, допустима и относительно тяжёлая обработка.

Создание Analyzer

Analyzer - это механизм, который анализирует код и выдаёт предупреждения или ошибки.

Например, можно создать такие правила.

Использовать DateTimeOffset.UtcNow вместо DateTime.Now
Имена async-методов должны заканчиваться на Async
Не вызывать API инициализации библиотеки в неправильном порядке
Не использовать определённое пространство имён в новом коде
Запретить использование Task.Result / Wait

Analyzer можно запускать в Visual Studio и во время сборки. Он позволяет механически выявлять нарушения командных соглашений и правил использования библиотек, не полагаясь на память рецензента.

Создание Code Fix

Code Fix - это механизм, предлагающий варианты исправления проблем, найденных Analyzer-ом.

Проще всего представить это как исправление, применяемое через значок лампочки в Visual Studio.

Например, предположим, что Analyzer обнаружил такой код.

DateTime.Now

Code Fix может предложить такое исправление.

DateTimeOffset.UtcNow

Сила Code Fix в том, что можно автоматизировать не просто «выдачу предупреждения», а «безопасный способ исправления».

Создание Source Generator

Source Generator - это механизм, который генерирует код на этапе компиляции и добавляет его в ту же самую компиляцию.

Например, у него такие применения.

Генерировать шаблонный код из классов с определённым атрибутом
Генерировать типобезопасные аксессоры из конфигурационных файлов
Генерировать код маппинга DTO
Генерировать код для сериализатора
Генерировать код преобразования enum в строку и обратно
Генерировать код маршрутизации или регистрации в DI

Обработку, которая раньше собирала информацию через Reflection во время выполнения, в ряде случаев можно заменить кодом, сгенерированным на этапе компиляции. Это может снизить затраты на запуск и улучшить совместимость с AOT.

13. Analyzer можно использовать как «автоматическое код-ревью»

На практике Analyzer проще всего воспринимать как «автоматическое код-ревью».

На код-ревью нередко раз за разом звучат одни и те же замечания.

Пожалуйста, не используйте этот API
Это имя метода не соответствует соглашениям
Этот catch просто проглатывает исключение
Эта проверка на null не нужна
У этого вызова есть проблемы с производительностью

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

Особенно хорошо для Analyzer подходят такие правила.

Можно однозначно определить, хорошо это или плохо
Мало исключений
Способ исправления уже определён
Вся команда хочет это соблюдать
Часто встречается на ревью
Допустимо останавливать сборку из-за этого

И наоборот, есть правила, не подходящие для Analyzer.

Оценка сильно зависит от контекста
Требуется архитектурное решение
Слишком много исключений
Мнения расходятся от человека к человеку
Предупреждений становится так много, что их никто не смотрит

Analyzer - мощный инструмент. Именно поэтому, если добавить их слишком много, это ухудшит опыт разработки. Лучше начинать с небольшого числа действительно важных правил.

14. Начните с Analyzer, входящих в .NET SDK

Прежде чем писать собственный Analyzer, разумнее сначала проверить те, что уже входят в .NET SDK.

В проектах на .NET 5 и новее анализ кода .NET включён по умолчанию.

Часто встречаются диагностические идентификаторы двух семейств.

CAxxxx : качество кода, надёжность, производительность, безопасность и т. п.
IDExxxx: стиль кода, поддержка IDE и т. п.

Уровень серьёзности Analyzer можно настроить в .editorconfig.

# Пример: сделать неиспользуемые using предупреждением
dotnet_diagnostic.IDE0005.severity = warning

# Пример: сделать CA2000 ошибкой
dotnet_diagnostic.CA2000.severity = error

Включение и ужесточение анализа также можно задать в файле проекта.

<PropertyGroup>
  <EnableNETAnalyzers>true</EnableNETAnalyzers>
  <AnalysisLevel>latest</AnalysisLevel>
  <TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>

При внедрении в существующий проект поначалу может появиться масса предупреждений. В этом случае лучше не превращать всё сразу в ошибки, а действовать поэтапно.

Сначала оценить общее число предупреждений
Установить правило не увеличивать их число в новом коде
Сделать warning только для важных правил
Сделать error только для правил, которые действительно нужно соблюдать неукоснительно
Планомерно сокращать существующие нарушения

Собственный Analyzer стоит воспринимать как нечто, дополняющее эту базу «правилами, специфичными для конкретной компании».

15. Минимальный пример Analyzer

Analyzer находит определённый синтаксис или символы и сообщает о них через Diagnostic.

Рассмотрим, например, Analyzer, предупреждающий об использовании DateTime.Now.

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

using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class NoDateTimeNowAnalyzer : DiagnosticAnalyzer
{
    private static readonly DiagnosticDescriptor Rule = new(
        id: "CMP001",
        title: "Не используйте DateTime.Now напрямую",
        messageFormat: "Вместо DateTime.Now в зависимости от сценария рассмотрите DateTimeOffset.UtcNow или другой подходящий вариант",
        category: "Usage",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true);

    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
        => ImmutableArray.Create(Rule);

    public override void Initialize(AnalysisContext context)
    {
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.EnableConcurrentExecution();

        context.RegisterSyntaxNodeAction(
            AnalyzeMemberAccess,
            SyntaxKind.SimpleMemberAccessExpression);
    }

    private static void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context)
    {
        var memberAccess = (MemberAccessExpressionSyntax)context.Node;

        if (memberAccess.Name.Identifier.Text != "Now")
        {
            return;
        }

        var symbol = context.SemanticModel.GetSymbolInfo(memberAccess).Symbol;
        if (symbol is not IPropertySymbol propertySymbol)
        {
            return;
        }

        if (propertySymbol.Name == "Now" &&
            propertySymbol.ContainingType.ToDisplayString() == "System.DateTime")
        {
            var diagnostic = Diagnostic.Create(Rule, memberAccess.GetLocation());
            context.ReportDiagnostic(diagnostic);
        }
    }
}

Важно, что в этом примере мы не просто ищем строку DateTime.Now.

С помощью SemanticModel проверяется, действительно ли это указывает на System.DateTime.Now.

Благодаря этому гораздо труднее ошибочно принять за него что-то другое, например вот это.

MyCompany.DateTime.Now

В Analyzer-ах часто применяется именно такой подход: сузить кандидатов по синтаксису, а затем через семантический анализ проверить, действительно ли это искомый объект.

Быстро искать кандидатов по Syntax
Точно определять с помощью SemanticModel
Сообщать о месте и сообщении через Diagnostic

16. Code Fix доносит до пользователя и «способ исправления»

Analyzer находит проблему, а Code Fix предлагает способ её исправления.

Например, при обнаружении DateTime.Now можно предложить такие варианты исправления.

Заменить на DateTimeOffset.UtcNow
Заменить на абстракцию вроде IClock.Now

Однако Code Fix нужно проектировать с осторожностью. Не всегда правильно всегда заменять DateTime.Now на DateTimeOffset.UtcNow. Когда нужно отобразить локальное время и когда нужно работать со временем для хранения или сравнения - подходящий тип и обращение с часовым поясом будут разными.

Поэтому Code Fix хорошо подходит, когда выполняются такие условия.

Смысл после исправления однозначен
Побочные эффекты минимальны
Можно безопасно исправить механическим преобразованием
Человеку легко проверить результат

Например, такие исправления хорошо сочетаются с Code Fix.

Заменить старое имя API на новое
Добавить недостающий using
Переименовать в соответствии с правилами именования
Добавить атрибут
Удалить ненужный аргумент

С другой стороны, для исправлений, требующих архитектурного решения, порой лучше ограничиться только предупреждением, не предлагая автоматическое исправление.

17. Source Generator - это «генерация кода на этапе компиляции»

Source Generator выполняется во время компиляции и добавляет сгенерированный код C# в ту же самую компиляцию.

Общий поток выглядит так.

Читает исходный код пользователя
Изучает атрибуты и определения типов
Генерирует необходимый код C#
Добавляет сгенерированный код в объект компиляции

Пример простого Source Generator.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;

[Generator]
public sealed class BuildInfoGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        context.RegisterPostInitializationOutput(static ctx =>
        {
            var source = """
namespace Generated;

public static class BuildInfo
{
    public static string Tool => "Roslyn Source Generator";
}
""";

            ctx.AddSource(
                "BuildInfo.g.cs",
                SourceText.From(source, Encoding.UTF8));
        });
    }
}

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

Console.WriteLine(Generated.BuildInfo.Tool);

Source Generator не переписывает существующий код пользователя. Всё, что он может, - это сгенерировать дополнительный исходный код и включить его в компиляцию.

Поэтому проще всего думать об этом так.

Это не преобразование существующего кода
Это создание дополнительного кода на основе анализа существующего

Если требуется массово переписать существующий код, стоит рассматривать не Source Generator, а инструмент миграции на основе Roslyn или Code Fix.

18. Как ссылаться на Source Generator

Когда во время разработки проект Generator-а подключается из другого проекта, обращение с ним отличается от обычной ссылки на библиотеку.

Дело в том, что генератор - это не библиотека, на которую ссылаются во время выполнения, а нечто, загружаемое компилятором как Analyzer во время компиляции.

В ссылке на проект это указывается так.

<ItemGroup>
  <ProjectReference Include="..\BuildInfoGenerator\BuildInfoGenerator.csproj"
                    OutputItemType="Analyzer"
                    ReferenceOutputAssembly="false" />
</ItemGroup>

Указав ReferenceOutputAssembly="false", вы делаете так, чтобы DLL генератора не рассматривался как обычная ссылочная сборка.

При распространении в виде пакета NuGet его также размещают так, чтобы он загружался как Analyzer / Source Generator.

Source Generator удобен, но его жизненный цикл отличается от обычной библиотеки.

Обычная библиотека: используется приложением во время выполнения
Source Generator: используется компилятором во время компиляции

Важно держать это различие в голове.

19. Для чего хорошо подходит Source Generator

Source Generator - это не инструмент, которым стоит генерировать вообще всё подряд.

Он хорошо подходит для такого кода.

Скучно и легко ошибиться, если писать вручную
Механически определяется из входных данных
Результат генерации легко читается
Позволяет сократить использование Reflection во время выполнения
Улучшает совместимость с AOT и trimming
Повышает типобезопасность

Приведём примеры.

Метаданные для JSON-сериализации
Код регистрации в DI
Аксессоры конфигурационных значений
API-клиенты
Код преобразования enum
Генерация типов из определений SQL или CSV
Вспомогательный код для INotifyPropertyChanged

Однако если сгенерированный код слишком сложен, при возникновении проблем его трудно отследить.

При использовании Source Generator стоит держать в уме следующее.

Сделать возможным просмотр сгенерированного кода
Обеспечить стабильность имён генерируемого кода
Сделать результат генерации детерминированным
Сделать Diagnostic при ошибках понятным
Не допускать, чтобы небольшое изменение входных данных давало огромный дифф

Сгенерированный код не должен выглядеть как магия. Важно выдавать код, который сможет прочитать будущий сопровождающий.

20. Для чего Source Generator не подходит

Есть виды обработки, для которых Source Generator не подходит.

Обработка, зависящая от состояния во время выполнения
Обработка, требующая сетевого доступа
Обработка, зависящая от текущих значений внешнего сервиса
Обработка, результат которой меняется каждый раз
Переписывание существующего исходного кода
Анализ огромного решения целиком

Поскольку Generator работает во время компиляции, медленный генератор ухудшает время сборки и опыт работы в IDE.

Кроме того, генератор, зависящий от окружения, порождает такие проблемы.

Работает на компьютере разработчика, но падает на CI
Работает на CI, но падает на другой ОС
Результат зависит от состояния кэша
Сбой сети приводит к падению сборки

Source Generator лучше по возможности приближать к чистой (pure) функции.

Вход: исходный код, AdditionalFiles, AnalyzerConfigOptions
Выход: сгенерированный код C#, Diagnostic

Чем яснее эта зависимость, тем стабильнее получается генератор.

21. Инструменты исследования кода на основе Roslyn

Применение Roslyn не ограничивается Analyzer и Source Generator. Использование Roslyn из собственного консольного инструмента тоже эффективно на практике.

Например, встречаются такие требования.

Составить список мест, использующих устаревший API
Посчитать число public-классов по каждому проекту
Выгрузить в CSV классы с определённым атрибутом
Изучить пространства имён, от которых зависит огромное решение
Выявить Windows-специфичные API перед миграцией с .NET Framework

В таких случаях зачастую удобнее сделать не Analyzer, а разовый или периодически запускаемый инструмент исследования.

В качестве примера - простой набросок, перечисляющий public-классы в решении.

using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.MSBuild;

MSBuildLocator.RegisterDefaults();

using var workspace = MSBuildWorkspace.Create();
var solution = await workspace.OpenSolutionAsync(args[0]);

foreach (var project in solution.Projects)
{
    foreach (var document in project.Documents)
    {
        var root = await document.GetSyntaxRootAsync();
        if (root is null)
        {
            continue;
        }

        var classes = root.DescendantNodes()
            .OfType<ClassDeclarationSyntax>()
            .Where(c => c.Modifiers.Any(m => m.Text == "public"));

        foreach (var cls in classes)
        {
            Console.WriteLine($"{project.Name},{document.FilePath},{cls.Identifier.Text}");
        }
    }
}

В этом примере рассматривается только синтаксис. Если нужно найти «public-классы, унаследованные от определённого базового класса», потребуется через SemanticModel изучить иерархию наследования типов.

Если достаточно имени и формы - Syntax
Если нужны тип и место разрешения ссылки - SemanticModel
Если нужно работать со всем проектом - Workspace

Это базовое разделение сфер применения.

22. Отличие от регулярных выражений

Регулярные выражения удобны, но не подходят для работы со смыслом кода C#.

Рассмотрим, например, такой код.

// Console.WriteLine("debug");

Поиск Console.WriteLine регулярным выражением может захватить и текст внутри комментария.

Бывают и такие строковые литералы.

var text = "Console.WriteLine";

Либо вызов может быть разбит переводом строки.

Console
    .WriteLine("Hello");

Более того, может использоваться псевдоним.

using C = System.Console;

C.WriteLine("Hello");

Корректно обработать всё это регулярными выражениями сложно.

С Roslyn можно чётко различать комментарии, строковые литералы, синтаксические вызовы методов и фактически разрешённый метод.

Разумеется, для простого поиска порой достаточно grep или ripgrep. Но если на основе результатов планируется принимать архитектурные решения или выполнять автоматическое исправление, безопаснее использовать Roslyn.

Для приблизительного поиска - поиск по строке
Для корректного определения как кода C# - Roslyn

23. Использование Roslyn для изучения существующих активов

При миграции с .NET Framework на современный .NET первым делом требуется «понять текущее состояние». Здесь Roslyn оказывается очень полезен.

Например, такие исследования.

Список зависимостей от System.Web
Список кода, рассчитанного на App.config / Web.config
Места использования API, специфичных для Windows Forms / WPF
Места использования Remoting / BinaryFormatter
Наличие COM-ссылок
Список P/Invoke
I/O-операции, не переведённые на асинхронность
Места использования устаревших криптографических API

Кандидатов можно получить и простым поиском по строке. Но с Roslyn список можно строить на основе результатов, разрешённых как типы и методы.

Например, если просто искать строку BinaryFormatter, будут захвачены и комментарии, и документация.

Если с помощью Roslyn искать использование типа System.Runtime.Serialization.Formatters.Binary.BinaryFormatter, можно получить гораздо более точных кандидатов.

В работе по миграции не нужно с самого начала делать идеальный Analyzer. Уже сама возможность консольного инструмента-исследователя выдать такой CSV представляет ценность.

Project,File,Line,Symbol,Kind
Legacy.Web,Controllers/HomeController.cs,42,System.Web.HttpContext.Current,Property
Legacy.Core,Serialization/OldStore.cs,18,System.Runtime.Serialization.Formatters.Binary.BinaryFormatter,Type

Имея такой список, гораздо проще составить план миграции.

24. Roslyn для разработчиков библиотек

Roslyn полезен не только разработчикам приложений, но и разработчикам библиотек. У любой библиотеки есть правильный способ использования.

Например, такие правила.

Сначала нужно вызвать метод инициализации
Нужно применить определённый атрибут
Нужно вызвать Dispose
Определённые настройки опций опасны
Не следует использовать устаревший API в новом коде

Если сообщать об этом только через документацию, пользователи могут это пропустить.

Если включить Analyzer в NuGet-пакет библиотеки, можно выдавать предупреждения прямо в коде пользователя.

Например, для собственной библиотеки Company.Messaging можно обнаруживать такое неправильное использование.

var client = new MessageClient();
client.Send(message); // Отправка выполняется до вызова Configure

Analyzer может выдать такое предупреждение.

CMP1001: Перед вызовом MessageClient.Send необходимо вызвать Configure

Кроме того, через Code Fix можно предложить варианты исправления и примеры кода. Это улучшает опыт использования библиотеки.

Донести то, что написано в документации, прямо в редакторе пользователя

Эта идея - одна из главных ценностей Roslyn.

25. Подход к распространению Analyzer через NuGet

Analyzer можно распространять как NuGet-пакет. Однако его нужно рассматривать отдельно от обычной библиотеки времени выполнения, потому что Analyzer не нужен приложению во время выполнения - он используется во время сборки и в IDE.

Поэтому при проектировании пакета стоит продумать такие моменты.

Включать ли библиотеку времени выполнения и Analyzer в один пакет
Делать ли Analyzer отдельным пакетом
Выдавать ли предупреждения по умолчанию
Какой задать уровень серьёзности
Обеспечить ли управление через .editorconfig
Не обрушится ли на существующих пользователей внезапный поток предупреждений

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

Если распространять как публичную библиотеку, нужно заботиться о том, чтобы внезапно не сломать сборку у пользователей.

Часто удобнее начать с Info или Warning, дав пользователям возможность при необходимости самим поднять уровень до Error.

26. Roslyn и функции IDE

В Visual Studio и других средах разработки .NET идеи Roslyn глубоко связаны и с функциями самой IDE.

Например, такие функции.

IntelliSense
Переход к определению (Go to Definition)
Поиск всех ссылок (Find All References)
Переименование (Rename)
Извлечение метода (Extract Method)
Быстрые действия (Quick Actions)
Предупреждения о стиле кода
Обнаружение неиспользуемых using

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

class User
{
    public string Name { get; set; }
}

class Product
{
    public string Name { get; set; }
}

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

API Roslyn служит фундаментом, позволяющим применять такие возможности уровня IDE и в собственных инструментах.

27. Замечания по производительности

Roslyn мощный инструмент, но, если написать тяжёлую обработку, она, естественно, будет работать медленно. Особенно Analyzer и Source Generator могут выполняться прямо во время набора текста разработчиком или во время сборки.

Поэтому стоит обращать внимание на следующее.

Избегать лишнего получения SemanticModel
Сначала сужать кандидатов по Syntax, затем делать семантический анализ
Избегать файлового I/O
Не выполнять сетевой доступ
Избегать тяжёлого Reflection
Учитывать запросы на отмену
Помнить о параллельном выполнении
Не переносить анализ всего решения в Analyzer

В Analyzer стоит максимально сузить набор объектов, регистрируемых в Initialize.

Плохой пример.

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

Хорошее направление.

Регистрировать только нужные SyntaxKind
Сначала легко сужать по имени или форме
Уточнять через SemanticModel только когда это действительно нужно

Analyzer может постоянно находиться в среде разработки пользователя. Поэтому лёгкость - такая же часть качества, как и точность.

28. Проектирование Diagnostic

Diagnostic, выдаваемый Analyzer-ом, - это не просто «показать предупреждение». Разработчик, увидев его, должен понять следующее.

Что является проблемой
Почему это проблема
Что именно нужно исправить
Как это исправить
Есть ли исключения

Пример плохого сообщения.

CMP001: Запрещено

По такому сообщению непонятно, что именно плохо.

Пример хорошего направления.

CMP001: DateTime.Now зависит от локального времени среды выполнения. Для времени, предназначенного для хранения или сравнения, используйте DateTimeOffset.UtcNow или провайдер времени.

Стоит заранее продумать и структуру Diagnostic ID.

CMP0001-CMP0999: общие правила
CMP1000-CMP1999: правила библиотеки A
CMP2000-CMP2999: правила поддержки миграции

Если есть возможность подготовить страницу документации, полезно также задать HelpLinkUri в DiagnosticDescriptor.

Предупреждение - это форма общения с разработчиком. Если сообщение сформулировано небрежно, доверие теряет и само правило.

29. Проектирование уровня серьёзности

Уровень серьёзности Analyzer следует определять осторожно. Основные градации таковы.

Hidden / Silent
Info
Suggestion
Warning
Error

На практике зачастую лучше не выставлять сразу Error. Особенно при большом объёме существующего кода включение Error с самого начала останавливает внедрение.

Реалистичнее всего внедрять поэтапно, например так.

1. Сначала внедрить как Warning
2. Сделать число предупреждений видимым в CI
3. Не допускать появления новых нарушений
4. Поднять до Error только важные правила
5. Составить план сокращения существующих нарушений

Цель Analyzer - не мучить разработчиков, а без лишнего напряжения повышать качество кодовой базы.

30. Отладка Source Generator

Source Generator выполняется не там, где обычное приложение, поэтому его отладка имеет свои особенности.

В основном для исследования применяются такие способы.

Посмотреть сгенерированный исходный код
Выдавать Diagnostic
Писать тесты
При необходимости подключить отладчик

В проектах SDK-style проще проверять результат, если включить настройку вывода сгенерированных файлов.

<PropertyGroup>
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
  <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

Это упрощает проверку сгенерированных файлов .g.cs.

$(BaseIntermediateOutputPath) обычно указывает на путь внутри obj/.

Если указать путь прямо в корне проекта, например Generated, то, поскольку в проектах SDK-style по умолчанию в компиляцию включается **/*.cs, при следующей сборке уже сгенерированные .g.cs могут снова попасть в неё как обычные исходники, что приведёт к ошибкам дублирования типов и членов.

Если вывод в корень проекта необходим во что бы то ни стало, следует явно исключить эти файлы из компиляции, например через <Compile Remove="Generated/**/*.cs" />.

В тестах для Generator часто применяется подход сравнения входного кода с результатом генерации.

Подготовить исходный код на входе
Запустить Generator
Проверить сгенерированный исходный код
Проверить ожидаемый Diagnostic

Если проверять работу Source Generator только вручную, он быстро ломается. Чем сложнее логика генерации, тем важнее тесты.

31. Тестирование с Roslyn

Analyzer и Source Generator стоит развивать, покрывая тестами. Особенно для Analyzer проблемой становятся и ложные срабатывания, и пропуски.

В тестах стоит подготовить такие сценарии.

Код, который должен быть обнаружен
Код, который не должен быть обнаружен
Код с использованием using alias
Код с полностью квалифицированным именем
Код с другим типом, имеющим похожее имя
Код, помеченный как сгенерированный (generated code)
Код при включённом nullable

Например, для Analyzer, запрещающего System.DateTime.Now, стоит проверить такие случаи.

// Должно быть обнаружено
var x = System.DateTime.Now;
// Должно обнаруживаться и при наличии using
using System;
var x = DateTime.Now;
// Не должно обнаруживаться, если это другой тип
namespace MyCompany;

public static class DateTime
{
    public static string Now => "now";
}

var x = DateTime.Now;

Этот последний случай - типичный пример, где легко ошибиться при поиске по строке. В Roslyn Analyzer этого удаётся избежать, проверяя целевой символ через SemanticModel.

32. Можно ли использовать в проектах на .NET Framework

Roslyn - это не только инструмент для современного .NET. Однако то, на что нужно обращать внимание, зависит от того, «как именно» его использовать.

При использовании как инструмента исследования

Реалистичный вариант - собрать Roslyn-инструмент как консольное приложение на .NET 8 или .NET 10 и загружать и анализировать им решение на .NET Framework.

В этом случае сам инструмент работает на современном .NET, а объектом анализа может выступать код на .NET Framework.

Однако для загрузки решения через MSBuildWorkspace требуется среда, способная собрать целевые проекты: MSBuild, SDK, ссылочные сборки и восстановление NuGet.

Иначе говоря, одним только Roslyn прочитать всё не получится - для разрешения реальной конфигурации проекта нужна среда сборки.

При использовании как Analyzer

Analyzer работает, будучи загруженным компилятором или IDE.

Даже если целевой проект на .NET Framework, его можно использовать, если окружение позволяет компилятору загружать Analyzer.

Однако при старом формате csproj, старой Visual Studio, старом MSBuild или конфигурации на базе packages.config внедрение и эксплуатация могут оказаться не такими простыми, как в современных проектах SDK-style.

При внедрении в существующий проект на .NET Framework стоит сначала проверить следующее.

Версии Visual Studio / MSBuild
Можно ли использовать PackageReference
Работает ли тот же Analyzer на CI
Появляются ли предупреждения в логе сборки
Действует ли .editorconfig

При использовании как Source Generator

Source Generator - это механизм, который компилятор загружает во время компиляции.

Поэтому важнее не фреймворк времени выполнения целевого проекта, а то, поддерживают ли его компилятор и SDK, используемые для сборки.

В проектах SDK-style на современном .NET с ним удобно работать, тогда как в старых проектах на .NET Framework нужна осторожность в зависимости от формата проекта и среды сборки.

Для существующих активов на .NET Framework зачастую безопаснее начинать не со встраивания Source Generator, а с исследовательских инструментов и Analyzer на базе Roslyn.

33. Осторожность при выборе версии

К NuGet-пакетам, связанным с Roslyn, относится семейство Microsoft.CodeAnalysis.*.

Вот основные из них.

Microsoft.CodeAnalysis.CSharp
Microsoft.CodeAnalysis.CSharp.Workspaces
Microsoft.CodeAnalysis.Workspaces.MSBuild
Microsoft.CodeAnalysis.Analyzers
Microsoft.CodeAnalysis.CSharp.CodeFix.Testing
Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing

Здесь стоит обратить внимание на то, что Analyzer и Source Generator загружаются компилятором на стороне пользователя.

Иначе говоря, если SDK / Visual Studio у разработчика или на CI устарели, Analyzer / Generator, использующий слишком новый API Roslyn, может не заработать.

Если использование строго внутреннее и среду сборки можно унифицировать, относительно новый API применять проще.

С другой стороны, для внешне распространяемых библиотек версию Microsoft.CodeAnalysis, от которой зависят, нужно выбирать осторожно, учитывая широкий спектр окружений пользователей.

В качестве ориентира можно руководствоваться таким подходом.

Только для внутреннего использования: выровнять CI и среду разработки и использовать более новый API
Внешнее распространение: выбирать консервативно, учитывая диапазон SDK/VS у пользователей
Generator: по возможности проектировать как Incremental Generator
Analyzer: отдавать приоритет лёгкости, не портящей опыт работы в IDE

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

34. Не пытайтесь решить с помощью Roslyn абсолютно всё

Roslyn мощный инструмент, но он не решает абсолютно все проблемы. Например, следующие вопросы одним только Roslyn не решить.

Какая ветка выполнится во время выполнения
Какие значения придут на реальных данных
Методы, вызываемые динамически через reflection
Результат регистрации в DI-контейнере во время выполнения
Обработка, меняющаяся в зависимости от конфигурационного файла
Значения, возвращаемые внешним сервисом

Roslyn - это прежде всего инструмент для работы с исходным кодом и информацией компиляции. Если нужно узнать поведение во время выполнения, требуются другие средства: тесты, логи, трассировка, профилирование, анализ дампов.

Поэтому роль Roslyn стоит понимать так.

Работать с высокой точностью с тем, что можно узнать статически

Если пытаться силой решить с помощью Roslyn и то, что можно узнать только динамически, получится сложный и неточный механизм.

35. Порядок внедрения

Если начинать использовать Roslyn на практике, рекомендуется такой порядок.

1. Привести в порядок существующие Analyzer .NET и .editorconfig
2. Написать небольшой инструмент исследования на основе Syntax Tree
3. Попробовать разрешение типов через SemanticModel
4. Прочитать решение через MSBuildWorkspace
5. Создать небольшой Analyzer, специфичный для команды
6. При необходимости добавить Code Fix
7. Рассмотреть Source Generator там, где много шаблонного кода

Не нужно сразу браться за Source Generator. На большинстве проектов Analyzer и инструменты исследования дают эффект быстрее.

Особенно при большом объёме существующих активов реалистичен такой порядок действий.

Понять текущее состояние с помощью инструмента исследования
Превратить часто встречающиеся проблемы в Analyzer
Превратить в Code Fix только то, что можно безопасно исправить
Превратить многократно повторяющийся шаблонный код в Generator

Roslyn - это инструмент, который можно внедрять постепенно.

36. Небольшой пример: составление списка вызовов методов

Напоследок посмотрим на использование Roslyn в форме, чуть более близкой к реальной практике. Здесь показан набросок, составляющий список вызовов методов в решении.

using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.MSBuild;

MSBuildLocator.RegisterDefaults();

using var workspace = MSBuildWorkspace.Create();
var solution = await workspace.OpenSolutionAsync(args[0]);

foreach (var project in solution.Projects)
{
    var compilation = await project.GetCompilationAsync();
    if (compilation is null)
    {
        continue;
    }

    foreach (var document in project.Documents)
    {
        var tree = await document.GetSyntaxTreeAsync();
        if (tree is null)
        {
            continue;
        }

        var root = await tree.GetRootAsync();
        var semanticModel = compilation.GetSemanticModel(tree);

        var invocations = root
            .DescendantNodes()
            .OfType<InvocationExpressionSyntax>();

        foreach (var invocation in invocations)
        {
            var symbol = semanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol;
            if (symbol is null)
            {
                continue;
            }

            var lineSpan = invocation.GetLocation().GetLineSpan();
            var line = lineSpan.StartLinePosition.Line + 1;

            Console.WriteLine(string.Join(",", new[]
            {
                project.Name,
                document.FilePath ?? document.Name,
                line.ToString(),
                symbol.ContainingType.ToDisplayString(),
                symbol.Name
            }));
        }
    }
}

Немного расширив такой инструмент, можно проводить такие исследования.

Извлекать только вызовы определённого метода
Выводить места использования устаревшего API
Выводить частоту использования по каждому проекту
Составлять список API, подлежащих миграции

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

37. Замечания при переписывании кода с помощью Roslyn

С помощью Roslyn можно также переписывать код, используя синтаксическое дерево.

Например, можно изменить имя определённого метода, добавить атрибут или добавить using.

Однако переписывание кода следует выполнять осторожно. Вот на что стоит обратить внимание.

Убедиться, что смысл не изменился
Не ломать комментарии и пробелы
Не допускать слишком большого диффа
Сохранять единообразие форматирования
Не выполнять слишком много преобразований за раз
Делать Git-дифф удобным для ревью

Поскольку Syntax Tree в Roslyn сохраняет Trivia, возможны преобразования с сохранением комментариев и пробелов. Но при небрежном создании узлов форматирование сгенерированного кода может нарушиться.

При создании инструмента переписывания безопасен следующий подход.

Сначала выполнять только обнаружение
Проверять дифф до и после преобразования
Начинать с небольших преобразований
Писать тесты для самого инструмента преобразования
На CI начинать с режима только обнаружения

При масштабных механических преобразованиях Roslyn мощен, но в конце всё равно нужно ревью человеком.

38. Roslyn и AI-помощь в написании кода

В последние годы генерация кода и помощь в ревью с помощью ИИ стали обычным делом, но ценность Roslyn от этого не снижается. ИИ хорошо справляется с естественным языком и окружающим контекстом, а Roslyn как компилятор хорошо справляется с точной синтаксической и семантической информацией. Они не столько конкурируют, сколько дополняют друг друга.

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

Точно извлекать целевые места с помощью Roslyn
Генерировать стратегию исправления и пояснения с помощью ИИ
Проверять компилируемость предложенного исправления с помощью Roslyn
Предотвращать повторение с помощью Analyzer

Порой безопаснее сначала точно извлечь целевые места с помощью Roslyn, чем просить ИИ «исправить все устаревшие API в этой кодовой базе».

А использование ИИ для проработки стратегии исправления и помощи в ревью более практично.

То, что известно компилятору, стоит доверить компилятору. Человек и ИИ пусть сосредоточатся на суждениях более высокого уровня.

Именно это разделение обязанностей важно.

39. Практический чек-лист

Перед использованием Roslyn стоит проверить следующее.

Какова цель: исследование, предупреждения, исправление или генерация
Достаточно ли одного синтаксиса или нужен семантический анализ
Достаточно ли одного файла или нужен весь проект
Нужно ли работать внутри IDE или достаточно разового инструмента
Допустимо ли влияние на время сборки
Будет ли выполняться на CI
Не вызовет ли это лавину предупреждений в существующем коде
Какой задать уровень серьёзности Analyzer
Можно ли безопасно применить Code Fix
Можно ли проверить код, сгенерированный Source Generator
Согласованы ли версии SDK / Visual Studio у пользователей

Если сомневаетесь, стоит разделять так.

Хочу исследовать                     -> консольный инструмент на Roslyn
Хочу обеспечить постоянное соблюдение -> Analyzer
Способ исправления уже определён      -> Code Fix
Хочу генерировать шаблонный код        -> Source Generator

Такое разделение снижает вероятность ошибиться с тем, где именно применять Roslyn.

40. Итоги

Roslyn открывает компилятор C# и Visual Basic разработчикам в виде доступного API.

С Roslyn исходный код можно обрабатывать не как простую строку, а в таких формах.

Читать синтаксис как Syntax Tree
Читать смысл через SemanticModel
Работать со всей компиляцией как с Compilation
Работать с решением и проектами через Workspace
Выдавать предупреждения как Analyzer
Предлагать варианты исправления как Code Fix
Генерировать код как Source Generator

На практике это особенно полезно в таких ситуациях.

Исследование существующей кодовой базы
Помощь в миграции с .NET Framework на .NET
Автоматическая проверка командных соглашений
Руководство для пользователей библиотеки
Генерация шаблонного кода
Обеспечение качества в IDE и на CI

Важно не относиться к Roslyn слишком настороженно, как к «сложной технологии компиляторов».

Для начала достаточно прочитать один файл через CSharpSyntaxTree.ParseText и перечислить имена методов. А дальше можно постепенно расширяться до SemanticModel, Workspace, Analyzer и Source Generator.

Если выразить суть Roslyn одной фразой, получится так.

Позволяет работать с кодом C# не как со строкой, а как со структурой, понятой компилятором.

Имея такой взгляд, гораздо проще даётся автоматизация код-ревью, миграции, исследования и генерации кода.

Справочные материалы

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Что такое Roslyn?
Roslyn, официально называемый .NET Compiler Platform, - это реализация компилятора C# и Visual Basic, а также набор API для создания инструментов анализа кода. Он делает доступной для приложений и инструментов информацию, которую компилятор - раньше воспринимавшийся как чёрный ящик - создаёт внутри себя (например, какому типу принадлежит идентификатор или какому методу соответствует вызов). Благодаря этому код C# можно читать не как строку, а как синтаксис, не по внешнему виду, а по смыслу, и на этой основе выдавать предупреждения, варианты исправлений и сгенерированный код.
Что можно делать с помощью Roslyn?
Можно выполнять синтаксический анализ C# / VB, семантический анализ типов и методов, анализ целых проектов и решений, создавать собственные Analyzer, Code Fix и Source Generator, а также генерировать и преобразовывать код. Если говорить более практично, это, например, превращение использования запрещённого API в предупреждение сборки, составление списка мест с устаревшим API, генерация кода маппинга DTO на этапе компиляции или помощь в исследовании миграции с .NET Framework на .NET. Способы использования в целом делятся на четыре: использование как библиотеки, Analyzer, Code Fix и Source Generator.
Чем это отличается от поиска кода с помощью регулярных выражений или grep?
Регулярным выражениям сложно корректно обрабатывать текст внутри комментариев, строковые литералы, вызовы, разбитые переводом строки, и вызовы через псевдоним (using alias). С Roslyn можно чётко различать комментарии, строковые литералы, синтаксические вызовы методов и фактически разрешённый метод. Для приблизительного поиска порой достаточно поиска по строке, но если на основе результатов планируется принимать архитектурные решения или выполнять автоматическое исправление, безопаснее Roslyn, поскольку он позволяет судить на основе результатов разрешения имён самим компилятором.
С чего лучше начать изучение Roslyn?
Не обязательно сразу браться за Source Generator. Рекомендуется сначала привести в порядок Analyzer, уже входящие в .NET SDK, и файл .editorconfig, затем написать небольшой инструмент исследования, который читает один файл через CSharpSyntaxTree.ParseText и перечисляет имена методов, а после этого постепенно расширяться: разрешение типов через SemanticModel, загрузка решения через MSBuildWorkspace и, наконец, небольшой Analyzer, специфичный для команды. На большинстве проектов Analyzer и инструменты исследования дают эффект быстрее, чем Source Generator.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог