接触过 Babel 插件编写的话,可能有个感受,官方文档提供的介绍太少了,社区中关于如何写插件介绍的比较多,但对其中用到的工具的介绍却比较少。
为什么需要插件呢?
babel-parser 主要有两个功能:
- 识别各种语法,且默认情况下,并不开启所有语法的识别支持,这是出于性能考虑
- 解析源码字符串为抽象语法树,并不负责对源码的转译
在 Babel 的整体设计中,对于部分语法支持的开关、对抽象语法树的读取和修改等操作,均用插件配合完成。有些插件可以作为语法开关,有些插件可以解析修改抽象语法树以实现转译功能。
那么,本章就来攻克下 Babel 插件编写这一环吧。
编写插件的过程,实际上是对 AST 节点的遍历和操作。需要了解如何遍历节点、如何操作节点。
# 6.1 遍历节点
Babel 通过 babel-traverse 遍历 AST 节点,并在遍历的过程中,接收开发者传入的处理逻辑对 AST 节点进行增删改查。
# 6.1.1 babel-traverse
基本功能
babel-traverse 维护了 Babel 的节点状态,通过它,可以遍历 AST 节点,插件开发者可以在遍历过程中对节点进行替换、删除、添加等操作。
比如:
import * as parser from '@babel/parser'; import traverse from '@babel/traverse'; import generator from '@babel/generator'; const code = `function square(n) { return n * n; }`; const ast = parser.parse(code); traverse(ast, { enter(path) { if (path.node.type === 'Identifier' && path.node.name === 'n') { path.node.name = 'x'; } } }); const { code: newCode } = generator(ast); console.log(newCode);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21traverse
接收一些参数,并处理后,AST 发生了变动。上述代码执行后,打印
newCode
结果是:function square(x) { return x * x; }
1
2
3参数
babel-traverse 对外暴露的方法
traverse
定义在packages/babel-traverse/src/index.ts
:function traverse( parent: t.Node, opts: TraverseOptions = {}, scope?: Scope, state?: any, parentPath?: NodePath, ) { ... }
1
2
3
4
5
6
7
8
9各参数含义如下:
parent
: 要遍历的节点对象opts
: 遍历配置项参数
opts
的类型描述TraverseOptions
内容如下:type TraverseOptions<S = t.Node> = | { scope?: Scope; noScope?: boolean; denylist?: string[]; } | Visitor<S>;
1
2
3
4
5
6
7也就是说,
opts
的值可以是:{ scope, noScope, denyList }
1也可以是一个 Visitor(访问者),下节将详细介绍 Visitor。
state
: TODOTODO
parentPath
: TODOTODO
# 6.1.2 Visitor
访问者模式
TODO
Visitor 对象
babel-traverse 接收一个 Visitor 对象,开发者通过该对象可以拦截 babel-traverse 对某个节点的访问时刻,并做处理。
Visitor 对象有哪些格式呢?babel-traverse 中定义的 Visitor 类型如下:
export type Visitor<S = {}> = VisitNodeObject<S, t.Node> & { [Type in t.Node["type"]]?: VisitNode<S, Extract<t.Node, { type: Type }>>; } & { [K in keyof t.Aliases]?: VisitNode<S, t.Aliases[K]>; } & { [K in keyof VirtualTypeAliases]?: VisitNode<S, VirtualTypeAliases[K]>; } & { [k: string]: VisitNode<S, t.Node>; }; export type VisitNode<S, P extends t.Node> = | VisitNodeFunction<S, P> | VisitNodeObject<S, P>; export type VisitNodeFunction<S, P extends t.Node> = ( this: S, path: NodePath<P>, state: S, ) => void; export interface VisitNodeObject<S, P extends t.Node> { enter?: VisitNodeFunction<S, P>; exit?: VisitNodeFunction<S, P>; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27可以看到,Visitor 可以有多种写法:
enter/exit 式写法
{ enter(...) { ... }, exit(...) { ... } }
1
2
3
4
5
6
7
8比如,
babelTraverse(ast, { enter() {}, exit() {} })
1
2
3
4遍历ast上的每个时,
enter/exit
均会触发。有意思的是,
babel-core
内部也实用了babel-traverse
对目标代码对应的 AST 节点进行遍历,但它禁止这样写,比如:index.js:
const babel = require('babel-core'); const code = `const a = 1;` babel.transformSync(code, { plugins: [ './plugin.js' ] })
1
2
3
4
5
6
7
8./plugin.js
:module.exports = function(api) { return { visitor: { enter(path, state) { console.log(111) }, exit(path, state) { } } } };
1
2
3
4
5
6
7
8
9
10
11
12babel-core 将这种写法视为非法:
cannot contain catch-all "enter" or "exit" handlers. Please target individual nodes.
。节点名称 + 函数式写法
Identifier(...) { ... }
1
2
3节点名称 + enter/exit 式写法
Identifier: { enter(...) { ... }, exit(...) { ... } }
1
2
3
4
5
6
7
8
其中,节点名称可以是以下几种方式:
节点名称
定义在
babel-types/src/ast-types/generated/index.ts
的Node
字段。节点别名
定义在
babel-types/src/ast-types/generated/index.ts
的Aliases
字段,以及定义在babel-traverse/src/path/generated/virtual-types.ts
的类型。其他名称
源码中是这样写的:
[k: string]: VisitNode<S, t.Node>;
1可以是任何字符串,但很显然没有这么自由,具体可写的内容后文会介绍。
Visitor 的参数
对 Visitor 处理函数的封装
babel-traverse 内部对传入的插件处理函数做了一层封装,在文件
babel-traverse/src/visitors.ts
中有这么一段代码:let newFn = fn; if (state) { newFn = function (path) { // 执行 visitor return fn.call(state, path, state); }; } if (wrapper) { newFn = wrapper(state.key, key, newFn); } // Override toString in case this function is printed, we want to print the wrapped function, same as we do in `wrapCheck` if (newFn !== fn) { newFn.toString = () => fn.toString(); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17其中,
fn
是插件传入的处理方法,newFn
是封装后的方法,有这么几个特点:fn
的两个参数是path
和state
fn
执行的时候,上下文是state
newFn
重写了toString
方法,当需要打印函数体时,可以还原fn
,这是一个值得借鉴的细节
Visitor 的参数
上述代码中,Visitor 对象提供的处理函数
fn
可接收 2 个参数:path
、state
,且this
的值为state
。path
和state
对象描述了当前被访问节点的各类信息,这两个参数携带的属性和方法非常多,下文另开一节对其详细介绍。
节点处理队列
插件开发者提供的 Visitor,在 babel-traverse 处理时,会针对每个 Visitor 的 key(节点名称),会有一个数组用于存储插件传入的处理方法,也就是说,针对一个节点,有一个处理队列。
# 6.1.3 path
path 对象是内部 NodePath 类的实例。
# path功能
path
上的 API 非常多,如下图所示:
根据上图,从来源看,path
作为 NodePath
类的实例,该对象上的 API 可以分为几类:
path
对象自身的属性和方法path
继承自NodePath
类的原型上的属性和方法- 各功能模块对外暴露的属性和方法,被添加到
NodePath.prototype
,便于path
使用 - 各功能功能模块为
path
对象直接设置的属性/方法
从功能上,path
有以下能力:
- 断言与校验,等同于 babel-types 的断言
- 操作祖先节点、家族节点
- 替换、转换、修改、删除节点
- 节点计算操作
- 获取节点上下文的信息
- 获取节点命名空间的信息
- 和注释节点相关的操作
- ...
# API
笔者将下文中部分 API 的调用链路进行了图解,重点描述:path.context
、插件引入的方法执行链路等信息。
阅读建议:下文的 API 介绍可以参照上图一起了解
基本属性/方法
path.node: t.Node
表示 AST 节点对象。关于节点对象的具体信息,章节"Babel节点集"中对各个节点有详细的介绍。
path.opts: any = null
path.opts
就是执行babelTraverse
传入的遍历配置项。从上文的图解可以看到该属性的设置过程:babelTraverse(..., opts, ...) --> // ./context.ts context = new TraversalContext({ ..., opts }) context.opts = opts --> // ./path/context.ts path.opts = context.opts
1
2
3
4
5
6
7
8
9
10
11
12path.skipKeys
- babel-traverse/src/context.ts
path.parentPath
父级
path
对象。path.container
TODO
path.listKey
path.key
path.type
path.inList
path.getScope(scope: Scope): Scope
获取当前节点对应的
scope
作用域空间对象。path.traverse<T>(visitor: Visitor<T>, state: T): void
遍历当前节点及其子节点。
path.buildCodeFrameError
内部调用的是
path.hub.buildError
:buildError(node, msg, Error = TypeError): Error { return new Error(msg); }
1
2
3path.parent
表示当前节点的父节点。
上下文信息:
path.context
path.context: TraversalContext
path.context
是TraversalContext
类的实例,表示当前访问节点的上下文信息。文件babel/packages/babel-traverse/src/context.ts
定义了TraversalContext
类。在遍历节点队列的过程中,每访问一个节点,
path.context
就会被设置为被访问节点对应的上下文context
对象,上图有描述。一个较为迷惑的点是,
./context.ts
和./path/context.ts
二者有什么区别?./context.ts
定义了TraversalContext
类,该类的实例,作为属性path.context
的值./path/context.ts
对外暴露的各个方法和属性,可以直接被path
对象访问,因为这些方法和属性被直接添加到NodePath.prototype
上,path
作为NodePath
的实例是可以直接访问的
path.context.visit(node, key): boolean
遍历
node[key]
节点集合及其子节点,返回值表示是否结束节点遍历。该方法的执行,会调用 Babel 插件里注册的处理函数,分别在
enter/exit
阶段执行,从源码和上图可以看到如下执行链路:// ./context.ts path.context.visit() --> // ./path/context.ts path.visit() --> // ./path/context.ts path.call() --> // ./path/context.ts fns队列依次执行,fns就是插件传入的处理方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17这是插件提供的处理函数的调用方式:
const ret = fn.call(this.state, this, this.state); if (ret && typeof ret === "object" && typeof ret.then === "function") { throw new Error( `You appear to be using a plugin with an async traversal visitor, ` + `which your current version of Babel does not support. ` + `If you're using a published plugin, you may need to upgrade ` + `your @babel/core version.`, ); } if (ret) { throw new Error(`Unexpected return value from visitor method ${fn}`); }
1
2
3
4
5
6
7
8
9
10
11
12可以看到:
- 函数的
this
指向path.state
,两个参数分别指向path
和path.state
- Babel 还未支持异步的 Visitor 处理函数
- Visitor 处理函数不能反悔非空值
- 函数的
path.contexts: Array<TraversalContext>
某种类型的节点队列在遍历过程中,
path
对象维护了一个path.contexts
数组,每当遍历一个节点时,path.contexts
数组即会将当前被访问节点的上下文信息对象context
推入该数组。如果整个队列遍历结束,则会清空path.contexts
。
作用域信息:
path.scope
path.scope
描述path.node
节点所在的作用域的所有信息。path.scope
对象对应的构造函数Scope
,定义于文件babel/packages/babel-traverse/src/scope/index.ts
,通过init/crawl
等方法完成实例的初始化操作。path.scope.binding: Binding
既然介绍作用域空间,肯定要涉及变量绑定等信息,babel-traverse 用单独的
babel/packages/babel-traverse/src/scope/binding.ts
文件定义了绑定信息类Binding
。在此初始化一个
Binding
类的实例binding
。path.scope.binding = new Binding(...);
1以下是
path.scope.binding
对象的一些属性和方法:path.scope.binding.identifier: t.Identifier
指向标识符 AST 节点。
path.scope.binding.scope: Scope
对
scope
的反向引用:path.scope.binding.scope
指向path.scope
。path.scope.binding.path: NodePath
对
path
的反向引用:path.scope.binding.path
指向path
。path.scope.binding.kind: string
绑定类型,值可以是:
"var" | "let" | "const" | "module" | "hoisted" | "param" | "local" | "unknown"
。path.scope.binding.references: number
表示
binding
对象对应的变量被引用的次数。以下述代码为例:let key; for (let i of [ 1, 2, 3 ]) { key key }
1
2
3
4
5变量
key
的binding.references
值是 2,因为for
循环内部有 2 处对key
的引用。path.scope.binding.referencePaths: Array<NodePath>
它是一个数组,用于记录
path.node
被引用的各个节点。path.scope.binding.referenced: boolean
描述当前
path.node
节点是否被引用到。该属性可用于"tree-shaking"功能,如果没有被引用到,则该节点可被删除。
path.scope.bindings
该对象初始值为
Object.create(null)
,近似于{}
,但原型链查找链路只有一层(对象自身)。babel-traverse 在节点扫描过程中会为该对象不断添加当前
path.node
节点作用域内的变量绑定信息,设置方式为:// this 就是 path.scope this.bindings[name] = new Binding({ identifier: id, scope: this, path: bindingPath, kind: kind, });
1
2
3
4
5
6
7path.scope.uid: string
唯一的 scope id。
babel-traverse 过程中,各个
path.scope
具有唯一的uid
,如"_id1"
,其中id1
往往是当前变量的名称。path.scope.path: NodePath
反向引用:
path.scope.path
指向path
。path.scope.block
TODO
path.scope.globals
path.scope
的内部变量,用于当前节点所处的ProgramParent
节点存储一些绑定信息。path.scope.uids: { [name: string]: boolean }
TODO,待验证
初始值为
Object.create(null)
。该对象记录了
path.scope
作用域中涉及的所有节点的uid
,其值类似:{ _id1: true, _id2: true, _id3: false }
。path.scope.data: Object
path.scope.data
记录了 babel-traverse 运行过程中的一些数据信息,用于内部流转。path.scope.crawling: boolean
标志位,表示
path.scope
是否在初始化过程中,因为初始化中涉及对各类树的遍历操作,因此称为crawling
。path.scope.parent
指向父级
scope
节点。path.scope.parentBlock
指向父级
block
。path.scope.hub
TODO
遍历方法:
path.scope.traverse: Function
该方法就是 babel-traverse 对外暴露的
traverse
方法,用于遍历节点树。path.scope.generateUidIdentifier(name?: string): t.Identifier
生成基于
name
,且名称唯一的、新 Identifier 节点。path.scope.generateDeclaredUidIdentifier(name?: string): t.Identifier
生成基于
name
,且名称唯一的、新 Identifier 节点,不过其返回的是用path.scope.generateUidIdentifier
生成的新节点的副本节点。path.scope.generateUid(name?: string): string
基于
name
生成新的唯一的uid
。内部使用
do...while
保证了对名称唯一性验证的持续进行,同时新的uid
避免和以下场景冲突:this.hasLabel(uid)
: 和标记的关键词同名this.hasBinding(uid)
: 和绑定的变量同名this.hasGlobal(uid)
: 和全局变量同名this.hasReference(uid)
: 和被引用中的变量同名
path.scope.generateUidBasedOnNode(node: t.Node, defaultName?: string): string
基于
node
生成uid
。方法内部通过对node
的递归遍历,生成一个name
数组。案例TODOpath.scope.generateUidIdentifierBasedOnNode(node: t.Node, defaultName?: string): t.Identifier
生成既有基于
node
生成的新uid
的新Identifier
节点。path.scope.isStatic
TODO
path.scope.maybeGenerateMemoised
TODO
path.scope.checkBlockScopedCollisions
TODO
path.scope.rename
TODO
path.scope.dump
TODO
path.scope.hasLabel(name: string): boolean
path.node
是否含有标签类型的关键字。path.scope.getLabel(name: string): NodePath<t.LabeledStatement>
获取
path.node
携带的标签关键字节点。path.scope.registerLabel(path: NodePath<t.LabeledStatement>): void
将标签关键字节点注册到当前
scope
。path.scope.registerDeclaration(path: NodePath): void
根据
path
对应的节点类型,注册到各类map对象中。和
path.scope.binding.kind: string
字段对应的("var" | "let" | "const" | "module" | "hoisted" | "param" | "local" | "unknown"
),以下类型的节点会被注册:LabeledStatement
: 写入this.labels
对象FunctionDeclaration
: 写入hoisted
VariableDeclaration
: 写入path.scope.bindings
的var/let/const
类型ClassDeclaration
: 写入path.scope.bindings
的let
类型ImportDeclaration
: 写入path.scope.bindings
的module
类型ExportDeclaration
: 解构该节点,并递归注册- 无法识别的,注册为
unknown
path.scope.buildUndefinedNode(): t.UnaryExpression
生成一个
undefined
节点。path.scope.registerConstantViolation
TODO
path.scope.registerBinding
为
path.scope.bindings
对象设置当前path.node
节点对应作用域的所有绑定信息。path.scope.addGlobal
TODO
path.scope.hasUid(name: string): boolean
沿着
path.scope
及以上的scope
节点对象,判断是否含有uid
为name
的节点。path.scope.hasGlobal(name: string): boolean
判断当前节点及各级父节点中,每个
scope.globals
对象是否含有key
为name
的属性。path.scope.hasReference(name: string): boolean
直接根据顶层作用域节点
ProgramParent
得知是否含有uid
为name
的节点。path.scope.isPure
TODO
path.scope.crawl
初始化
path.scope
实例时执行的方法,crawl
意为"爬取",该方法有以下作用:- 初始化
path.scope
的一系列属性:references/bindings/globals/uids/data
- 遍历当前
path
节点及其子节点,注册path.scope
的各类属性
- 初始化
path.scope.getProgramParent(): Scope | null
向上遍历
scope
树,直到遇到类型是Program
的scope
节点。path.scope.getFunctionParent(): Scope | null
向上遍历
scope
树,直到遇到类型是Function
的scope
节点。path.scope.getBlockParent: Scope
从当前节点到各级父节点,判断是否有块级作用域的存在。块级作用域对应的节点有:
BlockStatement/Loop/Program/Function/Switch
。path.scope.getAllBindings: { [ key: string ]: Binding }
从当前节点到各级父节点,遍历时收集每个
scope
节点的scope.bindings
信息。返回收集的所有绑定信息对象。path.scope.getAllBindingsOfKind(...kinds: string[]): any
从当前节点到各级父节点,判断每个节点的类型是否匹配
kinds: string[]
中的元素,如有匹配,则收集该scope
节点的scope.bindings
信息。返回收集的所有绑定信息对象。path.scope.bindingIdentifierEquals(name: string, node: t.Node): boolean
从当前节点到各级父节点,获取
uid
为name
的标识符节点,并判断该节点和传入的node
是否相同。path.scope.getBinding(name: string): Binding | undefined
从当前节点到各级父节点,判断每个节点是否含有
uid
为name
的scope.binding
对象,如有,则就近返回第一个匹配的binding
对象。path.scope.getOwnBinding(name: string): Binding | undefined
获取当前节点中,
uid
为name
的scope.binding
对象。path.scope.getBindingIdentifier(name: string): t.Identifier | undefined
TODO PR从当前节点及各级父节点,获取索引
key
为name
的节点。path.scope.getOwnBindingIdentifier(name: string): t.Identifier | undefined
从当前节点的
binding
信息获取uid
为name
的节点。path.scope.hasOwnBinding(name: string): boolean
判断当前节点中,是否含有
uid
为name
的scope.binding
对象。path.scope.hasBinding(name: string, noGlobals?): boolean
判断当前节点及向上一级的父节点是否有
uid
为name
的绑定节点信息。path.scope.parentHasBinding(name: string, noGlobals?)
判断当前节点的向上一级的父节点,是否有
uid
为name
的绑定节点信息。path.scope.moveBindingTo(name: string, scope: Scope)
移动当前命名空间中名称为
name
的绑定信息到目标scope
命名空间。path.scope.removeOwnBinding(name: string): void
删除当前节点的
uid
为name
的绑定信息。path.scope.removeBinding(name: string): void
删除当前节点及各级父节点的
uid
为name
的绑定信息。
家族节点(family)
path.getOpposite(this: NodePath): NodePath | null
获取“对面”的兄弟节点,比如一个二元表达式含有左右两个节点,如
1 + 2
,节点是left
节点,该方法返回的就是right
节点。源码为:
function getOpposite(this: NodePath): NodePath | null { if (this.key === "left") { return this.getSibling("right"); } else if (this.key === "right") { return this.getSibling("left"); } return null; }
1
2
3
4
5
6
7
8path.getCompletionRecords
TODO
path.getSibling(this: NodePath, key: string | number): NodePath
获取对应
key
值的兄弟节点。path.getPrevSibling(this: NodePath): NodePath
获取前一个兄弟节点。
path.getNextSibling(this: NodePath): NodePath
获取下一个兄弟节点。
path.getAllNextSiblings(this: NodePath): NodePath[]
获取后面所有的兄弟节点。
path.getAllPrevSiblings(this: NodePath): NodePath[]
获取前面所有的兄弟节点。
path.get<T extends t.Node>(this: NodePath<T>, key: string, context?: true | TraversalContext ): NodePath | NodePath[]
根据
key
值获取目标节点。key的内容可以是"foo"
或"foo.bar"
这样的写法。path.getBindingIdentifiers( duplicates?: boolean ): Record<string, t.Identifier[] | t.Identifier>
获取和
path.node
节点绑定的标识符列表。该方法等同于t.getBindingIdentifiers()
path.getOuterBindingIdentifiers(duplicates: true ): Record<string, t.Identifier[]>
TODO
path.getBindingIdentifierPaths(this: NodePath, duplicates: boolean = false, outerOnly: boolean = false ): { [x: string]: NodePath;}
TODO
path.getOuterBindingIdentifierPaths(this: NodePath, duplicates?: boolean ): { [x: string]: NodePath;}
TODO
祖先节点(ancestry)
path.findParent(callback: (path: NodePath) => boolean ): NodePath | null
从当前节点的父节点开始,递归向上查找目标祖先节点,直到
callback
返回true
,callback
参数为递归向上过程的各级父节点。path.find(callback: (path: NodePath) => boolean ): NodePath | null
从当前节点开始,递归向上查找目标祖先节点,直到
callback
返回true
,callback
参数为递归向上过程的各级父节点。path.getFunctionParent(this: NodePath): NodePath<t.Function> | null
这是基于
findParent
的快捷方法,如果向上递归过程中,有一个祖先节点是函数,则停止查找并返回该节点。path.getStatementParent(this: NodePath): NodePath<t.Statement>
从当前节点开始,递归向上查找类型为
Statement
的祖先节点。path.getEarliestCommonAncestorFrom(this: NodePath, paths: Array<NodePath> ): NodePath
TODO
path.getDeepestCommonAncestorFrom
参数及返回值类型:
path.getDeepestCommonAncestorFrom(this: NodePath, paths: Array<NodePath>, filter?: (deepest: t.Node, i: number, ancestries: NodePath[][]) => NodePath): NodePath
1TODO
path.getAncestry(this: NodePath): Array<NodePath>
获取包含当前节点的所有祖先节点的节点列表。
path.isAncestor(this: NodePath, maybeDescendant: NodePath): boolean
TODO
path.isDescendant(this: NodePath, maybeAncestor: NodePath): boolean
判断
maybeAncestor
节点是否是node
的祖先节点,内部利用findParent
方法。path.inType(this: NodePath, ...candidateTypes: string[]): boolean
TODO
推断相关: inference
path.getTypeAnnotation(): t.FlowType
获取当前节点的类型推断。
path.isBaseType(baseName: string, soft?: boolean): boolean
是否是基本类型,包括:
string / number / boolean / any / mixed / empty / void
。值得注意的是,该方法接收一个
soft: boolean
属性,表示是否是宽松式地检查,如果值为false
,则会抛出错误。path.couldBeBaseType(name: string): boolean
判断当前节点是否是基础类型,包括:
string / number / boolean / any / mixed / empty / void
。TODO
path.baseTypeStrictlyMatches(rightArg: NodePath): boolean
TODO
path.isGenericType(genericName: string): boolean
TODO
计算相关: evaluation
path.evaluateTruthy(this: NodePath): boolean | undefined
计算当前表达式节点的计算结果是否可以转为
true
或false
,该方法可能有如下返回值:true
: 确定该表达式的计算结果可以转为true
false
: 确定该表达式的计算结果可以转为false
undefined
: 不确定该表达式的计算结果是否可转为true
还是false
对于该方法的返回值
returnedValue
,要特别注意要使用returnedValue === false
判断其是否为false
。path.evaluate(this: NodePath): { confident: boolean; value: any; deopt?: NodePath; }
计算当前表达式节点的值的相关信息。
当然这也是尝试计算,所以该方法的返回值结构为:
{ confident, // 表示是否确定能获取表达式的值,类型为 boolean value, // 表达式的值,默认为 undefined deopt // TODO }
1
2
3
4
5源码的注释里介绍了几种场景:
t.evaluate(parse("5 + 5")) // { confident: true, value: 10 } t.evaluate(parse("!true")) // { confident: true, value: false } t.evaluate(parse("foo + foo")) // { confident: false, value: undefined, deopt: NodePath }
1
2
3
introspection
path.matchesPattern(this: NodePath, pattern: string, allowPartial?: boolean ): boolean
判断当前节点是否匹配传入的
pattern
模板字符。比如pattern = 'React.createClass'
匹配的AST节点对象是React.createClass
和React["createClass"]
。方法内部依赖 babel-types:
t.matchesPattern(this.node, pattern, allowPartial)
。path.has(this: NodePath, key: string): boolean
判断
node
节点对象的node[key]
是否有值。这里额外判断了
node[key]
为数组时,是否为空数组的情况。path.is(this: NodePath, key: string): boolean
等同于
path.has
。path.isnt(this: NodePath, key: string): boolean
等同于
!path.is(this: NodePath, key: string): boolean
。path.isStatic(this: NodePath): boolean
function isStatic(this: NodePath): boolean { return this.scope.isStatic(this.node); }
1
2
3判断当前
node
节点所在的作用域是否是静态的。TODOpath.equals(this: NodePath, key: string, value): boolean
检查当前
node
的 key 值是否严格等于 value。path.isNodeType(this: NodePath, type: string): boolean
function isNodeType(this: NodePath, type: string): boolean { return t.isType(this.type, type); }
1
2
3判断当前节点类型
node.type
是否是目标类型type
。path.canHaveVariableDeclarationOrExpression
function canHaveVariableDeclarationOrExpression(this: NodePath) { return ( (this.key === "init" || this.key === "left") && this.parentPath.isFor() ); }
1
2
3
4
5判断当前节点是下述
for
循环中的变量:for (KEY in right); for (KEY;;);
1
2path.canSwapBetweenExpressionAndStatement
判断一个节点是否可以在表达式与语句之间相互转化,针对的是这种写法:
() => true () => { return true; }
1
2
3path.isCompletionRecord
TODO
path.isStatementOrBlock
TODO
path.referencesImport
TODO
path.getSource
TODO
path.willIMaybeExecuteBefore
TODO
path.resolve
TODO
path.isConstantExpression
TODO
path.isInStrictMode
判断当前节点是否位于严格模式的上下文中。
上下文相关(context)
path.call(this: NodePath, key: string): boolean
- 执行 Visitor 传入的相关节点处理方法
- 返回 true 时:TODO
- 返回 false 时:TODO
path.isDenylisted(this: NodePath): boolean
babelTraverse(node, opts)
中,opts
有配置项denyList: Array<string>
,配置不访问的节点。path.visit
path.skip
path.skipKey
path.stop
path.setScope
path.setContext
path.resync
path.popContext
path.pushContext
path.setup
path.setKey
path.requeue
替换节点: replacements
path.replaceWithMultiple<Nodes extends Array<t.Node>>(nodes: Nodes): NodePath[]
用多个节点替换掉当前节点,并返回替换后的
path
节点列表。path.replaceWithSourceString(this: NodePath, replacement): void
用字符串形式的代码,替换目标节点
node
。内部实现上,会使用
babel-parser
的parse
方法将代码字符串解析为 AST 节点,然后通过replaceWith
方法完成节点替换。解析代码字符串失败的话,会抛出错误,描述为
BABEL_REPLACE_SOURCE_ERROR
错误,并用babel-code-frame
描述出错的代码位置信息。path.replaceWith(this: NodePath, replacement: t.Node | NodePath)
TODO
path.replaceExpressionWithStatements(this: NodePath, nodes: Array<t.Statement>)
TODO
path.replaceInline(this: NodePath, nodes: t.Node | Array<t.Node>)
TODO
转换节点: conversion
path.toComputedKey(this: NodePath)
path.ensureBlock
TODO
path.arrowFunctionToShadowed
path.unwrapFunctionEnvironment
path.arrowFunctionToExpression
将箭头函数转为 ES5 里的函数表达式。
参数:
allowInsertArrow
: TODOspecCompliant
: TODOnoNewArrows
: TODO
删除节点: removal
path.remove
修改节点: modification
path.insertBefore
path.insertAfter
path.updateSiblingKeys
path.unshiftContainer
path.pushContainer
path.hoist
注释相关: comments
path.shareCommentsWithSiblings
path.addComment
添加一条注释。
path.addComments
添加多条注释。
校验节点: validate
文件
packages/babel-traverse/src/path/index.ts
中,为NodePath.prototype
上添加了校验方法,这些方法全部来自 babel-types。使用方式为
path.is{NodeName}
,如path.isFunctionStatement(...)
。
# 6.1.4 state
babel-traverse 负责调用插件传入的处理方法,其本身是支持传入一个初始化的state的,开发者可以根据自己需要传入一个初始化的 state 值,traverse
方法的定义如下:
function traverse(
parent: t.Node,
opts: TraverseOptions = {},
scope?: Scope,
state?: any,
parentPath?: NodePath,
) {
...
}
2
3
4
5
6
7
8
9
开发者实际使用的时候,一般不需要独立使用 babel-traverse,而是根据 babel-core 间接调用 babel-traverse,babel-core 在内部为 state 对象提供了一些属性、方法,便于开发者使用,如下图所示:
scope - index - crawl
在开发者没有设置 state 的情况下,内置遍历过程提供的 state 对象是 babel-core/src/transformation/plugin-pass.ts
定义的 PluginPass
实例。
state 实例本身内容并不复杂,其有如下属性或方法:
state.key
state.cwd
state.filename
state.file
state.file
对象是File
类的实例。File
类定义自文件babel/packages/babel-core/src/transformation/file/file.ts
。state.file.availableHelper
state.file.addHelper
state.file.addImport
state.file.buildCodeFrameError
# 6.2 节点操作: babel-types
Babel 使用 babel-types 进行节点操作。babel-types 对外暴露出的 API,总体来说,可以分为对节点的“读操作”和“写操作”。
- 读节点
- 节点校验
- 查找节点
- 节点遍历
- 写节点
- 创建节点
- 修改节点
- 转换节点
- 操作注释
- 复制节点
babel-types 也提供了对 TypeScript 和 Flow 的 API 支持
下文中,用 t
代表 babel-types
对外暴露的对象。
# 6.2.1 读节点
读操作api分布:
查找节点
t.getBindingIdentifiers
参数与返回值类型
t.getBindingIdentifiers(node: t.Node, duplicates?: boolean, outerOnly?: boolean ): Record<string, t.Identifier> | Record<string, Array<t.Identifier>>
1TODO
t.getOuterBindingIdentifiers
参数与返回值类型
t.getOuterBindingIdentifiers(node: t.Node, duplicates?: boolean ): Record<string, t.Identifier> | Record<string, Array<t.Identifier>>
1TODO
校验节点
t.is
参数与返回值类型
// 较源码做了简化 t.is(type: string, node: t.Node | null | undefined, opts?: undefined ): boolean
1
2功能
判断给定的节点
node
的类型是否是type
的值。
t.is{NodeName}
t.isBinding(node: t.Node, parent: t.Node, grandparent?: t.Node): boolean
TODO
t.isBlockScoped(node: t.Node): boolean
判断
node
节点是否应用于块级作用域。满足条件的节点包括:函数声明(
FunctionDeclaration
)、类声明(ClassDeclaration
)、let/const
声明的变量、TODOt.isImmutable(node: t.Node): boolean
TODO
t.isLet(node: t.Node): boolean
function isLet(node: t.Node): boolean { return ( isVariableDeclaration(node) && (node.kind !== "var" || node[BLOCK_SCOPED_SYMBOL]) ); }
1
2
3
4
5
6t.isNode(node: any): node is t.Node
function isNode(node: any): node is t.Node { return !!(node && VISITOR_KEYS[node.type]); }
1
2
3是否是
node
节点的条件:节点存在且是合法的节点类型。t.isNodesEquivalentisNodesEquivalent(a, b): boolean
判断两个节点是否相等。
t.isPlaceholderType(placeholderType: string, targetType: string): boolean
TODO
t.isReferenced(node: t.Node, parent: t.Node, grandparent?: t.Node): boolean
TODO
t.isScope(node: t.Node, parent: t.Node): boolean
TODO
t.isSpecifierDefault(specifier: t.ModuleSpecifier): boolean
TODO
t.isType(nodeType: string | null | undefined, targetType: string): boolean
TODO
t.isValidES3Identifier(name: string): boolean
TODO
t.isValidIdentifier(name: string, reserved: boolean = true): boolean
TODO
t.isVar(node: t.Node): boolean
TODO
t.matchesPattern(member: t.Node | null | undefined, match: string | string[], allowPartial?: boolean): boolean
TODO
t.validate(node: t.Node | undefined | null, key: string, val: any): void
TODO
t.buildMatchMemberExpression(match: string, allowPartial?: boolean)
TODO
遍历节点
t.traverse<T>(node: t.Node, handlers: TraversalHandler<T> | TraversalHandlers<T>, state?: T): void
TODO
t.traverseFast(node: t.Node | null | undefined, enter: (node: t.Node, opts?: any) => void, opts?: any): void
TODO
# 6.2.2 写节点
写操作api分布:
创建节点
t.{节点名称}
api格式
babel-types 创建节点的 API 以
t.{节点名称}
格式命名,其中节点名称既支持大写开头,也支持小写开头。如创建
Variance
的话,可以是:t.Variance
或t.variance
。创建节点的参数
创建节点的参数由节点定义决定。
例如,创建一个
for
循环语句(ForStatement
):for (let i = 0; i < 3; i++) { }
1
2
3babel-types 中的 API 是
t.forStatement(...)
那么,使用者如何知道
t.forStatement
方法所需的参数呢?babel-types 创建 API 的参数可参考
babel-types/src/definitions/
目录下各文件对节点的定义的builder
和fields
字段。ForStatement
节点的定义在babel-types/src/definitions/core.ts
文件,定义内容:defineType("ForStatement", { visitor: ["init", "test", "update", "body"], aliases: ["Scopable", "Statement", "For", "BlockParent", "Loop"], fields: { init: { validate: assertNodeType("VariableDeclaration", "Expression"), optional: true, }, test: { validate: assertNodeType("Expression"), optional: true, }, update: { validate: assertNodeType("Expression"), optional: true, }, body: { validate: assertNodeType("Statement"), }, }, });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21builder
字段提供参数名称及顺序,field
字段提供每个字段的格式校验规则。案例
从
ForStatement
节点的定义可以看到,t.ForStatement
方法的参数如下:init
: 初始化节点就是例子中的
let i = 0
。可以是变量声明(
VariableDeclaration
)、表达式(Expression
)。test
: 条件语句节点就是例子中的
i < 3
。需要是表达式(
Expression
)。update
: 更新语句节点就是例子中的
i++
。需要是表达式(
Expression
)。body
: 循环体就是例子中的循环体部分。
综上,目标 for 循环语句:
for (let i = 0; i < 3; i++>) { }
1
2
3创建该节点的方法为:
TODO
const forStatement = t.forStatement( );
1
2
3
部分 AST 节点没有对应的创建节点 API
babel-types 中,除了个别节点,其余节点均有对应的创建节点 API。
比如
Function
节点,babel-types
没有提供function
API 以创建Function
节点,一方面是因为function
本身就是 ECMAScript 的关键字,另一方面,函数要么以函数声明(FunctionDeclaration
)出现,要么以函数表达式(FunctionExpression
)出现,没有必要出现一个Function
。笔者整理了一些没有没有对应的创建 API 的节点,如下:
Function
Statement
Declaration
Expression
ObjectMember
UnaryOperator
UpdateOperator
BinaryOperator
LogicalOperator
Pattern
Class
ClassPrivateMethod
StaticBlock
ModuleDeclaration
ModuleSpecifier
ImportAttribute
修改节点
t.appendToMemberExpression
t.inherits
t.prependToMemberExpression
t.removeProperties
t.removePropertiesDeep
t.removeTypeDuplicates
转换节点
t.ensureBlock
t.toBindingIdentifierName
t.toBlock
t.toComputedKey
t.toExpression
t.toIdentifier
t.toKeyAlias
t.toSequenceExpression
t.toStatement
t.valueToNode
操作注释
t.addComment
t.addComments
t.inheritInnerComments
t.inheritLeadingComments
t.inheritsComments
t.inheritTrailingComments
t.removeComments
复制节点
t.cloneNode
t.clone
t.cloneDeep
t.cloneDeepWithoutLoc
t.cloneWithoutLoc