Appearance
Path和Node的用法
前言
文本介绍了 Babel
中的 Path
和 Node
的一些用法,在日常工作中应该都会涉及到,所以文章篇幅比较长,建议同学可以边看边动手去尝试调试输出下,会让自己更明白些。
Path 和 Node 的区别
在使用 AST 的过程中,接触最多的就是 path
和 node
,对于 AST 的初学者来说容易傻傻分不清楚,对于 node
来说就是 path
对象的一个 属性
,path.node
能取出当前 path
的 Node
对象,该对象与 AST Explorer 网页中解析出来的 AST 节点结构一致。
简单来说,Node
节点是生成 JS
代码的原材料,是 Path
中的一部分。 Path
是一个对象,用来描述两个节点之间的连接。Path
除了具有上述显示的这些属性以外,还包括 添加
、更新
、移动
和 删除
节点等很多有关的方法。
Path 中的方法
Path 中有很多的方法,以下做一些简单的汇总,然后一一做简单举例说明:
获取子节点/Path
判断 Path 类型
Path/Node转代码
替换节点属性
替换整个节点
删除节点
插入节点
获取子节点/Path
为了得到 AST 节点的属性值,一般会先访问到该节点,然后利用 path.node.property
方法获取属性,如下代码:
js
const visitor = {
BinaryExpression(path) {
console.log(path.node.left);
console.log(path.node.right);
console.log(path.node.opeartor);
}
};
traverse(ast, visitor);
/*
Node {type: 'BinaryExpression', ..., left: Node}
Node {type: 'NumericLiteral', ..., extra: {...}}
+
Node {type: 'Identifier', ..., name: 'a'}
Node {type: 'Identifier', ..., name: 'b'}
*/
通过这种方法直接 对象.属性
的方法获取到的是 Node
或者具体的属性值, Node
不能使用 Path
相关的方法,如果想要获取到该属性的 Path
,就需要使用 Path
对象的 get
的方法,传递的参数为 key(即该属性名的字符串形式)。如果是多级访问,那么就以 点
进行连接多个 key即可,如下:
js
const visitor = {
BinaryExpression(path) {
console.log(path.get('left.name'));
console.log(path.get('right'));
console.log(path.get('opeartor'));
}
};
traverse(ast, visitor);
/* 会输出 n 个 NodePathNodePath {parent: Node, hub: undefined, contexts: Array(0), data: null, _traverseFlags: 0}
*/
可以看到,任何形式的属性值,通过 Path
对象的 get
方法去获取,都会被包装成 Path
对象在返回。不过像 operator、name
这一类的属性值没必须要包装成 Path
对象
判断 Path 类型
Path
对象提供相应的方法来判断自身类型,使用方法与 types 组件差不多,只不过 types 组件判断的是 Node
的类型,下面以 a + b + 10
为例子:left是 a + b
,right是10
。
js
const visitor = {
BinaryExpression(path) {
console.log(path.get('left').isIdentifier());
console.log(path.get('right').isNumbericLiteral({
value: 10
}));
path.get('left').assertIdentifier();
}
};
traverse(ast, visitor);
/* 输出
false
true
报错
*/
Path/Node转代码
当代码比较复杂时,遇到问题就需要采用 调试模式
进行调试,比如在 WebStorm
中在行号处点击出现红点后,以调试模式运行就会断到此处,这里跟调试普通的 JS 代码没有区别,适当的可以在某些位置插入一下吐出语句(console.log())来排查错误,在对当前节点排查时,可以把当前的 Path
或者 Node
转换为 JS 代码,来查看下当前处理的节点是否符合自己的预期。可以使用如下代码转换为 JS 代码:
js
const visitor = {
FunctionExpression(path){
// Path转换为 JSconsole.log(path.toString());
// Node转换为 JS,前提是导入了 generator 组件
console.log(generator(path.node).code);
}
}
替换节点属性
替换节点属性与获取节点属性方法相同,只是改为赋值语句,但是也并非随意替换,需要注意的是,替换的类型要在允许的类型范围内,因此需要熟悉 AST 的结构,如下图所示:
js
/* 原代码
let obj = {
name: "mankvis",
add: function (a, b) {
return a + b;
},
mul: function (a, b) {
return a + b;
},
}
*/
const visitor = {
BinaryExpression(path) {
path.node.left = types.identifier('x');
path.node.right = types.identifier('y');
}
}
traverse(ast, visitor);
/* 替换后的代码
let obj = {
name: "mankvis",
add: function (a, b) {
return x + y;
},
mul: function (a, b) {
return x + y;
},
}
*/
替换整个节点
Path
对象中与替换相关的方法有 replaceWith
、replaceWithMultiple
、replaceInline
和 replaceWithSourceString
。
replaceWith
是用一个节点替换另一个节点,并且是严格的一换一,示例代码如下:
js
/* 原代码
let obj = {
name: "mankvis",
add: function (a, b) {
return a + b;
},
mul: function (a, b) {
return a + b;
},
}
*/
const visitor = {
BinaryExpression(path) {
path.replaceWith(types.stringLiteral('Mankvis'))
}
}
traverse(ast, visitor);
/* 替换后的代码
let obj = {
name: "mankvis",
add: function (a, b) {
return "mankvis";
},
mul: function (a, b) {
return "mankvis";
},
}
*/
replaceWithMultiple
也是用来替换节点,只不过是多换一,示例代码如下:
js
/* 原代码
let obj = {
name: "mankvis",
add: function (a, b) {
return a + b;
},
mul: function (a, b) {
return a + b;
},
}
*/
const visitor = {
ReturnStatement(path) {
path.replaceWithMultiple([
types.expressionStatement(types.stringLiteral("mankvis")),
types.expressionStatement(types.numericLiteral(10)),
types.returnStatement()
]);
}
}
traverse(ast, visitor);
/* 替换后的代码
let obj = {
name: "mankvis",
add: function (a, b) {
"mankvis";
10;
return;
},
mul: function (a, b) {
"mankvis";
10;
return;
},
}
*/
上述代码中有两处要特别说明,当表达式语句单独在一行时(没有赋值),最好用 expressionStatement
包裹;替换后的节点,traverse 也是能遍历到的,因此替换时要极其小心,否则容易造成不合理的递归调用。例如上述代码,把 return 语句进行替换,但是替换里面又有 return 语句,就会陷入死循环,解决方法是加入 path.stop
,替换完成之后立刻停止遍历当前节点和后续的子节点。
replaceInline
,该方法接收一个参数,如果参数不是数组,那么 replaceInline
等同于 replaceWith
,如果参数是一个数组,那么 replaceInline
等同于 replaceWithMultiple
,其中数组成员必须都是节点。示例代码如下:
js
/* 原代码
let obj = {
name: "mankvis",
add: function (a, b) {
return a + b;
},
mul: function (a, b) {
return a + b;
},
}
*/
const visitor = {
ReturnStatement(path) {
path.replaceInline([
types.expressionStatement(types.stringLiteral("mankvis")),
types.expressionStatement(types.numericLiteral(10)),
types.returnStatement()
]);
path.stop();
}
}
traverse(ast, visitor);
/* 替换后的代码
let obj = {
name: "mankvis",
add: function (a, b) {
"mankvis";
10;
return;
},
mul: function (a, b) {
"mankvis";
10;
return;
},
}
*/
上述代码中, visitor 中的函数也加入了 path.stop
,原因和之前介绍的一样。
replaceWithSourceString
,该方法是用字符串源码替换节点,如把原始代码中的函数改为闭包形式,示例代码如下:
js
/* 原代码
let obj = {
name: "mankvis",
add: function (a, b) {
return a + b;
},
mul: function (a, b) {
return a + b;
},
}
*/
const visitor = {
ReturnStatement(path) {
let argumentPath = path.get('argument');
argumentPath.replaceWithSourceString(
'function(){return' + argumentPath + '}()'
)
path.stop();
}
}
traverse(ast, visitor);
/* 替换后的代码
let obj = {
name: "mankvis",
add: function (a, b) {
return function(){
return a + b;
}
},
mul: function (a, b) {
return function(){
return a * b;
}
},
}
*/
删除节点
在处理混淆代码的时候,在还原代码后,需要删除一些无用的代码,就需要用到删除节点,示例代码如下:
js
traverse(ast, {
EmptyStatement(path) {
path.remove();
}
})
EmptyStatemnt
指的是空语句,就是多余的分号,使用 path.remove()
删除当前节点。
插入节点
想要把节点插入到兄弟节点中,可以使用 insertBefore
和 insertAfter
,分别是在当前节点的前后插入节点,示例代码如下:
js
/* 原代码
let obj = {
name: "mankvis",
add: function (a, b) {
return a + b;
},
mul: function (a, b) {
return a + b;
},
}
*/
const visitor = {
ReturnStatement(path) {
path.insertBefore(types.expressionStatement(types.stringLiteral("Before")));
path.insertBefore(types.expressionStatement(types.stringLiteral("After")));
}
}
traverse(ast, visitor);
/* 替换后的代码
let obj = {
name: "mankvis",
add: function (a, b) {
return function(){
"Before";
return a + b;
"After";
}
},
mul: function (a, b) {
return function(){
"Before";
return a * b;
"After";
}
},
}
*/
父级 Path 中的方法
以上介绍了 Path
的一些使用方法,接着介绍一些 父级Path
的使用方法。
在 visitor
处理过程中,使用 console.log()
打印当前 path 节点的时候,会发现有两个属性,分别是 parentPath
和 parent
,其中 parentPath
类型为 NodePath
,所以它是 父级 Path
,而 parent
类型则为 Node
,所以它是父节点。只要获取到 父级 Path
,那么就可以调用 Path
的各种方法去操作父节点。
父级 Path
的获取可以使用 path.parentPath
,父级 Path 的 Node
对象可以使用 path.parent
来获取。其实就是说 path.parent = path.parentPath.node
,两个效果相同。
path.findParent
path.find
path.getFunctionParent
path.getStatementParent
path.findParent
个别情况下,需要从一个路径向上遍历语法树,直到满足相应的条件。这时可以使用 Path
对象的 findParent
,示例代码如下:
js
const visitor = {
ReturnStatement(path) {
// console.log(path.findParent((p) => p.isObjectExpression()));
const newPath = path.findParent(function (p){return p.isObjectExpression()})
console.log(newPath.toString())
}
}
traverse(ast, visitor);
Path
对象的 findParent
接收一个回调函数,在向上遍历每一个 父级 Path
时,会调用该回调函数,并传入对应的 父级 Path
,当该回调函数返回真值时,则将对应的 父级 Path
返回。上述代码会遍历 ReturnStatement
,然后向上找 父级 Path
看,当找到 Path
对象类型为 ObjectExpression
时,就返回该 Path
对象。
path.find
这个方法使用场景比较少,使用方法与 findParent
一致,只不过 find
方法查找的范围包含当前节点,而 findParent
不包含。
js
const visitor = {
ObjectExpression(path) {
const findPath = path.find((p) => p.isObjectExpression());
console.log(findPath);
}
}
traverse(ast, visitor);
path.getFunctionParent
path.getFunctionParent
方法是向上查找与当前节点最接近的 父函数
,返回的也是 Path
对象。
path.getStatementParent
path.getStatementParent
方法是向上遍历语法树直到找到语句父节点,例如,声明语句、return 语句、if 语句、switch 语句和 while 语句,返回的也是 Path
对象。该方法从当前节点开始找,如果想要找到 return 语句的父语句,就需要从 parentPath
中去调用,代码如下:
js
const visitor = {
ReturnStatement(path) {
const findPath = path.parentPath.getStatementParent();
console.log(findPath.toString());
}
}
traverse(ast, visitor);
父级 Path 的其他方法
其他方法的使用与之前介绍的 Path
方法类似,如替换父节点 path.parentPath.replaceWith(Node)
和 删除父节点 path.parentPath.remove
等
同级 Path 中的方法
path.inList
path.key
path.container
path.listKey
path.getSibling(index)
path.getPrevSibling()
path.getNextSibling()
unshiftContainer()
pushContainer()
在介绍 同级 Path
之前,需要先介绍下容器(container),先看下面的例子:
js
traverse(ast, {
ReturnStatement(path){
consloe.log(path);
}
})
/*
NodePath{
parent: Node {...},
...
parentPath: NodePath {...},
...
container: [
Node {
type: 'ReturnStatement',
...
argument: [Node]
}
],
listKey: 'body',
key: 0,
node: Node {...},
scope: Scope {...},
type: 'ReturnStatement'
}
*/
上述代码遍历 ReturnStatemnt
节点,并直接输出 Path
对象,之前介绍的 AST 结构,ReturnStatement
是放在 BlockStatement
中的 body
节点中的,并且该 body
节点是一个数组,输出的 Path
对象中的几个关键属性里,container
就是容器,在这个例子中它是一个数组,里面只有一个 ReturnStatement
节点,与原始代码吻合。 listKey
是容器名,ReturnStatement
是放在 BlockStatement
的 body
节点中的,因为把 body
节点看作一个容器。
接下来介绍 key
,在上述代码输出中的 Path
对象中,可以看到有一个 key
属性,这个 key
就是之前介绍的 path.get
方法的参数。实际上它就是容器对象的属性名,或者说是容器数组的索引。 这里的容器是一个数组,key
代表当前节点在容器中的位置。
并非只有 body 节点才是容器
。再来看下面的例子:
js
traverse(ast, {
ObjectExpression(path) {
console.log(path);
}
})
/*
NodePath {
parent: Node {...},
...
parentPath: Node {...},
container: [
Node {
type: 'ObjectProperty',
...
method: false,
key: [Node],
computed: false,
shorthand: false,
value: [Node]
},
Node {...},
Node {...}
],
listKey: 'properties',
key: 0,
node: Node {...},
scope: Scope {...},
type: 'ObjectProperty'
}
*/
上述代码遍历 ObjectProperty
节点,然后直接输出 Path
对象, ObjectProperty
是在 ObjectExpression
的 properties
属性中的,查看 Path
对象中的几个关键属性,container
是容器,listKey
是容器名。在原始代码中,有三个 ObjectProperty
,对应容器中的三个 Node
对象。 key
为 0,表示当前节点是容器中索引为 0 的成员,也就是说容器中的节点互为兄弟(同级)节点。
container并非一直都是数组
。例如下面:
js
traverse(ast, {
ObjectExpression(path) {
console.log(path);
}
})
traverse(ast, visitor);
/*
NodePath {
parent: Node {...},
...
parentPath: Node {...},
...
container: Node {
type: 'VariableDeclarator',
...
id: Node {
type: 'Identifier',
...
name: 'obj'
},
init: Node {
type: 'ObjectExpression',
...
properties: [Array],
extra: [Object]
}
},
listKey: undefined,
key: 'init',
node: Node {...},
scope: Scope {...},
type: 'ObjectExpression'
}
*/
上述代码中,container
是一个 Node
对象,listKey
为 undefined,其实可以说它没有容器,也就没有兄弟(同级)节点。在原始代码解析后的 AST 结构中,ObjectExpression
是 VariableDeclarator
的初始化值(init 节点)。此时 key
不是数组下标,而是对象的属性名。
了解了容器之后,接着介绍 同级 Path
相关的属性和方法。一般 container
为数组时就有同级节点,以下内容只考虑 container
为数组的情况,只有这种情况才有意义。示例代码如下:
js
traverse(ast, {
ReturnStatement(path) {
console.log(path.inList); // true
console.log(path.container); // [node {type: 'ReturnStatement' ...}]
console.log(path.listKey); // body
console.log(path.key); // 0
console.log(path.getSibling(path.key)); // node {type: 'ReturnStatement' ...}
console.log(path.getPrevSibling()); // Array[0] 获取上一个兄弟节点
console.log(path.getPrevSibling()); // Array[0] 获取下一个兄弟节点
}
})
path.inList
用于判断是否有同级节点。注意,当 container
为数组,但只有一个成员时,会返回 true
。
path.key
获取当前节点在容器中的索引。
path.container
获取容器(包含所有同级节点的数组)
path.listKey
获取容器名
path.getSibling(index)
用于获取 同级 Path
,其中参数 index 为容器数组中的索引,index 通过 path.key
来获取,可以对 path.key
进行加减操作来定位到不同的 同级 Path
。
path.getPrevSibling
用来获取当前节点的上一个兄弟节点。
path.getNextSibling()
用来获取当前节点的下一个兄弟节点。
unshiftContainer 与 pushContainer
unshiftContainer
是往容器最前面添加节点,pushContainer
是往容器最后面添加节点。示例如下:
js
traverse(ast, {
ReturnStatement(path) {
path.parentPath.unshiftContainer('body', [
types.expressionStatement(types.stringLiteral('Before1')),
types.expressionStatement(types.stringLiteral('Before2')),
]);
path.parentPath.pushContainer('body', [
types.expressionStatement(types.stringLiteral('After1')),
types.expressionStatement(types.stringLiteral('After2')),
]);
}
})
/*
let obj = {
name: "mankvis",
add: function (a, b) {
"Before1";
"Before2";
return a + b;
"After1";
"After2";
},
mul: function (a, b) {
"Before1";
"Before2";
return a + b;
"After1";
"After2";
}
};
*/
unshiftContainer
和 pushContainer
在 ts 文件中的定义如下:
js
pushContainer<Nodes extends Node | Node []>(listKey: ArrayKeys<T>, nodes: Nodes): NodePaths<Nodes>;
unshiftContainer<Nodes extends Node | Node []>(listKey: ArrayKeys<T>, nodes: Nodes): NodePaths<Nodes>;
可以看出,第一个参数给 listKey
,第二个参数给 Nodes
。 Nodes
是 Nodes extends Node | Node[]
,因此可以给 Node
或者 Node数组
,最后函数返回加入的 Nodes
的 Path
对象。