Недавно мне понадобилось написать генератор кода для одного из своих проектов. Так как надо было обеспечить поддержку 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.
bustedbunny
На самом деле Unity 2022.2+ поддерживает 4.0.1 roslyn.
А по виджетам не совсем понятно почему просто нельзя сохранить class declaration в переменную и явно добавить всё необходимое?
Например:
myClass.WithMembers(member1,member2)