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

· Обновлено: · · .NET, C#, Roslyn, Analyzer, Source Generator, Компилятор, Статический анализ, Генерация кода, Существующий код

История изменений (1 обновлений, последнее 30 Aug 2026)

Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.

Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
Первая публикация
Цитирование статьи(DOI (зарегистрированный архив): 10.5281/zenodo.21619903)

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

Го Комура (2026). Roslyn: как читать, править и генерировать C# глазами компилятора. KomuraSoft LLC. https://comcomponent.com/ru/blog/2026/06/10/002-roslyn-dotnet-compiler-platform/

DOI (зарегистрированный архив)
10.5281/zenodo.21619903
DOI (последняя зарегистрированная версия)
10.5281/zenodo.21619904

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 открывает приложениям и инструментам в виде API ту информацию, которой располагает компилятор C# и Visual Basic.

Коротко: с 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)

Что читать в зависимости от цели

В статье 40 глав. Главы для тех, кто хочет только включить готовые Analyzer и получить эффект, и для тех, кто хочет написать Analyzer сам, почти не пересекаются. Ниже — по целям.

Цель Какие главы читать Комментарий
Сначала включить готовые Analyzer и получить эффект глава 13 → 14 → 29 Свой Analyzer не нужен. В центре .editorconfig
Понять, что такое Roslyn глава 2 → 4 → 5 → 8 → 9 Концепции. Код почти не обязателен
Написать инструмент обследования существующего кода глава 5 → 8 → 11 → 21 → 23 → 36 Главные герои — Syntax Tree и Workspace
Написать Analyzer под правила своей компании глава 12 → 15 → 28 → 29 → 31 Дальше вы уже на стороне автора
Ещё и распространять Code Fix к предыдущему пункту глава 16 → 25  
Оценить Source Generator глава 17 → 18 → 19 → 20 → 30 Сначала 19 и 20 — «где подходит / где нет»: так быстрее решить
Применить к коду на .NET Framework глава 32 → 33  
Решить, с чего начать глава 35 (порядок внедрения) → 39 (практический чек-лист)  

Концепции (главы 2–11) и практику (главы 13–37) не обязательно читать подряд. Если писать свои инструменты не планируете, самый выгодный путь — главы 13 и 14 и настройка .editorconfig.

Первый шаг: что подготовить

Набор зависит от того, что вы делаете. В тексте это размазано по главам, поэтому список — здесь. Подробности в соответствующих главах.

Что делаете Вид проекта Основные пакеты NuGet
Инструмент обследования одного файла обычное консольное приложение (актуальный .NET) Microsoft.CodeAnalysis.CSharp
Инструмент обследования всего решения обычное консольное приложение (актуальный .NET) к предыдущему Microsoft.CodeAnalysis.Workspaces.MSBuild, Microsoft.Build.Locator
Analyzer / Code Fix библиотека классов, netstandard2.0 Microsoft.CodeAnalysis.CSharp, Microsoft.CodeAnalysis.Analyzers
Source Generator библиотека классов, netstandard2.0 Microsoft.CodeAnalysis.CSharp
Тесты Analyzer / Generator тестовый проект (актуальный .NET) Microsoft.CodeAnalysis.CSharp.CodeFix.Testing, Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing

Целевой фреймворк критичен для Analyzer и Source Generator. Их загружает не ваше приложение, а компилятор на стороне пользователя, поэтому целевой netstandard2.0 — обычная практика. Шаблон Visual Studio так и называется: «Analyzer with Code Fix (.NET Standard)». Как выбирать версии — в главе 33.

Если начинаете с шаблона Visual Studio, сначала поставьте .NET Compiler Platform SDK. В Visual Studio Installer на вкладке «Отдельные компоненты» отметьте «.NET Compiler Platform SDK» (раздел “Compilers, build tools, and runtimes”) либо выберите его как необязательный компонент рабочей нагрузки «Разработка расширений Visual Studio». Сама рабочая нагрузка этот SDK не ставит — галочку нужно поставить явно.

После установки в File > New > Project поиск по «Analyzer» находит «Analyzer with Code Fix (.NET Standard)». Шаблон сразу создаёт пять проектов: сам Analyzer, Code Fix, упаковку в NuGet, модульные тесты и VSIX для проверки. VSIX по умолчанию — стартовый проект: при запуске открывается второй Visual Studio с вашим Analyzer.

На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 19, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle

2. Что такое Roslyn

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

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

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

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

На деле компилятор не просто переводит текст в машинный код или IL. Во время компиляции он собирает сведения вроде таких.

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

Roslyn отдаёт эту информацию разработчику. Поэтому Roslyn — не просто компилятор, а платформа, через которую можно понимать код.

3. Что можно делать с 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, комментарии можно было бы отбросить. В практических преобразованиях кода важно не ломать комментарии и переводы строк.

Как посмотреть дерево своими глазами

До сих пор Node / Token / Trivia были словами. Syntax Tree быстрее понять по живому коду. Есть два способа.

1. Syntax Visualizer в Visual Studio

Появляется после установки .NET Compiler Platform SDK из главы 1.

View > Other Windows > Syntax Visualizer

Ставите курсор в редакторе — соответствующий узел подсвечивается в дереве. Выбираете узел в дереве — выделяется фрагмент в редакторе. Цвета разные: SyntaxNode — синий, SyntaxToken — зелёный, SyntaxTrivia — красный. Три вида из этой главы сразу видны цветом.

Если нужна ещё и графовая схема, в Visual Studio Installer на вкладке «Отдельные компоненты» поставьте DGML editor (раздел “Code tools”).

2. sharplab.io

sharplab.io — площадка C# / VB / F# прямо в браузере. Слева пишете код, справа переключаете декомпиляцию в C#, IL и нативный код JIT.

Само Syntax Tree удобнее смотреть в Syntax Visualizer. sharplab лучше отвечает на вопрос «во что на самом деле разворачивается этот синтаксический сахар». Видно, например, как оператор using становится try / finally. Так проще почувствовать тему главы 4: Roslyn работает со смыслом, а не с внешним видом.

Практический приём при написании Analyzer: сначала в Syntax Visualizer посмотрите имя типа узла (MemberAccessExpressionSyntax и подобные) и SyntaxKind — и только потом пишите код. Возвратов назад становится меньше. То, что Analyzer в главе 15 регистрирует SyntaxKind.SimpleMemberAccessExpression, как раз можно увидеть этим способом.

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

Syntax Tree в Roslyn неизменяемо (immutable). Полученное дерево не правят на месте: создают новое дерево с внесёнными изменениями.

Даже чтобы сменить имя метода, существующий MethodDeclarationSyntax не модифицируют.

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

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

У неизменяемости несколько плюсов.

Проще работать из нескольких потоков
Безопаснее брать снимок кода, который как раз правят в IDE
Проще строить diff
Проще сравнить состояние до и после изменения

Поначалу это кажется неудобным. Но в мире, где один и тот же код одновременно смотрят 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

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

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

У каждого своя цель.

Как библиотека

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

Подходящие сценарии примерно такие.

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

Инструмент запускают, когда удобно. Ему не нужно работать во время набора в IDE, поэтому относительно тяжёлая обработка допустима.

Написать Analyzer

Analyzer разбирает код и выдаёт предупреждения или ошибки.

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

Вместо DateTime.Now использовать DateTimeOffset.UtcNow
Имена 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 при ошибках понятным
Не допускать, чтобы небольшое изменение входа давало огромный diff

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

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

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

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

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

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

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

Source Generator лучше по возможности держать «чистым».

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

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

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

Roslyn нужен не только для Analyzer и Source Generator. Вызов Roslyn из своего консольного инструмента на практике тоже полезен.

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

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

В таких случаях часто удобнее не 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
Операции ввода-вывода, которые ещё не сделали асинхронными
Места вызовов старых криптографических 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); // Send вызван до 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, затем делать семантический анализ
Не делать файловый ввод-вывод
Не ходить в сеть
Не делать тяжёлый 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 только вручную, он быстро ломается. Чем сложнее логика генерации, тем важнее тесты.

Что проверять, если сгенерированного кода не видно

Когда Source Generator «как будто не работает», причина чаще не в логике генерации, а в способе ссылки или в кэше IDE. Ниже — что подозревать по симптомам.

Симптом Что проверить сначала
Сборка проходит, а ожидаемого типа нет Как ссылаются на проект Generator. Есть ли OutputItemType="Analyzer" и ReferenceOutputAssembly="false" (глава 18)
В obj/ нет .g.cs Включён ли EmitCompilerGeneratedFiles. Если включён, а файлов нет — Generator, скорее всего, вообще не вызывается
Сборка из командной строки зелёная, в IDE всё красное Кэш IDE. Перезапустить Visual Studio, закрыть решение и удалить obj/ и bin/
Generator поправили, а эффекта нет То же. DLL Generator иногда остаётся загруженной в процесс IDE
Ошибка про дублирование типов или членов Каталог генерации попал в компиляцию. Исключить его через <Compile Remove>, как выше
Ни ошибок, ни генерации Возможно, внутри Generator вылетело исключение. Оно не всегда всплывает в понятном виде

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

1. Собрать через dotnet build в командной строке и сравнить симптомы
   → если в командной строке проблемы нет, дело в кэше IDE
2. Включить EmitCompilerGeneratedFiles и проверить, появляются ли .g.cs
   → нет — Generator не вызывается (смотреть способ ссылки)
   → да — читать содержимое (проблема в том, что генерируется)
3. В точке входа Generator сообщить один Diagnostic и проверить, доходит ли выполнение туда
4. Если всё ещё непонятно — написать тест

Если на стороне Generator не глотать исключения, а ловить их через try / catch и самому сообщать через ReportDiagnostic, последний симптом из таблицы (тихо ничего не генерируется) встречается заметно реже.

Подключать отладчик стоит в последнюю очередь. Шаги 1–3 обычно быстрее. Generator вызывают и IDE, и сборка: если смотреть не тот процесс, время уйдёт впустую.

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.

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

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

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

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

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

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

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

В последние годы генерация кода и помощь в ревью с помощью ИИ стали обычным делом, но ценность 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 для инструментов анализа кода. Через эти 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, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

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

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