Генерация кода на языке C# с использованием Roslyn

Roslyn можно использовать не только для анализа кода, но и для генерации. В контексте генерации кода обычно рассматриваются два варианта – изменение структуры существующего кода и генерация кода с нуля. Рассмотрим оба варианта.

Генерация кода с чистого листа

Элементы дерева кода Roslyn Compiler API состоят из объектов, которые являются наследниками абстрактного класса SyntaxNode. Для генерации элементов синтаксического дерева используется вспомогательный класс Syntax.

Рассмотрим процесс генерации кода на примере генерации простого метода. Метод описывается классом MethodDeclarationSyntax. Чтобы создать этот класс воспользуемся методом Syntax.MethodDeclaration(), указав тип возвращаемого значения и имя генерируемого метода:

MethodDeclarationSyntax method =
  Syntax.MethodDeclaration(Syntax.ParseTypeName("string"), "TestMethod")

Добавим к нашему методу модификаторы, которые говорят о том, что метод статический и публичный:

MethodDeclarationSyntax method =
  Syntax.MethodDeclaration(Syntax.ParseTypeName("string"), "TestMethod")
    .AddModifiers(Syntax.Token(SyntaxKind.PublicKeyword))
    .AddModifiers(Syntax.Token(SyntaxKind.StaticKeyword))

Теперь самое время определить тело метода. Для этого существует метод AddBodyStatements(), который принимает переменное число параметров (в зависимости от того, какое количество statement-ов мы хотим добавить). Также можно вызвать этот метод несколько раз, что на мой взгляд удобнее.

Пусть тело метода содержит вызов метода Console.Write(), параметром которого является строка test. Для этого нам нужно создать цепочку ExpressionStatementSyntax (выражение, которое будет добавлено в тело метода) – InvocationExpressionSyntax (вызов метода) – MemberAccessExpressionSyntax (обращение к методу). Кроме того, для вызова метода нужно передать параметры. Вот как будет выглядеть код по заполнению тела метода:

.AddBodyStatements(
  Syntax.ExpressionStatement(
    Syntax.InvocationExpression(
      Syntax.MemberAccessExpression(SyntaxKind.MemberAccessExpression, Syntax.IdentifierName("Console"),
                      Syntax.IdentifierName("Write")),
      Syntax.ArgumentList(
        Syntax.SeparatedList(
          Syntax.Argument(Syntax.LiteralExpression(SyntaxKind.StringLiteralExpression, Syntax.Literal("test")))
          ))
      )
    )
)

Добавим также к телу метода вызов оператора return, который будет возвращать строку OK:

.AddBodyStatements(
  Syntax.ReturnStatement(
    Syntax.LiteralExpression(SyntaxKind.StringLiteralExpression, Syntax.Literal("OK"))
    )

В итоге код для генерации нашего тестового метода будет выглядеть так:

MethodDeclarationSyntax method =
  Syntax.MethodDeclaration(Syntax.ParseTypeName("string"), "TestMethod")
    .AddModifiers(Syntax.Token(SyntaxKind.PublicKeyword))
    .AddModifiers(Syntax.Token(SyntaxKind.StaticKeyword))
    .AddBodyStatements(
      Syntax.ExpressionStatement(
        Syntax.InvocationExpression(
          Syntax.MemberAccessExpression(SyntaxKind.MemberAccessExpression, Syntax.IdentifierName("Console"),
                          Syntax.IdentifierName("Write")),
          Syntax.ArgumentList(
            Syntax.SeparatedList(
              Syntax.Argument(Syntax.LiteralExpression(SyntaxKind.StringLiteralExpression, Syntax.Literal("test")))
              ))
          )
        )
    )
    .AddBodyStatements(
      Syntax.ReturnStatement(
        Syntax.LiteralExpression(SyntaxKind.StringLiteralExpression, Syntax.Literal("OK"))
        )
        );

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

Далее нам необходимо создать объект CompilationUnitSyntax. В дальнейшем он потребовался бы нам, если бы мы захотели скомпилировать данный код. В нашем случае он будет необходим нам для того, чтобы отформатировать исходный текст (расставить отсутпы и переносы строк):

CompilationUnitSyntax tree = Syntax.CompilationUnit()
    .AddMembers(method);

Console.WriteLine(tree
  .Format(FormattingOptions.GetDefaultOptions()).GetFormattedRoot()
  .ToFullString());

Метод Format() в данном случае как раз выполняет форматирование кода, а метод ToFullString() отдает нам строковое представление сгенерированного кода.

Аналогичным образом генерируются классы, пространства имен и т.д. Таким образом, мы можем получить полноценный сгенерированный код, который в дальнейшем будем компилировать.

Изменение структуры существующего кода

Другой сценарий – взять уже существующий код и модифицировать в соответствии с некоторыми правилами. За основу возьмем файл из библиотеки Monads.NET. При помощи синтаксического дерева Roslyn Compiler API найдем первый метод в исходном файле и будем его модифицировать:

public static TSource Do<TSource>(this TSource source, Action<TSource> action)
  where TSource : class
{
  if (source != default(TSource))
  {
    action(source);
  }

  return source;
}

В этом методе есть условный оператор if, давайте попробуем изменить его условие:

SyntaxTree tree = SyntaxTree.ParseText(source);
MethodDeclarationSyntax method = tree.GetRoot()
  .DescendantNodes()
  .OfType<MethodDeclarationSyntax>()
  .First();

IfStatementSyntax ifStatement = method
  .DescendantNodes()
  .OfType<IfStatementSyntax>()
  .First();

method = method.ReplaceNode(ifStatement,
          ifStatement
          .WithCondition((Syntax.LiteralExpression(SyntaxKind.TrueLiteralExpression)))
          );

Ключевой момент – вызов метода ReplaceNode. Первый аргумент этого метода – узел, который мы хотим заменить, второй – новое состояние этого узла. Вызов метода WithCondition позволяет заменить условие оператора if.

Если требуется какая-то более общая замена, вместо изменения условия, то для узла следует использовать метод Update. Давайте найдем элемент BlockSyntax (тело метода), и заменить его на пустую коллекцию:

SyntaxTree tree = SyntaxTree.ParseText(source);
MethodDeclarationSyntax method = tree.GetRoot()
  .DescendantNodes()
  .OfType<MethodDeclarationSyntax>()
  .First();

BlockSyntax methodBody = method
  .DescendantNodes()
  .OfType<BlockSyntax>()
  .First();

method = method.ReplaceNode(methodBody,
  methodBody.Update(methodBody.OpenBraceToken, new SyntaxList<StatementSyntax>(), methodBody.CloseBraceToken));

Как видно, в метод Update вторым параметром передается коллекция элементов. Эта коллеция и содержит новое тело метода. Поскольку в данном случае она пустая, то и тело обновленное тело метода будет пустым.

Наконец, третий вариант изменения структуры кода – использование паттерна Visitor. Для этого нужно реализовать объект-visitor, унаследовав его от базового класса SyntaxRewriter. В созданном классе можно переопределить нужный метод, в зависимости от того, с каким типом элементов требуется работать. В нашем случае мы будем искать элемент типа BlockSyntax, поэтому переопределим метод VisitBlock:

public class MyRewriter : SyntaxRewriter
{
  public override SyntaxNode VisitBlock(BlockSyntax node)
  {
    if (node.DescendantNodes().OfType<BlockSyntax>().Count() >= 1)
      return node.Update(node.OpenBraceToken,
                        new SyntaxList<StatementSyntax>(),
                        node.CloseBraceToken);

    return base.VisitBlock(node);
  }
}

Для того, чтобы запустить этот Visitor используем следующий простой код:

SyntaxNode updatedTree = new MyRewriter()
  .Visit(SyntaxTree.ParseText(source)
  .GetRoot());

Вызов метода Visit() запустит Visitor и вернет обновленное дерево, которое как и в предыдущем случае будет содержать очищенное тело метода:

Таким образом, Roslyn Compiler API предоставляет множество различных способов по генерации и модфикации кода. Несмотря на некоторую громозкость кода, API достаточно простой и логичный.