许有建 發表於 2025-8-25 21:27:00

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

<p>在上文构建源生成器的过程中,我们使用字符串直接插入代码。这样做固然方便快捷,但字符串需要手动格式化,且无法检测拼写错误,这对需要生成复杂结构的源生成器项目很不友好。</p>
<p>本文将介绍生成代码的另一种方式:使用Roslyn API构建语法树。</p>
<h2 id="什么是语法树-syntax-tree">什么是语法树 (Syntax Tree)?</h2>
<p>语法树是编译器用于理解C#程序的数据结构。Roslyn在解析C#代码后就会生成一棵语法树,以供后续的进一步分析和编译。</p>
<p>一棵语法树由<code>Node(节点)</code>、<code>Token(标记)</code>、<code>Trivia(额外信息)</code>构成。</p>
<ul>
<li>
<p><code>SyntaxNode</code>:声明、语句、子句和表达式等语法构造</p>
<p>如一个类的声明会被解析成一个<code>ClassDeclaration</code></p>
</li>
<li>
<p><code>SyntaxToken</code>:独立的关键字、标识符、运算符或标点</p>
<p>如一个左括号<code>(</code>会被解析为<code>OpenParenToken</code></p>
</li>
<li>
<p><code>SyntaxTrivia</code>:不重要的信息,例如标记、预处理指令和注释之间的空格</p>
<p>如一行注释会被解析为<code>SingleLineCommentTrivia</code></p>
</li>
</ul>
<p>例如,有如下代码:</p>
<pre><code class="language-csharp">using System;
namespace App
{
    internal class Program
    {
      static void Main(string[] args)
      {
            Console.WriteLine("Hello, World!");
      }
    }
}

</code></pre>
<p>Roslyn会将这段代码解析为如下结构(此处仅保留<code>SyntaxNode</code>):</p>
<pre><code class="language-csharp">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!")
</code></pre>
<blockquote>
<p>我们还可以安装语法树可视化工具(<code>VS Installer&gt;找到对应版本&gt;修改&gt;单个组件&gt;DGML 编辑器</code>)</p>
<p>安装完成后,搜索"Syntax Visualizer "即可</p>
<p>具体可查看使用 Visual Studio 中的 Roslyn 语法可视化工具浏览代码</p>
</blockquote>
<p>既然Roslyn需要将代码解析成语法树,那么我们是否可以自行构建一个语法树并"反向"输出C#代码呢?</p>
<p>答案是:可以!</p>
<h2 id="构建语法树">构建语法树</h2>
<p>在开始之前,我们需要引入<code>Microsoft.CodeAnalysis.CSharp</code>包</p>
<p>若我们需要编写的代码如下:</p>
<pre><code class="language-csharp">using System;
namespace ConsoleApp1;

public class HelloWorld
{
    public static void SayHello()
    {
      Console.WriteLine("Hello, World!");
    }
}
</code></pre>
<p>创建一个<code>CompilationUnit</code>并添加using语句:</p>
<pre><code class="language-csharp">using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

var complicationUnit = CompilationUnit()
    .AddUsings(
      UsingDirective(
            IdentifierName("System")));
</code></pre>
<p>添加命名空间:</p>
<pre><code class="language-csharp">AddMembers(
    FileScopedNamespaceDeclaration(
      IdentifierName("ConsoleApp1")));
</code></pre>
<p>添加HelloWorld类:</p>
<pre><code class="language-csharp">AddMembers(
    ClassDeclaration("HelloWorld")
    .AddModifiers(
      Token(SyntaxKind.PublicKeyword))
    .AddMembers(/*这里编写SayHello方法*/));
</code></pre>
<p>编写SayHello方法:</p>
<pre><code class="language-csharp">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&lt;ArgumentSyntax&gt;(
                              Argument(
                                    LiteralExpression(
                                        SyntaxKind.StringLiteralExpression,
                                        Literal("Hello World")))))))))
</code></pre>
<p>构建语法树:</p>
<pre><code class="language-csharp">var syntaxTree = SyntaxTree(complicationUnit);
</code></pre>
<p>全部代码:</p>
<pre><code class="language-csharp">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);
</code></pre>
<blockquote>
<p>使用Roslyn Quoter工具可以将代码直接转化为上文的形式(会比上文写的更长),可作为一些参考</p>
</blockquote>
<h2 id="将语法树转换为c代码">将语法树转换为C#代码</h2>
<p>对<code>syntaxTree</code>调用<code>GetText()</code>后调用<code>ToString()</code>即可得字符串</p>
<pre><code class="language-csharp">var code = syntaxTree.GetText().ToString();
//使用异步重载
//var code = (await syntaxTree.GetTextAsync()).ToString();
</code></pre>
<blockquote>
<p>或创建一个<code>StreamWriter</code>,将<code>complicationUnit</code>写入即可</p>
<pre><code class="language-csharp">await using var streamWriter = new StreamWriter("output.txt");
complicationUnit.WriteTo(streamWriter);
</code></pre>
</blockquote>
<p>打开输出,我们发现这些代码并没有被格式化——我们需要添加必要的<code>Trivia</code></p>
<pre><code class="language-csharp">usingSystem;namespaceConsoleApp1;publicclassHelloWorld{publicstaticvoidSayHello(){Console.WriteLine("Hello World");}}
</code></pre>
<p>我们可以在需要空格或换行的代码后调用<code>WithLeadingTrivia()</code>方法手动添加,但这样会显得代码异常冗长且可读性不佳</p>
<p>更好的方法是给<code>complicationUnit</code>调用<code>NormalizeWhitespace()</code>方法自动添加所需的<code>Trivia</code></p>
<pre><code class="language-csharp">complicationUnit = complicationUnit.NormalizeWhitespace();
var syntaxTree = SyntaxTree(complicationUnit);
var code = (await syntaxTree.GetTextAsync()).ToString();
</code></pre>
<h2 id="在源生成器中的应用">在源生成器中的应用</h2>
<p>在构建<code>SyntaxTree</code>时,手动指定编码形式为<code>UTF8</code>,即可将语法树转换后的代码供源生成器使用</p>
<pre><code class="language-csharp">context.RegisterPostInitializationOutput(ctx =&gt;
    ctx.AddSource("HelloWorldSyntaxTree.g.cs",SyntaxTree(complicationUnit, encoding:Encoding.UTF8).GetText()));
</code></pre>
<h2 id="源代码">源代码</h2>
<p>在源生成器中的应用</p>
<p>https://github.com/zxbmmmmmmmmm/SourceGeneratorDemo/blob/master/SourceGeneratorDemo.Generator/SyntaxTreeGenerator.cs</p>
<h2 id="引用">引用</h2>
<p>使用代码编写代码 ——Roslyn API 入门</p>
<p>使用 Visual Studio 中的 Roslyn 语法可视化工具浏览代码</p>
<p>Roslyn Quoter</p><br><br>
来源:https://www.cnblogs.com/BettaFish/p/19057611
頁: [1]
查看完整版本: 用代码写代码:使用Roslyn API构建语法树并应用于源生成器