Babel API
Nothing someone says before the word “but” really counts.
0x01 Babel Intro
上一节使用Python实现了一个简易的解析器,了解了源代码到AST的转换过程。但若从头开始实现一个编译器是十分复杂的。
Babel 是一个工具链,主要用于在当前和旧的浏览器或环境中,将 ECMAScript 2015+ 代码转换为 JavaScript 向后兼容版本的代码。它能做下面的事情:
转换语法
源代码转换
polyfill目标环境缺少的功能
Babel编译过程主要有以下三个阶段
解析(Parse):将输入字符流解析为AST抽象语法树
转化(Transform):对抽象语法树进一步转化
接收AST并对其进行遍历,在此过程中对节点进行添加、更新、移除等操作,后续的混淆JS代码以及混淆过的JS代码进行还原都在此处。
生成(Generate):根据转化后的语法树生成目标代码
之所以把源码转化为抽象语法树,是因为树状结构更加容易进行原子操作,可以对任意的节点进行精细化处理。抽象语法树中,代码间的关系被抽象为节点间的关系,实现相同功能的节点之间的表示也相同。因此我们不需要关心具体的代码,只要指定几条规则就行
源代码:
let obj = {
name: 'test',
add: function(a, b) {
return a + b + 10;
},
mul: function(a, b) {
return a + b * 2;
}
}使用Babel生成AST并进行转换
Babel有很多组件和API
@babel/parser: 将JS代码转换为AST@babel/travers: 遍历AST节点@babel/types: 判断节点类型、生成新节点@babel/generator: 把AST转化为JS代码
Babel处理JS文件的流程
读取JS文件
解析成AST
对AST节点进行增删改查
生成新的JS代码并保存
0x02 parser|generator
parser: JS转AST
generator: AST转JS
parser.parse返回一个对象,可以使用JSON.stringify将其转化为JSON字符串第二个参数是一个对象,
sourceType默认为script,当解析的JS代码中含有import、export等关键字时,需要指定sourceType为module
generator返回的一个对象,其code属性才是需要的代码retainLines: 是否使用与源代码相同的行号,默认false,输出的是格式化后的代码comments: 是否保留注释,默认truecompact: 是否压缩代码
0x03 traverse|visitor
traverse用于遍历节点,配合visitor进行节点过滤
打印了两次visit FunctionExpression
visitor中方法的参数path对应Path对象而非节点对象Node
visitor的定义一般有如下三种方式:
在遍历节点的过程中,有两次机会访问一个节点,分别是进入节点(enter)和退出节点(exit)
可以把同一个函数应用到多个节点,|连接方法名
也可以把多个函数应用于同一个节点
当遍历到某个节点时,我们可以定义新的visitor对其下的子树进行特殊的处理,如下面代码的功能为替换所有函数的第一个参数为x
先根据visitor去traverse所有节点,当遍历到FunctionExpression节点时,再用updateParamNameVisitor去遍历当前节点下的所有子节点。使用path.traverse时还可以额外传入一个对象,在对应的visitor中可以用this去引用。
0x04 types
用于判断节点类型、生成新的节点
下面两种写法等价,用于把所有变量名x改成n
下面用types来生成我们最早的代码
由上一节的AST在线解析的分析,首先要生成一个variableDeclaration,kind参数是声明类型,declarations是一个VariableDeclarator数组
id为Identifier, init后面跟着?表示可选参数,init表示是否在变量声明时初始化变量,这里需要一个ObjectExpression
对象可以有多个属性,因此properties是个数组,我们需要三个ObjectProperty
第一个属性值是StringLiteral
第二、三个属性值是FunctionExpression
id: 函数名Identifierparams: 参数数组body: 函数体BlockStatement
body为Statement数组,我们的函数体内只有返回语句
返回语句由二元表达式构成
完整代码:
生成的代码:
上面用到了StringLiteral和NumericLiteral, Babel 中还定义了一些其他的字面量
当生成比较多的字面量时,会比较麻烦,所以Babel还提供了valueToNod
0x05 Path
Node对象包含在Path对象中,下面会介绍Path对象的属性及方法

Path Methods
获取Node/Path
为获取节点的属性值,一般先访问到该节点,再直接点访问(.)属性

但这种方法获取的是Node或具体属性值,无法使用Path类的方法

path.get支持多级访问,如path.get('left.type')
得到的是封装的NodePath对象,就可以使用Path相关方法
判断Path类型
上面的types组件可以用来判断Node的类型
Path也提供了判断自身类型的方法
节点转代码
很多时间需要在执行过程中把部分节点转为代码,而非在最后才把整个AST转成代码,generator组件可以做到
Path对象重写了Object的toString,其中调用了generator组件把节点转为代码,因此可以直接path.toString()来把节点转为代码,或者path + ''隐式转换
替换节点属性
获取节点属性直接赋值即可
替换整个节点
replaceWith:一换一
replaceWithMultiple:多换一
replaceInline:接收一个参数,若该参数不为数组,等价于replaceWith;若该参数为数组,等价于replaceWithMultiple
replaceWithSourceString:用字符串源码替换节点
删除节点
EmptyStatement为空语句,就是多余的分号
插入节点
Parent Path
Path对象有两个属性
parentPath:NodePath父级Pathparent:Node父节点
path.findParent
一些情况下需要从一个路径向上遍历语法树,直到满足相应的条件

path.find
使用方法和上面的path.findParent一致,不过find方法查找的范围包含当前节点,而findParent不包含
path.getFunctionParent
向上查找与当前节点最近的函数父节点
path.getStatementParent
向上查找与当前节点最近的语句父节点,如声明语句、return语句、if语句、switch语句、while语句
查找包含当前节点,因此在这个例子中需要从parentPath中去调用
Parallel Path
先了解一下container

listKey:容器名
key:当前节点在容器中的位置
container:存储同级节点的数组
(有时候当前节点没有同级节点,container可能为单个Node,而非数组)
path.inList:判断是否有同级节点(注意,当container为数组但只有一个成员时,会返回true)
path.getSibling(index):获取同级Path,index为容器数组中的索引,index可以通过对path.key进行加减操作来定位到不同
unshiftContainer、pushContainer
unshiftContainer往容器最前面加入节点
pushContainer往容器最后面加入节点
0x05 scope
scope可以方便地查找标识符的作用域,获取并修改标识符的所有引用,以及判断标识符是否为参数或常量
以下面的代码为例
获取标识符作用域
scope.block属性来获取标识符作用域,返回Node对象
上述代码中变量e是定义在add函数内部的,作用域范围为整个add
若遍历的是一个函数,需要去获取父级作用域
标识符绑定
scope.getBinding('a')

constant:判断当前变量是否被更改
identifier:标识符的Node对象
kind:表明a是add的参数
referenced:标识符是否被引用
references:被引用的次数
scope.getOwnBinding用于获取当前节点自己的绑定,即不包含父级作用域中定义的标识符的绑定,但该函数会得到子函数中定义的标识符的绑定
可以看到demo中定义的d变量也通过getOwingBinding获取到了。若只想获取当前节点下定义的标识符,而不涉及子函数的话,还需要进一步判断,可以通过判断标识符作用域是否与当前函数的一致来确定
通过binding.scope.block获取标识符作用域,转为代码后再与当前节点的代码比较,可以确定是否为当前函数中定义的标识符
referencePaths
若标识符被引用,referencePaths中会存放所有引用该标识符节点的Path对象(NodePath对象)

add方法return处引用了两次a,其父节点为BinaryExpression,两个NodePath的key分别为left和right
constantViolations
若标识符被修改,constantViolations中会存放所有修改了该标识符节点的Path对象(NodePath数组)

add方法的第一行修改了a标识符a = 400
scope.traverse
用于遍历作用域中的节点
上述代码将FunctionExpression下的a标识符作用域中对a的赋值语句改成赋值为666
scope.rename
标识符重命名,这个方法会同时修改所有引用该标识符的地方
如下将add函数中的b变量重命名为x
但我们指定的'x'可能会与现有标识符冲突,这时可以使用scope.generateUidIdentifier来生成一个标识符,这样就可以实现一个简单的标识符混淆
让我们多定义一些函数,并让其局部变量重名
可以发现不同函数的局部变量混淆过后的变量名不同,实际上可以让混淆之后的变量名重复,甚至让函数中的局部变量跟当前函数中没有引用到的全局变量重名(如原始代码中的全局变量a和add中的a参数),以此做到更复杂的混淆。
scope的其他方法scope.hasBinding('a')查询是否有标识符a的绑定scope.getBinding('a')返回undefined等价于scope.hashBinding('a')返回falsescope.hasOwnBinding('a')查询当前节点是否有自己的绑定上面的demo函数中,OwnBinding只有一个d,函数名demo属于父级作用域
scope.getAllBindings获取当前节点的所有绑定,返回一个对象,以标识符名为属性名,对于的Binding为属性值
scope.hasReference('a')查询当前节点是否有a标识符的引用
scope.getBindingIdentifier('a')获取当前节点中绑定的a标识符,返回Identifier的Node对象
Last updated