AST:调试与可视化——从查看结构到开发工具链
写 AST 相关的代码时,最头疼的往往是“不知道当前节点长什么样”“转换后结果对不对”。
这一篇聊聊如何调试和可视化 AST:从最简单的打印,到专业的可视化工具,再到开发 Language Server 时的调试技巧。
最简单的调试:打印 AST 结构
拿到一个 SourceFile 后,最直接的方式是打印它的结构:
1import * as ts from 'typescript';
2
3const sourceCode = `
4function greet(name: string) {
5 console.log('Hello ' + name);
6}
7`;
8
9const sourceFile = ts.createSourceFile(
10 'demo.ts',
11 sourceCode,
12 ts.ScriptTarget.Latest,
13 true
14);
15
16// 直接打印整个 AST(会很长)
17console.log(JSON.stringify(sourceFile, null, 2));
但这样打印出来的结果通常非常庞大,因为 AST 节点包含很多元数据(位置信息、标志位等)。
更实用的方式是只打印你关心的部分:
1function printAST(node: ts.Node, indent = 0) {
2 const prefix = ' '.repeat(indent);
3 const kindName = ts.SyntaxKind[node.kind];
4
5 // 打印节点类型和关键信息
6 if (ts.isIdentifier(node)) {
7 console.log(`${prefix}${kindName}("${node.text}")`);
8 } else if (ts.isStringLiteral(node)) {
9 console.log(`${prefix}${kindName}("${node.text}")`);
10 } else if (ts.isFunctionDeclaration(node)) {
11 console.log(`${prefix}${kindName}(${node.name?.text || 'anonymous'})`);
12 } else {
13 console.log(`${prefix}${kindName}`);
14 }
15
16 // 递归打印子节点
17 ts.forEachChild(node, (child) => {
18 printAST(child, indent + 1);
19 });
20}
21
22printAST(sourceFile);
输出大概长这样:
1SourceFile
2 FunctionDeclaration(greet)
3 Identifier("greet")
4 Parameter(name)
5 Identifier("name")
6 StringKeyword
7 Block
8 ExpressionStatement
9 CallExpression
10 PropertyAccessExpression
11 Identifier("console")
12 Identifier("log")
13 BinaryExpression
14 StringLiteral("Hello ")
15 Identifier("name")
使用 TypeScript Compiler API 的内置工具
TypeScript 提供了一些辅助函数来查看节点信息:
1// 获取节点的文本内容
2const nodeText = node.getText();
3
4// 获取节点的起始和结束位置
5const start = node.getStart();
6const end = node.getEnd();
7
8// 获取行号和列号
9const { line, character } = sourceFile.getLineAndCharacterOfPosition(start);
10
11// 获取节点的完整文本(包括注释)
12const fullText = node.getFullText();
13
14// 检查节点类型
15if (ts.isFunctionDeclaration(node)) {
16 console.log('This is a function declaration');
17}
18
19// 获取节点的标志位(modifiers、flags 等)
20const flags = ts.getCombinedModifierFlags(node);
在调试时,可以写一个辅助函数来打印节点的关键信息:
1function debugNode(node: ts.Node, sourceFile: ts.SourceFile) {
2 const kindName = ts.SyntaxKind[node.kind];
3 const start = node.getStart();
4 const { line, character } = sourceFile.getLineAndCharacterOfPosition(start);
5 const text = node.getText();
6
7 console.log({
8 kind: kindName,
9 position: `${line + 1}:${character + 1}`,
10 text: text.substring(0, 50), // 只显示前50个字符
11 start,
12 end: node.getEnd(),
13 });
14}
AST Explorer:在线可视化工具
AST Explorer 是最常用的 AST 可视化工具,支持多种语言和解析器。
使用方式:
- 打开网站,选择语言(比如 JavaScript、TypeScript);
- 在左侧输入代码;
- 右侧会实时显示 AST 结构,可以展开/折叠节点;
- 支持切换不同的解析器(Babel、TypeScript、ESLint 等)。
在 AST Explorer 里,你可以:
- 快速查看代码对应的 AST 结构;
- 测试不同的解析器对同一段代码的解析结果;
- 写 transform 代码,实时看到转换结果。
比如你想知道 console.log('hello') 的 AST 结构,
直接在 AST Explorer 里输入,就能看到完整的树形结构,比手写打印函数方便得多。
TypeScript AST Viewer
如果你主要用 TypeScript Compiler API,可以用 TypeScript AST Viewer:
- 专门针对 TypeScript AST;
- 显示节点的所有属性(包括位置、标志位等);
- 支持高亮显示节点对应的源码位置;
- 可以查看节点的类型信息(如果有 TypeChecker)。
在开发 TypeScript Language Server 或写 TypeScript 相关的工具时,这个工具特别有用。
调试 AST 转换:打印前后对比
写 transformer 时,最需要的是看到“转换前”和“转换后”的对比:
1function transformAndCompare(sourceCode: string) {
2 const sourceFile = ts.createSourceFile(
3 'demo.ts',
4 sourceCode,
5 ts.ScriptTarget.Latest,
6 true
7 );
8
9 console.log('=== BEFORE ===');
10 console.log(sourceCode);
11 console.log('\n=== AST STRUCTURE ===');
12 printAST(sourceFile);
13
14 // 执行转换
15 const result = ts.transform(sourceFile, [myTransformer]);
16 const transformed = result.transformed[0];
17 const printer = ts.createPrinter();
18 const newCode = printer.printFile(transformed);
19
20 console.log('\n=== AFTER ===');
21 console.log(newCode);
22 console.log('\n=== NEW AST STRUCTURE ===');
23 printAST(transformed);
24}
更精细的做法是只打印被修改的节点:
1function transformWithTracking(sourceFile: ts.SourceFile) {
2 const modifiedNodes: ts.Node[] = [];
3
4 const transformer: ts.TransformerFactory<ts.SourceFile> = (context) => {
5 return (sourceFile) => {
6 function visit(node: ts.Node): ts.Node {
7 const original = node;
8
9 // 执行转换逻辑
10 const transformed = ts.visitEachChild(node, visit, context);
11
12 // 如果节点被修改了,记录下来
13 if (transformed !== original) {
14 modifiedNodes.push(transformed);
15 console.log(`Modified: ${ts.SyntaxKind[transformed.kind]} at ${transformed.getStart()}`);
16 }
17
18 return transformed;
19 };
20
21 return ts.visitNode(sourceFile, visit) || sourceFile;
22 };
23 };
24
25 const result = ts.transform(sourceFile, [transformer]);
26
27 console.log(`Modified ${modifiedNodes.length} nodes`);
28 return result;
29}
在 Language Server 里调试 AST
开发 Language Server 时,调试 AST 分析逻辑有几种方式:
使用日志输出
在 Language Server 的关键位置添加日志:
1connection.onDefinition((params) => {
2 const { textDocument, position } = params;
3 const sourceFile = getSourceFile(textDocument.uri);
4
5 console.log(`[DEBUG] Definition request at ${position.line}:${position.character}`);
6 console.log(`[DEBUG] Source file: ${sourceFile.fileName}`);
7
8 const offset = sourceFile.getPositionOfLineAndCharacter(
9 position.line,
10 position.character
11 );
12 const node = findNodeAtPosition(sourceFile, offset);
13
14 console.log(`[DEBUG] Found node: ${ts.SyntaxKind[node.kind]}`);
15 console.log(`[DEBUG] Node text: ${node.getText().substring(0, 50)}`);
16
17 // ... 后续逻辑
18});
使用调试器断点
如果 Language Server 是用 Node.js 写的,可以在 VS Code 或 Theia 里直接调试:
- 在 Language Server 的代码里打断点;
- 在
launch.json里配置调试启动参数; - 启动调试,触发 LSP 请求时会在断点处停下。
1{
2 "type": "node",
3 "request": "launch",
4 "name": "Debug Language Server",
5 "program": "${workspaceFolder}/server/dist/index.js",
6 "args": ["--stdio"],
7 "console": "integratedTerminal"
8}
输出中间结果到文件
对于复杂的分析逻辑,可以把中间结果写到文件里:
1import * as fs from 'fs';
2
3function debugToFile(data: any, filename: string) {
4 fs.writeFileSync(
5 filename,
6 JSON.stringify(data, null, 2),
7 'utf-8'
8 );
9}
10
11// 在分析过程中
12const symbols = analyzeSymbols(sourceFile);
13debugToFile(symbols, 'debug-symbols.json');
14
15const references = findReferences(symbols);
16debugToFile(references, 'debug-references.json');
可视化 AST 的工具函数
可以写一些工具函数来辅助调试:
1// 高亮显示节点在源码中的位置
2function highlightNode(node: ts.Node, sourceFile: ts.SourceFile) {
3 const start = node.getStart();
4 const end = node.getEnd();
5 const fullText = sourceFile.getFullText();
6
7 const before = fullText.substring(0, start);
8 const target = fullText.substring(start, end);
9 const after = fullText.substring(end);
10
11 console.log(before + `>>>${target}<<<` + after);
12}
13
14// 打印节点的所有属性
15function inspectNode(node: ts.Node) {
16 const info: any = {
17 kind: ts.SyntaxKind[node.kind],
18 text: node.getText(),
19 };
20
21 // 根据节点类型添加特定属性
22 if (ts.isIdentifier(node)) {
23 info.name = node.text;
24 } else if (ts.isFunctionDeclaration(node)) {
25 info.name = node.name?.text;
26 info.parameters = node.parameters.map((p) => p.name.getText());
27 } else if (ts.isCallExpression(node)) {
28 info.expression = node.expression.getText();
29 info.arguments = node.arguments.map((arg) => arg.getText());
30 }
31
32 console.log(JSON.stringify(info, null, 2));
33}
34
35// 查找特定类型的节点
36function findNodesOfKind(
37 sourceFile: ts.SourceFile,
38 kind: ts.SyntaxKind
39): ts.Node[] {
40 const nodes: ts.Node[] = [];
41
42 function visit(node: ts.Node) {
43 if (node.kind === kind) {
44 nodes.push(node);
45 }
46 ts.forEachChild(node, visit);
47 }
48
49 visit(sourceFile);
50 return nodes;
51}
在 Theia 里调试 Language Server
如果你在 Theia 里开发 Language Server,调试流程是:
启动 Language Server
- 在终端里运行 Language Server,或者通过 Theia 的扩展机制启动。
查看 LSP 消息
- 在 Language Server 的代码里打印所有收到的 LSP 请求和响应;
- 或者在 Theia 的 LSP 客户端层添加日志,查看发送和接收的消息。
在 Monaco 编辑器里触发功能
- 打开一个文件,触发补全、hover、跳转等功能;
- 观察 Language Server 的日志输出,看 AST 分析的结果。
对比预期和实际结果
- 如果结果不对,回到 AST 分析逻辑,用前面提到的工具函数打印节点信息,找出问题。
常见调试场景
场景1:节点类型判断错误
1// 错误:直接用 node.kind 比较
2if (node.kind === ts.SyntaxKind.CallExpression) {
3 // ...
4}
5
6// 正确:用 Type Guard 函数
7if (ts.isCallExpression(node)) {
8 // ...
9}
调试时,如果发现节点类型判断不对,可以打印 ts.SyntaxKind[node.kind] 看看实际类型。
场景2:位置信息不对
1// 获取位置时要注意:getStart() 返回的是字符偏移,不是行列号
2const offset = node.getStart();
3const { line, character } = sourceFile.getLineAndCharacterOfPosition(offset);
4
5// 如果位置不对,检查:
6// 1. 节点是否正确(可能选错了节点)
7// 2. sourceFile 是否正确(可能是旧版本的文件)
8console.log(`Node at offset ${offset} = line ${line + 1}, col ${character + 1}`);
场景3:转换后代码格式丢失
1// 使用 createPrinter 时可以配置选项
2const printer = ts.createPrinter({
3 removeComments: false,
4 newLine: ts.NewLineKind.LineFeed,
5});
6
7// 如果格式不对,检查:
8// 1. 是否保留了原始格式信息
9// 2. 是否需要用 Prettier 等工具二次格式化
小结
调试和可视化 AST 是开发 Language Server、写 ESLint 规则、做代码转换时的必备技能:
- 打印 AST 结构:最简单直接的方式,适合快速查看;
- AST Explorer / TypeScript AST Viewer:专业的可视化工具,适合探索和学习;
- 对比转换前后:写 transformer 时最需要的能力;
- Language Server 调试:结合日志、断点、文件输出等方式,定位问题。
在 Theia + LSP 的开发流程里,理解如何调试 AST 分析逻辑,
有助于快速定位 Language Server 的问题,验证分析结果的正确性,
最终让 IDE 的语言智能功能更可靠。