@babel/types 构造与修改节点

本节目标

上一节我们直接改了 path.node.name。但新建一个节点时,千万别手写 { type: 'Identifier', name: 'x' }——容易漏字段、拼错类型。Babel 提供 @babel/types(社区简称 t)来“按规范造节点”:

心智模型:t 是“AST 节点的正规工厂”。你只要说“给我一个标识符叫 x”,工厂保证产出的节点字段齐全、符合规范。

动手:用 t 构造节点并替换

// 运行环境:Node.js 18+,需安装: npm install @babel/parser @babel/traverse @babel/types
// 运行方式:把整段保存为 app-l7.cjs,终端执行  node app-l7.cjs
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const t = require('@babel/types')

const code = 'const area = radius * radius'
const ast = parser.parse(code)

traverse(ast, {
  // 把所有数字字面量 2 改成字符串 'two'(演示“用 t 造新节点并替换”)
  NumericLiteral(path) {
    if (path.node.value === 2) {
      // 用构造器造一个新节点,比手写 { type:'StringLiteral', value:'two' } 安全
      const newNode = t.stringLiteral('two')
      console.log('是否字符串字面量?', t.isStringLiteral(newNode)) // true
      path.replaceWith(newNode)
    }
  }
})

// 此时 ast 已变:radius * radius 中若原先是数字 2 会被替换。
// 注意 area 仍是 radius*radius,本例只是演示 replaceWith。
// 下面用 t 造一个完整的新表达式节点,并验证它的结构:
const addNode = t.binaryExpression('+', t.identifier('a'), t.numericLiteral(1))
console.log('用 t 造的表达式:', JSON.stringify(addNode))
// {"type":"BinaryExpression","operator":"+",
//  "left":{"type":"Identifier","name":"a"},
//  "right":{"type":"NumericLiteral","value":1}}

常用构造器速查(记这几个就够入门)

// 运行环境:Node.js 18+,需安装: npm install @babel/types
// 运行方式:把整段保存为 app-l7b.cjs,终端执行  node app-l7b.cjs
const t = require('@babel/types')

// 下面每一行都可直接运行,观察 @babel/types 构造/校验的效果
const id = t.identifier('foo')              // 标识符 foo
const str = t.stringLiteral('hi')           // 字符串 'hi'
const num = t.numericLiteral(42)            // 数字 42
const bool = t.booleanLiteral(true)         // 布尔 true
const bin = t.binaryExpression('+', id, num) // foo + 42
const call = t.callExpression(id, [str])     // foo('hi')
const member = t.memberExpression(id, t.identifier('bar')) // foo.bar
const vard = t.variableDeclaration('const', [t.variableDeclarator(id, num)]) // const foo = 42
const ret = t.returnStatement(num)          // return 42
const block = t.blockStatement([ret])        // { return 42 }

console.log('identifier 类型:', id.type)            // Identifier
console.log('isIdentifier(id) =', t.isIdentifier(id)) // true
console.log('isStringLiteral(num) =', t.isStringLiteral(num)) // false
console.log('bin 结构:', JSON.stringify(bin))
// {"type":"BinaryExpression","operator":"+",
//  "left":{"type":"Identifier","name":"foo"},"right":{"type":"NumericLiteral","value":42}}

名词解释

builders(构造器):@babel/types 里“按规范造出一个节点”的函数,例如 t.identifier('x')、t.binaryExpression('+', a, b)。每种节点类型都对应一个构造器,你只管把必需的子节点传进去,它会补全其余细节。你可以把它当成“AST 节点的正规工厂”。

validators(校验器):判断“某对象是不是某种节点”的函数,例如 t.isIdentifier(node)、t.isStringLiteral(node)。它比手写 node.type === 'Identifier' 更可靠——AST 规范演进时,校验器内部会同步更新,你不用跟进。

课后练习

练习 1:用 t 构造一个表示 x + 1 的 AST 节点,并写出它的结构(包括 type 与各个字段)。

答案:const node = t.binaryExpression('+', t.identifier('x'), t.numericLiteral(1))。结构是:{ type: 'BinaryExpression', operator: '+', left: { type: 'Identifier', name: 'x' }, right: { type: 'NumericLiteral', value: 1 } }。

练习 2:教程说“手写 { type: 'Identifier', name: 'x' } 不如用 t.identifier('x')”,根本原因是什么?

答案:手写对象容易漏掉规范要求的字段(某些节点还需要 loc、extra 等辅助字段),也容易把类型名拼错成非规范字符串;而构造器保证产出的节点字段齐全、类型名正确、符合当前规范,后续 generate 才不会因为你少字段而报错。

本节小结(观点与完整描述)

本节补齐了你“安全地造节点”的能力。一句话记住:新建节点一律用 t.xxx() 构造器,别再手写 { type: ... } 对象。原因不只是“少打字”,而是规范层面——手写对象像徒手捏零件,尺寸可能不对;构造器是正规工厂,出厂即合格。配套的 t.isXxx(node) 校验器则让你判断节点类型时不必依赖容易拼错的字符串比较,规范一升级它自己就跟着变。把“构造器造节点 + 校验器判类型 + path.replaceWith 落树”这三步连起来,你就拥有了完整且安全的改树工具链。下一节,我们把这条工具链装进一个真正的 Babel 插件里。