> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# JavaScript AST 学习笔记
- URL: https://blog.vercanti.com/javascript-ast-xue-xi-bi-ji/
- Published: 2026-08-28T14:34:54.000Z
- Updated: 2026-08-28T14:57:39.000Z
- Description: 几乎所有编程语言都有 AST，这是编译器/解释器的基础。 Babel 的 visitor 模式有几个让人别扭的地方： Facebook 出品，用于批量自动化修改代码，典型场景是库升级迁移。 先用 AST Explorer 验证节点类型：在写任何遍历逻辑之前，先把目标代码粘进 https://astexplorer.net，选对解析器（babel/parser 或 acorn），点击你关心的代码位置查看节点类型和字段名。手写遍历时节点名打错是最常见的低级错误。 用 path.node 读，用 path.replaceWith 写：path.node 是当前
- Author: yellowdog
- Tags: js逆向, AST

> 官方文档：<https://babeljs.io/docs/babel-parser>

## 一、AST 解析工具生态

### JS 专用解析器

| 工具                            | 说明                         |
| ----------------------------- | -------------------------- |
| **Babel**                     | 最常用，支持最新语法和 JSX，生态丰富       |
| **acorn**                     | 轻量、标准兼容，Babel/ESLint 底层都用过 |
| **espree**                    | ESLint 官方解析器，基于 acorn      |
| **@typescript-eslint/parser** | 专门处理 TypeScript AST        |
| **oxc-parser**                | Rust 实现，极速，新兴选择            |
| **swc**                       | Rust 实现，比 Babel 快几十倍       |

### 遍历/操作工具

| 工具              | 说明                                  |
| --------------- | ----------------------------------- |
| **recast**      | 修改 AST 后能保留原始格式，做 codemod 常用        |
| **jscodeshift** | Facebook 出品，基于 recast，批量 codemod 工具 |
| **esquery**     | 类 CSS 选择器语法查询 AST 节点                |
| **ast-types**   | recast 底层，提供路径遍历和节点类型系统             |

### 选型参考

| 场景            | 推荐                   |
| ------------- | -------------------- |
| 通用代码分析/lint   | acorn / espree       |
| 批量 codemod    | jscodeshift + recast |
| TypeScript 项目 | ts-morph             |
| 追求极致性能        | swc 或 oxc            |
| 需要保留原始格式输出    | recast               |
| 类选择器查询        | esquery              |

---

## 二、多语言 AST

几乎所有编程语言都有 AST，这是编译器/解释器的基础。

| 语言          | 工具                           |
| ----------- | ---------------------------- |
| Python      | 标准库 ast、libcst（保留格式）、astroid |
| Rust        | syn（proc-macro 必备）           |
| Go          | 标准库 go/ast \+ go/parser      |
| Java/Kotlin | JavaParser、IntelliJ PSI      |
| C/C++       | libclang（LLVM 官方）            |
| Ruby        | parser gem（RuboCop 底层）       |
| PHP         | nikic/PHP-Parser             |
| 通用多语言       | **tree-sitter**（50+ 种语言）     |

### tree-sitter 特点

- 核心用 C 编写，极速
- **增量解析**，只重新解析变化部分，适合编辑器实时场景
- 生成 **CST（具体语法树）**，保留所有原始信息含空白和注释
- 有错误恢复能力，代码不完整也能解析
- Neovim 语法高亮底层就是 tree-sitter

---

## 三、tree-sitter 使用

### 安装

```bash
pip install tree-sitter tree-sitter-languages

```

### 基本解析

```python
from tree_sitter_languages import get_parser

parser = get_parser("python")  # 支持 javascript、rust、go 等

code = b"""
def hello(name: str) -> str:
    return f"hello {name}"
"""

tree = parser.parse(code)
root = tree.root_node

print(root.type)        # module
print(root.child_count)

```

### Query 语法（S 表达式，类 CSS 选择器）

```python
query = get_language("python").query("""
(function_definition
  name: (identifier) @func_name)
""")

captures = query.captures(root)
for node, name in captures:
    print(node.text.decode())

```

### tree-sitter vs Babel 对比

|      | tree-sitter  | Babel    |
| ---- | ------------ | -------- |
| 语言支持 | 50+ 种        | 主要 JS/TS |
| 速度   | 极快（C 实现）     | 较慢       |
| 树类型  | CST（保留所有）    | AST（语义化） |
| 代码修改 | 不方便          | 方便       |
| 查询能力 | 强（Query DSL） | 需手动遍历    |
| 主要用途 | 分析、高亮、搜索     | 转译、修改代码  |

> tree-sitter 更适合**读**，Babel 更适合**改**。

---

## 四、Babel 插件的痛点

Babel 的 visitor 模式有几个让人别扭的地方：

```js
// 结构臃肿，注入对象来源不直观
export default function({ types: t }) {
  return {
    visitor: {
      CallExpression(path, state) {
        // path 和 node 傻傻分不清
        // t、path、state、scope 关系不直观
      }
    }
  }
}

```

### 更简洁的写法（不用插件形式）

```js
import { parse } from "@babel/parser"
import traverse from "@babel/traverse"
import generate from "@babel/generator"
import * as t from "@babel/types"

const ast = parse(`console.log("hello")`)

traverse(ast, {
  CallExpression(path) {
    console.log(path.node.callee)
  }
})

const { code } = generate(ast)

```

---

## 五、esquery —— 类 CSS 选择器查询

### 安装

```bash
npm install esquery @babel/parser

```

### 基本用法

```js
import { parse } from "@babel/parser"
import esquery from "esquery"

const ast = parse(`
  foo(1, 2)
  console.log("hello")
`)

esquery.query(ast, "CallExpression")

```

### 选择器语法速查

```js
// 节点类型
"CallExpression"

// 属性等于某值
"Identifier[name='foo']"

// 嵌套属性访问
"CallExpression[callee.name='foo']"
"CallExpression[callee.object.name='console']"

// 后代（任意层级）
"FunctionDeclaration Identifier"

// 直接子节点
"FunctionDeclaration > Identifier"

// :has 包含
"CallExpression:has([name='foo'])"

// :not 排除
"Identifier:not([name='undefined'])"

// 多个选择器
"FunctionDeclaration, ArrowFunctionExpression"

// 通配
"*[name='foo']"

```

### 配合修改

```js
import generate from "@babel/generator"

const nodes = esquery.query(ast, "CallExpression[callee.name='foo']")
nodes.forEach(node => {
  node.callee.name = "bar"  // 直接改节点属性
})

const { code } = generate(ast)

```

---

## 六、jscodeshift —— 批量 codemod

Facebook 出品，用于**批量自动化修改代码**，典型场景是库升级迁移。

```js
// transform.js
export default function(fileInfo, api) {
  const j = api.jscodeshift
  const root = j(fileInfo.source)

  root.find(j.CallExpression, {
    callee: { name: 'foo' }
  })
  .forEach(path => {
    path.node.callee.name = 'bar'
  })

  return root.toSource()
}

```

```bash
jscodeshift -t transform.js src/**/*.js

```

|      | jscodeshift     | Babel transform |
| ---- | --------------- | --------------- |
| 定位   | 批量改文件           | 编译时转换           |
| 格式保留 | ✅ recast 保留原始风格 | ❌ 重新生成          |
| 运行方式 | 命令行跑一次          | 每次构建都跑          |

---

## 七、ESTree 节点类型参考

> ESTree 是 JS AST 的通用规范，Babel/ESLint/acorn 等工具都遵循此格式。

### 基础节点

```
Identifier          标识符（变量名、函数名等）
  .name             "foo"

Literal             字面量（字符串、数字、布尔、null、正则）
  .value            实际值
  .raw              原始文本 '"hello"'

```

### 表达式 Expression

```
CallExpression          函数调用  foo(a, b)
  .callee               被调用对象（Identifier 或 MemberExpression）
  .arguments[]          参数列表

MemberExpression        成员访问  obj.prop 或 obj[key]
  .object / .property / .computed

AssignmentExpression    赋值  a = b
  .operator  .left  .right

BinaryExpression        二元运算  a + b
  .operator  .left  .right

UnaryExpression         一元运算  !x  typeof x
  .operator  .argument

LogicalExpression       逻辑运算  a && b
  .operator  .left  .right

ConditionalExpression   三元  a ? b : c
  .test  .consequent  .alternate

ArrowFunctionExpression 箭头函数  (x) => x + 1
  .params[]  .body  .async

FunctionExpression      函数表达式  function() {}
  .id  .params[]  .body

NewExpression           new 调用  new Foo()
  .callee  .arguments[]

TemplateLiteral         模板字符串  `hello ${name}`
  .quasis[]（静态部分）  .expressions[]（动态插值）

ObjectExpression        对象字面量  { a: 1 }
  .properties[]

ArrayExpression         数组字面量  [1, 2, 3]
  .elements[]

ThisExpression          this

```

### 语句 Statement

```
ExpressionStatement     表达式语句  foo()
  .expression

BlockStatement          块  { ... }
  .body[]

ReturnStatement         return x
  .argument

IfStatement             if/else
  .test  .consequent  .alternate

ForStatement            for (init; test; update) {}
ForInStatement          for (x in obj) {}
ForOfStatement          for (x of iter) {}
WhileStatement          while (test) {}

SwitchStatement         switch
  .discriminant  .cases[]

ThrowStatement          throw err
TryStatement            try/catch/finally

```

### 声明 Declaration

```
VariableDeclaration     var/let/const
  .kind  .declarations[]

VariableDeclarator
  .id（Identifier 或解构）  .init（初始值）

FunctionDeclaration     function foo() {}
  .id  .params[]  .body  .async  .generator

ClassDeclaration        class Foo {}
  .id  .superClass  .body

```

### 模块 Module

```
ImportDeclaration       import { a } from 'b'
  .specifiers[]  .source

ExportNamedDeclaration  export { a }
ExportDefaultDeclaration export default foo
ExportAllDeclaration    export * from 'b'

```

### 常用 esquery 选择器示例

```js
"CallExpression"                                          // 所有函数调用
"CallExpression[callee.name='foo']"                       // foo()
"CallExpression[callee.type='MemberExpression']"          // obj.method()
"CallExpression[callee.object.name='console'][callee.property.name='log']"  // console.log()
"VariableDeclaration[kind='const']"                       // const 声明
"Literal[typeof value='string']"                          // 字符串字面量
"ArrowFunctionExpression"                                 // 箭头函数
"FunctionDeclaration[async=true]"                         // async 函数
"FunctionDeclaration ReturnStatement"                     // 函数内的 return

```

---

## 八、工具资源

| 资源                           | 地址                                        |
| ---------------------------- | ----------------------------------------- |
| AST Explorer（Felix Kling 作品） | <https://astexplorer.net>                 |
| ESTree 规范                    | <https://github.com/estree/estree>        |
| esquery                      | <https://github.com/estools/esquery>      |
| jscodeshift                  | <https://github.com/facebook/jscodeshift> |
| tree-sitter                  | <https://tree-sitter.github.io>           |

---

## 最佳实践

**先用 AST Explorer 验证节点类型**：在写任何遍历逻辑之前，先把目标代码粘进 <https://astexplorer.net>，选对解析器（babel/parser 或 acorn），点击你关心的代码位置查看节点类型和字段名。手写遍历时节点名打错是最常见的低级错误。

**用 `path.node` 读，用 `path.replaceWith` 写**：`path.node` 是当前节点引用，修改它的属性会直接改 AST；`path.replaceWith(newNode)` 整体替换节点，会触发访问器重新执行。不要混用。

```javascript
// 正确：替换整个节点
path.replaceWith(t.stringLiteral('replaced'));

// 错误：直接赋值给 path 无效，path 是包装对象不是节点本身
path = t.stringLiteral('replaced');

```

**遍历顺序：先 enter 后 exit**：`enter`（默认）在进入节点时触发，`exit` 在离开时触发。处理嵌套结构时（如函数内的函数），用 `exit` 可以保证内层先处理完。

```javascript
// 处理嵌套函数时，exit 保证由内而外
traverse(ast, {
    FunctionExpression: {
        exit(path) { /* 先处理内层，再处理外层 */ }
    }
});

```

**使用 `path.scope` 避免变量名冲突**：生成新标识符前调用 `path.scope.generateUid('name')` 而不是直接写死名字，Babel 会自动在当前作用域内保证唯一性。

```javascript
const uid = path.scope.generateUid('temp'); // 返回 "_temp", "_temp2" 等

```

**批量替换用 `path.replaceWithMultiple`，不要手动操作 body**：直接 push 到 `body` 数组会跳过路径追踪，导致后续 visitor 看不到新节点。

```javascript
// 正确：让 Babel 接管插入
path.replaceWithMultiple([stmt1, stmt2, stmt3]);

// 错误：直接修改数组
path.node.body.push(stmt3);

```

---

## 常见陷阱

### 陷阱：visitor 里修改节点导致无限循环

**现象：** 遍历卡死，CPU 100%，或报 "Maximum call stack size exceeded"。

**原因：** 在 `enter` 中替换节点后，Babel 会重新访问新节点，而新节点满足同一条件，触发再次替换，陷入死循环。

**解决：** 替换后立即调用 `path.skip()` 跳过对新节点的访问，或者在处理前加标记检查。

```javascript
traverse(ast, {
    StringLiteral(path) {
        if (path.node._processed) return; // 已处理则跳过
        const newNode = t.stringLiteral(decode(path.node.value));
        newNode._processed = true;
        path.replaceWith(newNode);
    }
});

```

### 陷阱：`t.identifier` 和字符串混用

**现象：** 生成的代码出现 `"undefined"` 字符串，或变量名被加上引号变成字符串字面量。

**原因：** `t.memberExpression(obj, t.identifier('key'))` 和 `t.memberExpression(obj, t.stringLiteral('key'))` 生成的代码不同：前者是 `obj.key`，后者是 `obj["key"]`。误用 StringLiteral 做属性名就会出现 `obj["undefined"]` 这类错误。

**解决：** 点号属性用 `t.identifier`，方括号属性用 `t.stringLiteral` 并设 `computed: true`。

```javascript
t.memberExpression(obj, t.identifier('name'))         // → obj.name
t.memberExpression(obj, t.stringLiteral('name'), true) // → obj["name"]

```

### 陷阱：`path.remove()` 后继续访问同一 path

**现象：** 报错 `NodePath has been removed`，或删除节点后周边逻辑出错。

**原因：** `path.remove()` 从 AST 中移除节点，但不会停止当前 visitor 的执行，后续对 `path.node` 的访问会报错。

**解决：** `path.remove()` 之后立刻 `return`，不要再访问该 path 的任何属性。

```javascript
traverse(ast, {
    EmptyStatement(path) {
        path.remove();
        return; // 必须立刻 return
    }
});

```

---

## 参见

- [Babel AST入门](https://blog.vercanti.com/babel-ast-ru-men/)
- [混淆还原](https://blog.vercanti.com/hun-yao-huan-yuan/)
- [Babel TypeScript 类型完全参考](https://blog.vercanti.com/babel-typescript-lei-xing-wan-quan-can-kao/)
- [js逆向调试技巧](https://blog.vercanti.com/js-ni-xiang-diao-shi-ji-qiao/)