Найти тему
ИРОКЕЗ КОНФОРМИСТА

Путешествие в Source Generator. Часть 2

Оглавление

Как и обещал в предыдущем посте, я продолжаю свое погружение в мир генераторов исходников с погружения в книгу рецептов. В действительности, это не совсем книга рецептов, а описание концепции генераторов исходников. Помимо кейсов применения генераторов во вступлении отдельно описано для чего эта технология НЕ предназначена. Отмечается, что генераторы исходников не предназначены для замены новых языковых функций: например, для реализации record-ов через генератор, который преобразует указанный синтаксис в компилируемое представление на языке C#. Это плохо, анти-паттерн черевато проблемами и вообще гадость. Будем считать вводную часть законченной и посмотрим на “рецепты”. Код моих воплощений с редкими правками будет в репозитории.

Генерация класса

В целом это мало чем отличается от примера рассмотренного в прошлой части, но давайте все равно взглянем. Интерес представляют всего два файла:

ClassGenerator.cs из проекта Generator:

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

namespace Generator
{
[Generator]
public class ClassGenerator:ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context) { }

public void Execute(GeneratorExecutionContext context)
{
context.AddSource("GeneratedClass.cs", SourceText.From(@"
namespace GeneratedNamespace
{
public class GeneratedClass
{
public static void GeneratedMethod()
{
Console.WriteLine(""Irokez go brrrrr! "");
}
}
}", Encoding.UTF8));
}
}
}

И Program.cs из прокта Demo :

GeneratedNamespace.GeneratedClass.GeneratedMethod();

При добавлении генератора в проект не забываем, что нужно не просто закинуть референс на проект нашего генератора, а сделать это правильно:

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

А то я вот забыл и потратил не лишние пять минут на разборки.

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

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

namespace Generator
{
[Generator]
public class ClassGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context) { }

public void Execute(GeneratorExecutionContext context)
{
context.AddSource("GeneratedClass.cs", SourceText.From($@"
namespace GeneratedNamespace
{{
public class GeneratedClass
{{
public static void GeneratedMethod()
{{
Console.WriteLine(""Compilated at: {DateTime.UtcNow} "");
Console.WriteLine(""Irokez go brrrrr! "");
}}
}}
}}", Encoding.UTF8));
}
}
}

Теперь, программа будет менять надпись в первой строке только в том случае, если проект генератора будет перекомпилирован.

Работа с дополнительными файлами

Используя в .csproj файле конструкцию вида <AdditionalFiles Include="MyAmazingFile" /> можно докидывать в проект дополнительные файлы. Зачем это нужно? Это хороший вопрос. В оригинале используют выдуманный преобразователь XML в C# код. Я такое придумать не смог, по этому просто решил генерировать метод, в который будет добавляться Console.WriteLine на каждую строчку из каждого дополнительного txt файла. Полный код примера, как всегда в репозитории, а теперь давайте взглянем на код генератора:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.IO;
using System.Linq;
using System.Text;

namespace Generator
{
[Generator]
public class AddGenerator : ISourceGenerator
{

public void Initialize(GeneratorInitializationContext context) { }

public void Execute(GeneratorExecutionContext context)
{
var sb = new StringBuilder();
var myFiles = context.AdditionalFiles.Where(f=>f.Path.EndsWith(".txt"));
foreach (var file in myFiles)
{
var content = File.ReadAllLines(file.Path);
foreach(var line in content)
{
sb.AppendLine($"Console.WriteLine(@\\"{line.Trim()}\\");");
}
}

context.AddSource($"MegaPrint.generated.cs", SourceText.From($@"
namespace TotalPrint
{{
public static class MegaPrintClass
{{

public static void GeneratedMethod()
{{
{sb.ToString()}
}}
}}
}}", Encoding.UTF8));

}
}
}

По сложившейся традиции метод Initialize у нас пустой. В методе Execute(), мы создаём новый StringBuilder, чтобы добавить весь наш будущий код. Затем мы проходимся по всем файлам с расширением .txt используя метод Where(), чтобы фильтровать список дополнительных файлов. Далее, мы читаем содержимое каждого из этих файлов и добавляем каждую строку в StringBuilder, используя форматирование строк для вставки каждой строки как вызов Console.WriteLine(). Затем мы генерируем новый C# файл MegaPrint.generated.cs с помощью метода AddSource().

Заключение

На этом на сегодня все. Мы научились создавать полностью кодогенерированный класс, который способен знать когда его скомпилировали. Также мы посмотрели на то, как использовать при генерации дополнительные файлы проекта. В следующем посте разберем еще несколько рецептов и продолжим наше путешествие в Source Generator.