资讯动态

TypeScript 编译器源码探索:AST 子节点遍历 —— forEachChild 与 getChildren 全解析

发布时间:2026/9/20 13:20:50 来源:尧图企业网站定制
教程【免费下载链接】typescript-book:books: The definitive guide to TypeScript and possibly the best TypeScript book :book:. Free and Open Source 项目地址https://gitcode.com/gh_mirrors/ty/typescript-book点击查看免费下载导读本篇技术指南围绕 TypeScript 官方编译器typescript-compiler中 AST抽象语法树节点遍历的核心工具展开ts.forEachChild与Node.getChildren。文章首先给出二者的底层实现机制与差异再通过本仓库 code/compiler/parser/runParser.ts 中可直接运行的示例演示如何把任意源码的完整 AST 递归打印出来最后深入剖析SyntaxKind常量枚举、pos/end文本区间与 trivia注释/空白归属规则。读完你将掌握编写 AST 遍历器编译器插件、代码分析工具、codemod所需的关键 API 与心智模型。一、AST 节点模型速览在深入遍历 API 之前先建立对 AST 节点本身的认识。根据仓库文档 ast.md 的说明Node 是 AST 的基本构件。一般来说Node表示语言文法中的非终结符non-terminals但也有一些终结符terminals被保留在树中例如标识符Identifier和字面量Literal。描述一个 AST 节点需要两样东西SyntaxKind标识节点在 AST 中的类型和interface节点实例化为 AST 后提供的 API。interface Node中有几个对遍历至关重要的成员TextRange成员标识节点在源文件中的start与end位置即node.pos与node.endparent?: Node节点在 AST 中的父节点。Node 上还有用于 flags、modifiers 等信息的其他成员可以在源码中搜索interface Node查阅但上面两个成员是节点遍历的基础。最顶层的节点是SourceFileSyntaxKind.SourceFile对应interface SourceFile每个SourceFile是包含在Program中的一个顶层 AST 节点。一句话记忆AST 是“源码 → 扫描器scanner→ 令牌流Token Stream→ 解析器parser→ AST”这条流水线的最终产物详见 parser.md 与 scanner.md。二、ts.forEachChild按语法结构访问“语义子节点”ts.forEachChild是编译器提供的工具函数它可以遍历 AST 中任意节点的所有子节点。其核心思想是根据node.kind判断节点类型从而得知该节点在语义上拥有哪些子节点并对每个子节点调用回调。仓库文档 ast-tip-children.md 给出了源码的简化片段export function forEachChildT(node: Node, cbNode: (node: Node) T, cbNodeArray?: (nodes: Node[]) T): T { if (!node) { return; } switch (node.kind) { case SyntaxKind.BinaryExpression: return visitNode(cbNode, (BinaryExpressionnode).left) || visitNode(cbNode, (BinaryExpressionnode).operatorToken) || visitNode(cbNode, (BinaryExpressionnode).right); case SyntaxKind.IfStatement: return visitNode(cbNode, (IfStatementnode).expression) || visitNode(cbNode, (IfStatementnode).thenStatement) || visitNode(cbNode, (IfStatementnode).elseStatement); // .... lots more从这段代码可以提炼出几个关键设计点switch (node.kind)分发函数检查node.kind据此假定节点对应的 interface如BinaryExpression、IfStatement然后访问该 interface 暴露的各个子字段。短路求值语义visitNode(...) || visitNode(...) || ...意味着只要某个回调返回了“真值”即遍历结果后续子节点就不再访问。这正是forEachChild名字中 “for … Each” 的准确语义——它并非保证每个子节点都被调用而是按语法定义的顺序尝试访问遇到结果即停止这与Array.prototype.forEach完全不同。cbNodeArray参数对于SyntaxList这类包含多个同构子节点的字段例如参数列表可以传入数组版回调一次性处理。重要局限并非访问“所有”子节点文档特别强调这个函数不会为所有子节点调用visitNode。例如SemicolonToken分号这种在语法上“不重要”的终结符就被跳过了。如果你想要一个节点的全部子节点就应该调用Node的成员函数.getChildren()。这是 AST 遍历中一个极易踩坑的差异遍历方式访问范围典型用途ts.forEachChild按node.kind分发的“语义子节点”跳过如SemicolonToken等不重要的终结符编译器语义分析、类型检查、transform 等关心语法含义的场景node.getChildren()节点的全部子节点含终结符与 trivia 之外的细节打印/可视化完整 AST、教学演示、精确位置分析三、实战用getChildren递归打印完整 AST文档给出了一个打印节点完整 AST 的递归函数。该函数以node.kind对应的名称、node.pos、node.end三要素为输出内容并通过对getChildren()的递归调用自顶向下铺开整棵树function printAllChildren(node: ts.Node, depth 0) { console.log(new Array(depth1).join(----), ts.syntaxKindToName(node.kind), node.pos, node.end); depth; node.getChildren().forEach(c printAllChildren(c, depth)); }其中ts.syntaxKindToName(node.kind)把SyntaxKind数值成员反向映射为可读字符串其实现即(anyts).SyntaxKind[kind]见 ast-tip-syntaxkind.mdnode.pos/node.end节点在源文件中的起止偏移TextRange成员缩进用new Array(depth1).join(----)实现深度越深前缀越长打印结果倾斜头看就是一棵“向右生长的树”。仓库中的可运行示例本仓库在 code/compiler/parser/runParser.ts 中提供了该函数的完整落地版本并在同一目录保留了编译产物 runParser.jsimport * as ts from ntypescript; function printAllChildren(node: ts.Node, depth 0) { console.log(new Array(depth 1).join(----), ts.syntaxKindToName(node.kind), node.pos, node.end); depth; node.getChildren().forEach(c printAllChildren(c, depth)); } var sourceCode var foo 123; .trim(); var sourceFile ts.createSourceFile(foo.ts, sourceCode, ts.ScriptTarget.ES5, true); printAllChildren(sourceFile);示例通过ts.createSourceFile(foo.ts, sourceCode, ts.ScriptTarget.ES5, true)直接把源码字符串解析成SourceFile节点这正是编译器内部CompilerHost.getSourceFile → createSourceFile → Parser.parseSourceFile调用链的对外入口然后递归打印。输出解读按照 parser.md 中记录的运行结果对var foo 123;打印出的完整 AST 为SourceFile 0 14 ---- SyntaxList 0 14 -------- VariableStatement 0 14 ------------ VariableDeclarationList 0 13 ---------------- VarKeyword 0 3 ---------------- SyntaxList 3 13 -------------------- VariableDeclaration 3 13 ------------------------ Identifier 3 7 ------------------------ FirstAssignment 7 9 ------------------------ FirstLiteralToken 9 13 ------------ SemicolonToken 13 14 ---- EndOfFileToken 14 14注意几个细节getChildren保留了SemicolonToken13–14而forEachChild会跳过它——这正是上一节差异的直观证据每个节点都带pos/end偏移例如Identifier 3 7表明标识符foo占据源码第 3 到第 7 个字符EndOfFileToken 14 14是零宽度的文件结束标记pos end它是文件末尾 trivia 的“挂靠点”。提示runParser.ts使用ts.createSourceFile时传入true表示setParentNodes为节点填充parent指针便于在遍历时向上回溯。该示例工程的编译配置见 code/compiler/tsconfig.jsontarget: es5、module: commonjs、preserveConstEnums: true等。四、SyntaxKind常量枚举与syntaxKindToName的实现原理遍历离不开node.kind而kind的类型正是SyntaxKind。文档 ast-tip-syntaxkind.md 指出export const enum SyntaxKind { Unknown, EndOfFileToken, SingleLineCommentTrivia, // ... LOTS moreSyntaxKind是一个const enumenums.md 中讲解过该概念好处在于编译期内联使用处如ts.SyntaxKind.EndOfFileToken会被直接替换成字面量1从而避免运行时的枚举成员解引用开销。但编译器自身是用--preserveConstEnums编译标志编译的因此该枚举在运行时仍然可用——也就是说在 JavaScript 中你依然可以写ts.SyntaxKind.EndOfFileToken。关于这个标志的细节可回看 enums.md--preserveConstEnums会额外生成var Tristate之类的枚举定义供做数字↔字符串的反查且不影响内联行为。数字枚举天然支持反向映射Tristate[0]→FalseTristate[False]→0因此把SyntaxKind数字转成可读名称只需要一次索引export function syntaxKindToName(kind: ts.SyntaxKind) { return (anyts).SyntaxKind[kind]; }这正是printAllChildren中ts.syntaxKindToName(node.kind)的底层实现。五、进阶Token Start / Full Start 与 trivia 归属在分析节点位置pos/end时还需要区分两个“起点”概念详见 ast-trivia.mdToken Start更自然的定义——token 文本真正开始的位置Full Start扫描器自上一个“重要 token”之后开始扫描的位置即包含前导注释/空白。AST 节点提供getStart与getFullStart两个 API。考虑下面的源码片段debugger;/*hello*/ //bye /*hi*/ function对function而言token start 位于function关键字处而full start 则在/*hello*/处因为它连“本该属于前一个节点的 trivia”也包含了进来。理解这组概念还依赖trivia 归属规则trivia 即空白、注释、冲突标记等“琐碎”内容它们不存储在 AST 中以保持轻量但可用ts.*API 按需获取一般来说一个 token 拥有它之后、同一行内直到下一个 token 之前的全部 trivia该行之后的注释归属于后面的 token文件开头的全部初始 trivia 归第一个 token文件末尾的最后一段 trivia 挂在零宽度的 end-of-file token 上对应上面输出中EndOfFileToken 14 14的现象。获取注释的常用 API函数描述ts.getLeadingCommentRanges给定源码文本与位置返回该位置之后第一个换行到该 token 之间的注释区间通常配合ts.Node.getFullStart使用ts.getTrailingCommentRanges给定源码文本与位置返回直到该位置之后第一个换行前的注释区间通常配合ts.Node.getEnd使用以上面片段为例对function调用getLeadingCommentRanges只会返回最后两条注释//bye与/*hi*/而对debugger语句结尾调用getTrailingCommentRanges则会提取出/*hello*/。这些 API 与forEachChild/getChildren组合使用即可在遍历节点结构的同时按需恢复注释信息——这正是构建文档生成器、代码格式化器与 lint 工具的标准做法。六、总结与延伸阅读回到本指南的核心结论ts.forEachChild(node, cb)依据node.kind分发只访问语法上“重要”的语义子节点且采用短路求值返回真值即停止适合语义遍历node.getChildren()返回节点的全部子节点含SemicolonToken等终结符适合完整打印与可视化 ASTsyntaxKindToName借助const enum的反向映射与--preserveConstEnums标志在运行时也能把SyntaxKind数字还原为名称位置分析时注意pos/end、getStart/getFullStart的区别并理解 trivia 的归属规则。想继续深入可依次阅读仓库中的以下资源parser.md 与 parser-functions.md解析器如何把令牌流组装成 ASTscanner.md 与配套示例 runScanner.ts扫描器如何产生令牌流ast.md 与 ast-trivia.md节点模型与 trivia 的完整规则可运行示例runParser.ts、runParser.js。动手建议修改runParser.ts中的sourceCode字符串例如换成function add(a: number, b: number) { return a b; }重新编译运行观察forEachChild与getChildren输出结果的差异——这是理解 AST 遍历最直观的一课。赞分享教程【免费下载链接】typescript-book:books: The definitive guide to TypeScript and possibly the best TypeScript book :book:. Free and Open Source 项目地址https://gitcode.com/gh_mirrors/ty/typescript-book点击查看免费下载相关推荐XXPermissions与Jetpack Compose现代UI权限请求XXPermissions与Jetpack Compose现代UI权限请求 在Android应用开发中权限请求是确保应用功能正常运行的关键环节。随着Jetp开发工具代码质量PHP-Parser节点遍历高效操作AST结构PHP Parser节点遍历高效操作AST结构 你是否曾经面对复杂的PHP代码分析任务感到束手无策是否想要自动化处理代码重构、静态分析或代码生成PHP P编译器静态分析代码生成PHP-Parser 遍历 AST 完全指南NodeTraverser、NodeVisitor 与节点查找实战PHP Parser 遍历 AST 完全指南NodeTraverser、NodeVisitor 与节点查找实战 本文是 PHP Parser一个用 PHP编译器静态分析代码生成上一篇LSD 开源项目安装与使用指南下一篇nvim-dap 使用教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价