在上文构建源生成器的过程中,我们使用字符串直接插入代码。这样做固然方便快捷,但字符串需要手动格式化,且无法检测拼写错误,这对需要生成复杂结构的源生成器项目很不友好。
本文将介绍生成代码的另一种方式:使用Roslyn API构建语法树。
什么是语法树 (Syntax Tree)?
语法树是编译器用于理解C#程序的数据结构。Roslyn在解析C#代码后就会生成一棵语法树,以供后续的进一步分析和编译。
一棵语法树由Node(节点)、Token(标记)、Trivia(额外信息)构成。
- SyntaxNode:声明、语句、子句和表达式等语法构造
如一个类的声明会被解析成一个ClassDeclaration
- SyntaxToken:独立的关键字、标识符、运算符或标点
如一个左括号(会被解析为OpenParenToken
- SyntaxTrivia:不重要的信息,例如标记、预处理指令和注释之间的空格
如一行注释会被解析为SingleLineCommentTrivia
例如,有如下代码:- using System;
- namespace App
- {
- internal class Program
- {
- static void Main(string[] args)
- {
- Console.WriteLine("Hello, World!");
- }
- }
- }
复制代码 Roslyn会将这段代码解析为如下结构(此处仅保留SyntaxNode):- CompilationUnitSyntax
- ├── UsingDirectiveSyntax (using System;)
- └── NamespaceDeclarationSyntax (namespace ConsoleApp1)
- └── ClassDeclarationSyntax (internal class Program)
- └── MethodDeclarationSyntax (static void Main(string[] args))
- └── BlockSyntax
- └── ExpressionStatementSyntax (Console.WriteLine("Hello, World!");)
- └── InvocationExpressionSyntax
- ├── SimpleMemberAccessExpressionSyntax (Console.WriteLine)
- │ ├── IdentifierNameSyntax (Console)
- │ └── IdentifierNameSyntax (WriteLine)
- └── ArgumentListSyntax
- └── ArgumentSyntax
- └── LiteralExpressionSyntax ("Hello, World!")
复制代码我们还可以安装语法树可视化工具(VS Installer>找到对应版本>修改>单个组件>DGML 编辑器)
安装完成后,搜索"Syntax Visualizer "即可
具体可查看使用 Visual Studio 中的 Roslyn 语法可视化工具浏览代码
既然Roslyn需要将代码解析成语法树,那么我们是否可以自行构建一个语法树并"反向"输出C#代码呢?
答案是:可以!
构建语法树
在开始之前,我们需要引入Microsoft.CodeAnalysis.CSharp包
若我们需要编写的代码如下:- using System;
- namespace ConsoleApp1;
- public class HelloWorld
- {
- public static void SayHello()
- {
- Console.WriteLine("Hello, World!");
- }
- }
复制代码 创建一个CompilationUnit并添加using语句:- using Microsoft.CodeAnalysis.CSharp;
- using Microsoft.CodeAnalysis.CSharp.Syntax;
- using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
- var complicationUnit = CompilationUnit()
- .AddUsings(
- UsingDirective(
- IdentifierName("System")));
复制代码 添加命名空间:- AddMembers(
- FileScopedNamespaceDeclaration(
- IdentifierName("ConsoleApp1")));
复制代码 添加HelloWorld类:- AddMembers(
- ClassDeclaration("HelloWorld")
- .AddModifiers(
- Token(SyntaxKind.PublicKeyword))
- .AddMembers(/*这里编写SayHello方法*/));
复制代码 编写SayHello方法:- MethodDeclaration(
- PredefinedType(
- Token(SyntaxKind.VoidKeyword)),
- Identifier("SayHello"))
- .WithModifiers(
- TokenList(
- Token(SyntaxKind.PublicKeyword),
- Token(SyntaxKind.StaticKeyword)))
- .WithBody(
- Block(
- ExpressionStatement(
- InvocationExpression(
- MemberAccessExpression(
- SyntaxKind.SimpleAssignmentExpression,
- IdentifierName("Console"),
- IdentifierName("WriteLine")))
- .WithArgumentList(
- ArgumentList(
- SingletonSeparatedList(
- Argument(
- LiteralExpression(
- SyntaxKind.StringLiteralExpression,
- Literal("Hello World")))))))))
复制代码 构建语法树:- var syntaxTree = SyntaxTree(complicationUnit);
复制代码 全部代码:- using Microsoft.CodeAnalysis.CSharp;
- using Microsoft.CodeAnalysis.CSharp.Syntax;
- using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
- var complicationUnit = CompilationUnit()
- .AddUsings(
- UsingDirective(
- IdentifierName("System")))
- .AddMembers(
- FileScopedNamespaceDeclaration(
- IdentifierName("ConsoleApp1")))
- .AddMembers(
- ClassDeclaration("HelloWorld")
- .AddModifiers(
- Token(SyntaxKind.PublicKeyword))
- .AddMembers(
- MethodDeclaration(
- PredefinedType(
- Token(SyntaxKind.VoidKeyword)),
- Identifier("SayHello"))
- .WithModifiers(
- TokenList(
- Token(SyntaxKind.PublicKeyword),
- Token(SyntaxKind.StaticKeyword)))
- .WithBody(
- Block(
- ExpressionStatement(
- InvocationExpression(
- MemberAccessExpression(
- SyntaxKind.SimpleMemberAccessExpression,
- IdentifierName("Console"),
- IdentifierName("WriteLine")))
- .WithArgumentList(
- ArgumentList(
- SingletonSeparatedList(
- Argument(
- LiteralExpression(
- SyntaxKind.StringLiteralExpression,
- Literal("Hello World")))))))))));
- var syntaxTree = SyntaxTree(complicationUnit);
复制代码使用Roslyn Quoter工具可以将代码直接转化为上文的形式(会比上文写的更长),可作为一些参考
将语法树转换为C#代码
对syntaxTree调用GetText()后调用ToString()即可得字符串- var code = syntaxTree.GetText().ToString();
- //使用异步重载
- //var code = (await syntaxTree.GetTextAsync()).ToString();
复制代码或创建一个StreamWriter,将complicationUnit写入即可- await using var streamWriter = new StreamWriter("output.txt");
- complicationUnit.WriteTo(streamWriter);
复制代码 打开输出,我们发现这些代码并没有被格式化——我们需要添加必要的Trivia- usingSystem;namespaceConsoleApp1;publicclassHelloWorld{publicstaticvoidSayHello(){Console.WriteLine("Hello World");}}
复制代码 我们可以在需要空格或换行的代码后调用WithLeadingTrivia()方法手动添加,但这样会显得代码异常冗长且可读性不佳
更好的方法是给complicationUnit调用NormalizeWhitespace()方法自动添加所需的Trivia- complicationUnit = complicationUnit.NormalizeWhitespace();
- var syntaxTree = SyntaxTree(complicationUnit);
- var code = (await syntaxTree.GetTextAsync()).ToString();
复制代码 在源生成器中的应用
在构建SyntaxTree时,手动指定编码形式为UTF8,即可将语法树转换后的代码供源生成器使用- context.RegisterPostInitializationOutput(ctx =>
- 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
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |