How to write a code generator that even your grandmother can support?

I recently needed to write a code generator for one of my projects. Since it was necessary to provide support for Unity 2021, the more modern API – incremental generators had to be abandoned immediately. But the post is not about that, but about how to improve the readability and maintainability of the syntax tree for generating source code.

Let’s say we need to generate the following class:

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

The syntax tree for such a simple class would look like this:

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))))

Looks intimidating doesn’t it? Of course, few people write it all by hand. As a rule, they use ready-made tools for generating, for example, RoslynQuoter. But the readability and maintainability of such code leaves much to be desired.

Luckily, we can simplify this code using widgets. I did not come up with anything new, but simply applied my experience with Flutter here as well.

Using widgets, the code to generate our class would look like this:

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")
    )
)

I am sure that even if you have never encountered the Roslyn Compiler API, you will still understand what the result of executing this code will be, and spend much less effort and time on it, unlike the standard approach.

ClassWidget under the hood
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);
}

The generator code and all widgets can be found at GitHub.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *