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

Что такое генератор исходников?

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

Как работают генераторы исходного кода?

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

Source Generator анализирует синтаксические узлы и генерирует дополнительный исходный код на основе анализа. Этот сгенерированный код добавляется в компиляцию, как если бы он был написан разработчиком. Компиляция завершена, и сгенерированный код включен в сборку.

Когда использовать генераторы исходного кода?

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

  1. Сокращение стандартного кода: если вы обнаружите, что пишете один и тот же код снова и снова, генератор исходного кода может автоматически сгенерировать этот код для вас. Это может сэкономить время и снизить вероятность ошибок, вызванных ручным дублированием кода.
  2. Повышение производительности. Генераторы исходного кода могут генерировать код, оптимизированный для определенных сценариев, таких как сериализация данных или сетевое взаимодействие. Это может помочь повысить производительность приложения за счет уменьшения объема накладных расходов, необходимых для выполнения этих операций.
  3. Включение новых шаблонов кодирования. Генераторы исходного кода можно использовать для включения новых шаблонов кодирования, таких как внедрение зависимостей во время компиляции или аспектно-ориентированное программирование. Это может помочь разработчикам писать более модульный, поддерживаемый код, который легче тестировать и отлаживать.
  4. Адаптация к изменяющимся требованиям. Генераторы исходного кода можно использовать для адаптации кода к изменяющимся требованиям, таким как изменения во внешнем API или изменения в бизнес-логике. Это может помочь уменьшить количество необходимых обновлений кода вручную и повысить общую гибкость кодовой базы.
  5. Генерация кода для сквозных задач: Генераторы исходного кода можно использовать для генерации кода для сквозных задач, таких как ведение журнала, обработка ошибок или мониторинг производительности. Это может помочь обеспечить последовательное решение этих проблем во всем приложении и снизить риск ошибок, вызванных ручным кодированием.

Понимание компонентов генератора исходного кода

Генератор исходного кода состоит из двух основных компонентов, которые работают вместе, чтобы обеспечить плавный и эффективный процесс генерации кода:

ISyntaxReceiver и ISourceGenerator. Понимая компоненты и их взаимосвязь, вы можете создавать собственные генераторы исходного кода, адаптированные к вашим конкретным требованиям.

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

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

  1. ISyntaxReceiver собирает и фильтрует синтаксические узлы в процессе компиляции.
  2. ISourceGenerator получает отфильтрованные синтаксические узлы из ISyntaxReceiver.
  3. ISourceGenerator генерирует код на основе отфильтрованных узлов синтаксиса.

Вы также можете фильтровать синтаксические узлы непосредственно в методе Execute ISourceGenerator, но использование ISyntaxReceiver более эффективно, поскольку позволяет фильтровать синтаксические узлы в процессе компиляции, избегая ненужных накладных расходов.

Поэтапное создание пользовательского генератора исходного кода.

  1. Настройка проекта генератора исходного кода
    Создайте новый проект библиотеки классов (.NET Standard), предназначенный для netstandard2.0. Чтобы использовать генераторы исходного кода, вам необходимо добавить в проект следующие пакеты NuGet:
    - Microsoft.CodeAnalysis.CSharp
    - Microsoft.CodeAnalysis.Analyzers
  2. Реализация собственного синтаксического приемника
 public class CustomSyntaxReceiver : ISyntaxReceiver
 {
     public List<MethodDeclarationSyntax> CandidateMethods { get; } = new();
     public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
     {
         if (syntaxNode is MethodDeclarationSyntax methodDeclaration &&
             methodDeclaration.AttributeLists.Any(al => al.Attributes.Any(a => a.Name.ToString() == "MyCustomAttribute")))
         {
             CandidateMethods.Add(methodDeclaration);
         }
     }
}

3. Реализация собственного генератора исходного кода

 [Generator]
public class CustomSourceGenerator : ISourceGenerator
{
   public void Initialize(GeneratorInitializationContext context)
   {
       context.RegisterForSyntaxNotifications(() => new CustomSyntaxReceiver());
   }
   //Inside the `Execute` method, retrieve the SyntaxReceiver and generate code based on the filtered methods. Here's a simple example that generates a static class with a method that prints the names of the filtered methods:
   public void Execute(GeneratorExecutionContext context)
   {
       if (context.SyntaxReceiver is not CustomSyntaxReceiver receiver)
           return;
  
       var sourceBuilder = new StringBuilder();
       sourceBuilder.AppendLine("using System;");
       sourceBuilder.AppendLine("namespace GeneratedCode");
       sourceBuilder.AppendLine("{");
       sourceBuilder.AppendLine("    public static class GeneratedMethodsInfo");
       sourceBuilder.AppendLine("    {");
       sourceBuilder.AppendLine("        public static void PrintMethodNames()");
       sourceBuilder.AppendLine("        {");
  
       foreach (var method in receiver.CandidateMethods)
       {
           sourceBuilder.AppendLine($"            Console.WriteLine(\"{method.Identifier}\");");
       }
  
       sourceBuilder.AppendLine("        }");
       sourceBuilder.AppendLine("    }");
       sourceBuilder.AppendLine("}");
  
       context.AddSource("GeneratedMethodsInfo", sourceBuilder.ToString());
    }
}

4. Добавить исходный генератор из проекта в качестве анализатора

<!-- replacing paths and names appropriately -->
<ItemGroup>

    <ProjectReference Include="path-to-sourcegenerator-project.csproj" 
                      OutputItemType="Analyzer"
                      ReferenceOutputAssembly="false" />
</ItemGroup>

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

Таким образом, понимание компонентов генератора исходного кода и их взаимосвязей имеет решающее значение для создания пользовательских генераторов исходного кода, адаптированных к вашим конкретным требованиям. Используя возможности ISyntaxReceivers и ISourceGenerators, вы можете улучшить процесс разработки и в полной мере воспользоваться преимуществами генераторов исходного кода в C#.