2022-10-7 About 25 min

接触过 Babel 插件编写的话,可能有个感受,官方文档提供的介绍太少了,社区中关于如何写插件介绍的比较多,但对其中用到的工具的介绍却比较少。

为什么需要插件呢?

babel-parser 主要有两个功能:

  1. 识别各种语法,且默认情况下,并不开启所有语法的识别支持,这是出于性能考虑
  2. 解析源码字符串为抽象语法树,并不负责对源码的转译

在 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
    21

    traverse 接收一些参数,并处理后,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: TODO

      TODO

    • parentPath: TODO

      TODO

# 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
      12

      babel-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.tsNode 字段。

    • 节点别名

      定义在 babel-types/src/ast-types/generated/index.tsAliases 字段,以及定义在 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 的两个参数是 pathstate
      • fn 执行的时候,上下文是 state
      • newFn 重写了 toString 方法,当需要打印函数体时,可以还原 fn,这是一个值得借鉴的细节
    • Visitor 的参数

      上述代码中,Visitor 对象提供的处理函数 fn 可接收 2 个参数:pathstate,且 this 的值为 state

      pathstate 对象描述了当前被访问节点的各类信息,这两个参数携带的属性和方法非常多,下文另开一节对其详细介绍。

  • 节点处理队列

    插件开发者提供的 Visitor,在 babel-traverse 处理时,会针对每个 Visitor 的 key(节点名称),会有一个数组用于存储插件传入的处理方法,也就是说,针对一个节点,有一个处理队列。

# 6.1.3 path

path 对象是内部 NodePath 类的实例。

# path功能

path 上的 API 非常多,如下图所示:

Babel插件编写/path

根据上图,从来源看,path 作为 NodePath 类的实例,该对象上的 API 可以分为几类:

  • path 对象自身的属性和方法
  • path 继承自 NodePath 类的原型上的属性和方法
  • 各功能模块对外暴露的属性和方法,被添加到 NodePath.prototype,便于 path 使用
  • 各功能功能模块为 path 对象直接设置的属性/方法

从功能上,path 有以下能力:

  • 断言与校验,等同于 babel-types 的断言
  • 操作祖先节点、家族节点
  • 替换、转换、修改、删除节点
  • 节点计算操作
  • 获取节点上下文的信息
  • 获取节点命名空间的信息
  • 和注释节点相关的操作
  • ...

# API

笔者将下文中部分 API 的调用链路进行了图解,重点描述:path.context、插件引入的方法执行链路等信息。

Babel插件编写/path-context-visit

阅读建议:下文的 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
      12
    • path.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
      3
    • path.parent

      表示当前节点的父节点。

  • 上下文信息: path.context

    • path.context: TraversalContext

      path.contextTraversalContext 类的实例,表示当前访问节点的上下文信息。文件 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,两个参数分别指向 pathpath.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

        变量 keybinding.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
      7
    • path.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 数组。案例TODO

    • path.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.bindingsvar/let/const 类型
      • ClassDeclaration: 写入 path.scope.bindingslet类型
      • ImportDeclaration: 写入path.scope.bindingsmodule类型
      • 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 节点对象,判断是否含有 uidname 的节点。

    • path.scope.hasGlobal(name: string): boolean

      判断当前节点及各级父节点中,每个scope.globals对象是否含有keyname的属性。

    • path.scope.hasReference(name: string): boolean

      直接根据顶层作用域节点 ProgramParent 得知是否含有 uidname 的节点。

    • 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树,直到遇到类型是Programscope节点。

    • path.scope.getFunctionParent(): Scope | null

      向上遍历 scope 树,直到遇到类型是 Functionscope 节点。

    • 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

      从当前节点到各级父节点,获取 uidname 的标识符节点,并判断该节点和传入的 node 是否相同。

    • path.scope.getBinding(name: string): Binding | undefined

      从当前节点到各级父节点,判断每个节点是否含有 uidnamescope.binding 对象,如有,则就近返回第一个匹配的 binding 对象。

    • path.scope.getOwnBinding(name: string): Binding | undefined

      获取当前节点中,uidnamescope.binding 对象。

    • path.scope.getBindingIdentifier(name: string): t.Identifier | undefined TODO PR

      从当前节点及各级父节点,获取索引 keyname 的节点。

    • path.scope.getOwnBindingIdentifier(name: string): t.Identifier | undefined

      从当前节点的 binding 信息获取 uidname 的节点。

    • path.scope.hasOwnBinding(name: string): boolean

      判断当前节点中,是否含有 uidnamescope.binding 对象。

    • path.scope.hasBinding(name: string, noGlobals?): boolean

      判断当前节点及向上一级的父节点是否有 uidname 的绑定节点信息。

    • path.scope.parentHasBinding(name: string, noGlobals?)

      判断当前节点的向上一级的父节点,是否有 uidname 的绑定节点信息。

    • path.scope.moveBindingTo(name: string, scope: Scope)

      移动当前命名空间中名称为 name 的绑定信息到目标 scope 命名空间。

    • path.scope.removeOwnBinding(name: string): void

      删除当前节点的 uidname 的绑定信息。

    • path.scope.removeBinding(name: string): void

      删除当前节点及各级父节点的 uidname 的绑定信息。

  • 家族节点(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
      8
    • path.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 返回 truecallback 参数为递归向上过程的各级父节点。

    • path.find(callback: (path: NodePath) => boolean ): NodePath | null

      从当前节点开始,递归向上查找目标祖先节点,直到 callback 返回 truecallback 参数为递归向上过程的各级父节点。

    • 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
      
      1

      TODO

    • 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

      计算当前表达式节点的计算结果是否可以转为 truefalse,该方法可能有如下返回值:

      • 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.createClassReact["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 节点所在的作用域是否是静态的。TODO

    • path.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
      2
    • path.canSwapBetweenExpressionAndStatement

      判断一个节点是否可以在表达式与语句之间相互转化,针对的是这种写法:

      () => true
      
      () => { return true; }
      
      1
      2
      3
    • path.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-parserparse 方法将代码字符串解析为 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: TODO

      • specCompliant: TODO

      • noNewArrows: 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,
) {
    ...
}
1
2
3
4
5
6
7
8
9

开发者实际使用的时候,一般不需要独立使用 babel-traverse,而是根据 babel-core 间接调用 babel-traverse,babel-core 在内部为 state 对象提供了一些属性、方法,便于开发者使用,如下图所示:

Babel插件编写/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分布:

Babel插件编写/babel-types读操作

  • 查找节点

    • t.getBindingIdentifiers

      • 参数与返回值类型

        t.getBindingIdentifiers(node: t.Node, duplicates?: boolean, outerOnly?: boolean ): Record<string, t.Identifier> | Record<string, Array<t.Identifier>>
        
        1
      • TODO

    • t.getOuterBindingIdentifiers

      • 参数与返回值类型

        t.getOuterBindingIdentifiers(node: t.Node, duplicates?: boolean ): Record<string, t.Identifier> | Record<string, Array<t.Identifier>>
        
        1
      • TODO

  • 校验节点

    • 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 声明的变量、TODO

    • t.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
      6
    • t.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分布:

Babel插件编写/babel-types写操作

  • 创建节点

    t.{节点名称}

    • api格式

      babel-types 创建节点的 API 以 t.{节点名称} 格式命名,其中节点名称既支持大写开头,也支持小写开头。

      如创建 Variance 的话,可以是:t.Variancet.variance

      Babel插件编写/babel-types创建节点

    • 创建节点的参数

      • 创建节点的参数由节点定义决定。

        例如,创建一个 for 循环语句(ForStatement):

        for (let i = 0; i < 3; i++) {
        
        }
        
        1
        2
        3

        babel-types 中的 API 是 t.forStatement(...)

        那么,使用者如何知道 t.forStatement 方法所需的参数呢?

        babel-types 创建 API 的参数可参考 babel-types/src/definitions/ 目录下各文件对节点的定义的 builderfields 字段。

        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
        21

        builder 字段提供参数名称及顺序,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

# 6.3 插件编写实战

Last update: October 10, 2022 15:39
Contributors: hoperyy