找回密码
 立即注册
首页 业界区 安全 用代码写代码:使用Roslyn API构建语法树并应用于源生成 ...

用代码写代码:使用Roslyn API构建语法树并应用于源生成器

揭荸 前天 19:27
在上文构建源生成器的过程中,我们使用字符串直接插入代码。这样做固然方便快捷,但字符串需要手动格式化,且无法检测拼写错误,这对需要生成复杂结构的源生成器项目很不友好。
本文将介绍生成代码的另一种方式:使用Roslyn API构建语法树。
什么是语法树 (Syntax Tree)?

语法树是编译器用于理解C#程序的数据结构。Roslyn在解析C#代码后就会生成一棵语法树,以供后续的进一步分析和编译。
一棵语法树由Node(节点)、Token(标记)、Trivia(额外信息)构成。

  • SyntaxNode:声明、语句、子句和表达式等语法构造
    如一个类的声明会被解析成一个ClassDeclaration
  • SyntaxToken:独立的关键字、标识符、运算符或标点
    如一个左括号(会被解析为OpenParenToken
  • SyntaxTrivia:不重要的信息,例如标记、预处理指令和注释之间的空格
    如一行注释会被解析为SingleLineCommentTrivia
例如,有如下代码:
  1. using System;
  2. namespace App
  3. {
  4.     internal class Program
  5.     {
  6.         static void Main(string[] args)
  7.         {
  8.             Console.WriteLine("Hello, World!");
  9.         }
  10.     }
  11. }
复制代码
Roslyn会将这段代码解析为如下结构(此处仅保留SyntaxNode):
  1. CompilationUnitSyntax
  2. ├── UsingDirectiveSyntax (using System;)
  3. └── NamespaceDeclarationSyntax (namespace ConsoleApp1)
  4.       └── ClassDeclarationSyntax (internal class Program)
  5.            └── MethodDeclarationSyntax (static void Main(string[] args))
  6.                 └── BlockSyntax
  7.                      └── ExpressionStatementSyntax (Console.WriteLine("Hello, World!");)
  8.                           └── InvocationExpressionSyntax
  9.                                ├── SimpleMemberAccessExpressionSyntax (Console.WriteLine)
  10.                                │    ├── IdentifierNameSyntax (Console)
  11.                                │    └── IdentifierNameSyntax (WriteLine)
  12.                                └── ArgumentListSyntax
  13.                                     └── ArgumentSyntax
  14.                                          └── LiteralExpressionSyntax ("Hello, World!")
复制代码
我们还可以安装语法树可视化工具(VS Installer>找到对应版本>修改>单个组件>DGML 编辑器)
安装完成后,搜索"Syntax Visualizer "即可
具体可查看使用 Visual Studio 中的 Roslyn 语法可视化工具浏览代码
既然Roslyn需要将代码解析成语法树,那么我们是否可以自行构建一个语法树并"反向"输出C#代码呢?
答案是:可以!
构建语法树

在开始之前,我们需要引入Microsoft.CodeAnalysis.CSharp包
若我们需要编写的代码如下:
  1. using System;
  2. namespace ConsoleApp1;
  3. public class HelloWorld
  4. {
  5.     public static void SayHello()
  6.     {
  7.         Console.WriteLine("Hello, World!");
  8.     }
  9. }
复制代码
创建一个CompilationUnit并添加using语句:
  1. using Microsoft.CodeAnalysis.CSharp;
  2. using Microsoft.CodeAnalysis.CSharp.Syntax;
  3. using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
  4. var complicationUnit = CompilationUnit()
  5.     .AddUsings(
  6.         UsingDirective(
  7.             IdentifierName("System")));
复制代码
添加命名空间:
  1. AddMembers(
  2.     FileScopedNamespaceDeclaration(
  3.         IdentifierName("ConsoleApp1")));
复制代码
添加HelloWorld类:
  1. AddMembers(
  2.     ClassDeclaration("HelloWorld")
  3.     .AddModifiers(
  4.         Token(SyntaxKind.PublicKeyword))
  5.     .AddMembers(/*这里编写SayHello方法*/));
复制代码
编写SayHello方法:
  1. MethodDeclaration(
  2.         PredefinedType(
  3.             Token(SyntaxKind.VoidKeyword)),
  4.         Identifier("SayHello"))
  5.     .WithModifiers(
  6.         TokenList(
  7.             Token(SyntaxKind.PublicKeyword),
  8.             Token(SyntaxKind.StaticKeyword)))
  9.     .WithBody(
  10.         Block(
  11.             ExpressionStatement(
  12.                 InvocationExpression(
  13.                         MemberAccessExpression(
  14.                             SyntaxKind.SimpleAssignmentExpression,
  15.                             IdentifierName("Console"),
  16.                             IdentifierName("WriteLine")))
  17.                     .WithArgumentList(
  18.                         ArgumentList(
  19.                             SingletonSeparatedList(
  20.                                 Argument(
  21.                                     LiteralExpression(
  22.                                         SyntaxKind.StringLiteralExpression,
  23.                                         Literal("Hello World")))))))))
复制代码
构建语法树:
  1. var syntaxTree = SyntaxTree(complicationUnit);
复制代码
全部代码:
  1. using Microsoft.CodeAnalysis.CSharp;
  2. using Microsoft.CodeAnalysis.CSharp.Syntax;
  3. using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
  4. var complicationUnit = CompilationUnit()
  5.     .AddUsings(
  6.         UsingDirective(
  7.             IdentifierName("System")))
  8.     .AddMembers(
  9.         FileScopedNamespaceDeclaration(
  10.             IdentifierName("ConsoleApp1")))
  11.     .AddMembers(
  12.         ClassDeclaration("HelloWorld")
  13.             .AddModifiers(
  14.                 Token(SyntaxKind.PublicKeyword))
  15.             .AddMembers(
  16.                 MethodDeclaration(
  17.                         PredefinedType(
  18.                             Token(SyntaxKind.VoidKeyword)),
  19.                         Identifier("SayHello"))
  20.                     .WithModifiers(
  21.                         TokenList(
  22.                             Token(SyntaxKind.PublicKeyword),
  23.                             Token(SyntaxKind.StaticKeyword)))
  24.                     .WithBody(
  25.                         Block(
  26.                             ExpressionStatement(
  27.                                 InvocationExpression(
  28.                                         MemberAccessExpression(
  29.                                             SyntaxKind.SimpleMemberAccessExpression,
  30.                                             IdentifierName("Console"),
  31.                                             IdentifierName("WriteLine")))
  32.                                     .WithArgumentList(
  33.                                         ArgumentList(
  34.                                             SingletonSeparatedList(
  35.                                                 Argument(
  36.                                                     LiteralExpression(
  37.                                                         SyntaxKind.StringLiteralExpression,
  38.                                                         Literal("Hello World")))))))))));
  39. var syntaxTree = SyntaxTree(complicationUnit);
复制代码
使用Roslyn Quoter工具可以将代码直接转化为上文的形式(会比上文写的更长),可作为一些参考
将语法树转换为C#代码

对syntaxTree调用GetText()后调用ToString()即可得字符串
  1. var code = syntaxTree.GetText().ToString();
  2. //使用异步重载
  3. //var code = (await syntaxTree.GetTextAsync()).ToString();
复制代码
或创建一个StreamWriter,将complicationUnit写入即可
  1. await using var streamWriter = new StreamWriter("output.txt");
  2. complicationUnit.WriteTo(streamWriter);
复制代码
打开输出,我们发现这些代码并没有被格式化——我们需要添加必要的Trivia
  1. usingSystem;namespaceConsoleApp1;publicclassHelloWorld{publicstaticvoidSayHello(){Console.WriteLine("Hello World");}}
复制代码
我们可以在需要空格或换行的代码后调用WithLeadingTrivia()方法手动添加,但这样会显得代码异常冗长且可读性不佳
更好的方法是给complicationUnit调用NormalizeWhitespace()方法自动添加所需的Trivia
  1. complicationUnit = complicationUnit.NormalizeWhitespace();
  2. var syntaxTree = SyntaxTree(complicationUnit);
  3. var code = (await syntaxTree.GetTextAsync()).ToString();
复制代码
在源生成器中的应用

在构建SyntaxTree时,手动指定编码形式为UTF8,即可将语法树转换后的代码供源生成器使用
  1. context.RegisterPostInitializationOutput(ctx =>
  2.     ctx.AddSource("HelloWorldSyntaxTree.g.cs",SyntaxTree(complicationUnit, encoding:Encoding.UTF8).GetText()));
复制代码
源代码

在源生成器中的应用
https://github.com/zxbmmmmmmmmm/SourceGeneratorDemo/blob/master/SourceGeneratorDemo.Generator/SyntaxTreeGenerator.cs
引用

使用代码编写代码 ——Roslyn API 入门
使用 Visual Studio 中的 Roslyn 语法可视化工具浏览代码
Roslyn Quoter

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册