Недавно мне понадобилось написать генератор кода для одного из своих проектов. Так как надо было обеспечить поддержку 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.

Only registered users can participate in poll. Log in, please.
А как вы генерируете код?
38.46%Использую StringBuilder15
15.38%Использую Raw String Literal6
17.95%Использую Roslyn Compiler API7
48.72%Не генерирую код19
39 users voted. 6 users abstained.