Недавно мне понадобилось написать генератор кода для одного из своих проектов. Так как надо было обеспечить поддержку Unity 2021, от более современного API — incremental generators пришлось отказаться сразу. Но пост не об этом, а о том, как повысить читаемость и поддерживаемость синтаксического дерева для генерации исходного кода.

Допустим нам надо сгенерировать следующий класс:

[MyTestAttribute]
public class TestClass
{
    public string Value { get; set; } = "default";
}

Синтаксическое дерево для такого простого класса будет выглядеть так:

ClassDeclaration("TestClass")
.WithAttributeLists(
    SingletonList(
      AttributeList(
        SingletonSeparatedList(
          Attribute(
            IdentifierName("MyTestAttribute"))))))
.WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword)))
.WithMembers(
    SingletonList<MemberDeclarationSyntax>(
      PropertyDeclaration(PredefinedType(Token(SyntaxKind.StringKeyword)), Identifier("Value"))
        .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword)))
        .WithAccessorList(
          AccessorList(List(new[] {
                    AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
                      .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)),
                    AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
                      .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) })))
        .WithInitializer(EqualsValueClause(
          LiteralExpression(SyntaxKind.StringLiteralExpression, Literal("default"))))
        .WithSemicolonToken(Token(SyntaxKind.SemicolonToken))))

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

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

Используя виджеты, код для генерации нашего класса будет выглядеть так:

ClassWidget(
    identifier: "TestClass",
    modifier: SyntaxKind.PublicKeyword,
    attribute: Attribute(IdentifierName("MyTestAttribute")),
    member: PropertyWidget(
        identifier: "Value",
        type: PredefinedType(Token(SyntaxKind.StringKeyword)),
        modifier: SyntaxKind.PublicKeyword,
        accessors: new[]
        {
            SyntaxKind.GetAccessorDeclaration, 
            SyntaxKind.SetAccessorDeclaration
        },
        initializer: StringLiteralExpressionWidget("default")
    )
)

Уверен, даже если вы никогда не сталкивались с Roslyn Compiler API, вы всё равно поймете, каким будет рузультат выполнения данного кода, и затратите на это куда меньше сил и времени, в отличии от стандартного подхода.

ClassWidget под капотом
private static ClassDeclarationSyntax ClassWidget(
    string identifier,
    SyntaxKind? modifier = null,
    IEnumerable<SyntaxKind>? modifiers = null,
    BaseTypeSyntax? baseType = null,
    IEnumerable<BaseTypeSyntax>? baseTypes = null,
    MemberDeclarationSyntax? member = null,
    IEnumerable<MemberDeclarationSyntax>? members = null,
    AttributeSyntax? attribute = null,
    IEnumerable<AttributeSyntax>? attributes = null,
    bool addGeneratedCodeAttributes = false)
{
    var classDeclaration = ClassDeclaration(identifier);

    if (baseType is not null)
    {
        classDeclaration = classDeclaration
            .WithBaseList(BaseList(SingletonSeparatedList(baseType)));
    }

    if (baseTypes is not null)
    {
        classDeclaration = classDeclaration
            .WithBaseList(BaseList(SeparatedList(baseTypes)));
    }

    if (member is not null)
    {
        classDeclaration = classDeclaration
            .WithMembers(classDeclaration.Members.Add(member));
    }

    if (members is not null)
    {
        classDeclaration = classDeclaration
            .WithMembers(List(members));
    }

    return BaseWidgetDecoration(
        widget: classDeclaration,
        modifier: modifier,
        modifiers: modifiers,
        attribute: attribute,
        attributes: attributes,
        addGeneratedCodeAttributes: addGeneratedCodeAttributes);
}

Код генератора и все виджеты можно найти на GitHub.

Комментарии (3)


  1. bustedbunny
    02.06.2023 04:53
    +1

    На самом деле Unity 2022.2+ поддерживает 4.0.1 roslyn.

    А по виджетам не совсем понятно почему просто нельзя сохранить class declaration в переменную и явно добавить всё необходимое?

    Например: myClass.WithMembers(member1,member2)


  1. Str5Uts
    02.06.2023 04:53

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


    Имеется возможность использовать дополнительную логику, доступ к внешним источникам данных, схемам db и т.п.


  1. gev
    02.06.2023 04:53

    я генерирую код как-то так:

    получается вот такое: