path 记录了遍历路径,并且还实现了一系列增删改的 api,会在遍历 ast 的时候传递给 visitor 的回调函数。
这节我们来实现下 path。
思路分析
path 是节点之间的关联,每一个 path 记录了当前节点和父节点,并且 path 和 path 之间也有关联。
通过 path 我们可以找到父节点、父节点的父节点,一直到根节点。
path 的实现就是在 traverse 的时候创建一个对象来保存当前节点和父节点,并且能够拿到节点也就能对节点进行操作,可以基于节点来提供一系列增删改的 api。
代码实现
首先我们创建一个 path 的类,记录当前节点 node,父节点 parent 以及父节点的 path。
class NodePath {
constructor(node, parent, parentPath) {
this.node = node;
this.parent = parent;
this.parentPath = parentPath;
}
}
然后在遍历的时候创建 path 对象,传入 visitor。
function traverse(node, visitors, parent, parentPath) {
const defination = astDefinationsMap.get(node.type);
let visitorFuncs = visitors[node.type] || {};
if(typeof visitorFuncs === 'function') {
visitorFuncs = {
enter: visitorFuncs
}
}
const path = new NodePath(node, parent, parentPath);
visitorFuncs.enter && visitorFuncs.enter(path);
if (defination.visitor) {
defination.visitor.forEach(key => {
const prop = node[key];
if (Array.isArray(prop)) { // 如果该属性是数组
prop.forEach(childNode => {
traverse(childNode, visitors, node, path);// 改动
})
} else {
traverse(prop, visitors, node, path);// 改动
}
})
}
visitorFuncs.exit && visitorFuncs.exit(path);
}
之后 visitor 里面就可以拿到 path 了。
比如我们可以在 visotor 里从当前节点一直查找到根节点:
traverse(ast, {
Identifier: {
exit(path) {
path.node.name = 'b';
let curPath = path;
while (curPath) {
console.log(curPath.node.type);
curPath = curPath.parentPath;
}
}
}
});
接下来是实现 api,path 的 api 就是对 AST 的增删改,我们实现下 replaceWith、remove、findParent、find、traverse、skip 这些 api。
实现 path api
replaceWith 就是在父节点替换当前节点为另一个节点。但是我们现在并不知道当前节点在父节点的什么属性上,所以在遍历的时候要记录属性名的信息。
这里要记录两个属性 key 和 listkey,比如如果属性是数组的话就要记录 key 是啥属性、listkey 是啥下标。
比如 params 下的 Identifier 节点,key 是 params,listkey 是 1、2、3。
如果不是数组的话,listkey 为空。
在讲 path 的那一节,我们讲过 key 和 listkey,很多同学都不明白为什么要记录这个,现在就知道了,是为了实现对 AST 增删改的 api 用的。
我们对 traverse 的实现做下改动,传入 key 和数组下标(有改动标识的那两行):
module.exports = function traverse(node, visitors, parent, parentPath, key, listKey) {
const defination = visitorKeys.get(node.type);
let visitorFuncs = visitors[node.type] || {};
if(typeof visitorFuncs === 'function') {
visitorFuncs = {
enter: visitorFuncs
}
}
const path = new NodePath(node, parent, parentPath, key, listKey);
visitorFuncs.enter && visitorFuncs.enter(path);
if (defination.visitor) {
defination.visitor.forEach(key => {
const prop = node[key];
if (Array.isArray(prop)) { // 如果该属性是数组
prop.forEach((childNode, index) => {
traverse(childNode, visitors, node, path, key, index);// 改动
})
} else {
traverse(prop, visitors, node, path, key);// 改动
}
})
}
visitorFuncs.exit && visitorFuncs.exit(path);
}
path 也要做相应的改动,加上 key 和 listkey:
class NodePath {
constructor(node, parent, parentPath, key, listKey) {
this.node = node;
this.parent = parent;
this.parentPath = parentPath;
this.key = key;
this.listKey = listKey;
}
}
然后基于 key 和 listkey 实现 replaceWith 的 api:
path.replaceWith
replaceWith 是替换节点,如果是数组的话,就替换 key 属性的 listkey 个元素的节点,用数组的 splice 方法。
不是数组的话,那就直接替换改 key 属性对应的节点。
replaceWith(node) {
if (this.listKey != undefined) {
this.parent[this.key].splice(this.listKey, 1, node);
} else {
this.parent[this.key] = node
}
}
path.remove
同理,remove 也是一样的思路:
remove () {
if (this.listKey != undefined) {
this.parent[this.key].splice(this.listKey, 1);
} else {
this.parent[this.key] = null;
}
}
path.find、path.findParent
find 和 findParent 是顺着 path 链向上查找 AST,并且把节点传入回调函数,如果找到了就返回节点的 path。区别是 find 包含当前节点,findParent 不包含。
findParent(callback) {
let curPath = this.parentPath;
while (curPath && !callback(curPath)) {
curPath = curPath.parentPath;
}
return curPath;
}
find(callback) {
let curPath = this;
while (curPath && !callback(curPath)) {
curPath = curPath.parentPath;
}
return curPath;
}
path.traverse
traverse 的 api 是基于上面实现的 traverse,但是有一点不同,path.traverse 不需要再遍历当前节点,直接遍历子节点即可。
traverse(visitors) {
const traverse = require('../index');
const defination = types.visitorKeys.get(this.node.type);
if (defination.visitor) {
defination.visitor.forEach(key => {
const prop = this.node[key];
if (Array.isArray(prop)) { // 如果该属性是数组
prop.forEach((childNode, index) => {
traverse(childNode, visitors, this.node, this);
})
} else {
traverse(prop, visitors, this.node, this);
}
})
}
}
path.skip
skip 的实现可以给节点加个标记,遍历的过程中如果发现了这个标记就跳过子节点遍历。
skip() {
this.node.__shouldSkip = true;
}
module.exports = function traverse(node, visitors, parent, parentPath, key, listKey) {
const defination = visitorKeys.get(node.type);
let visitorFuncs = visitors[node.type] || {};
if(typeof visitorFuncs === 'function') {
visitorFuncs = {
enter: visitorFuncs
}
}
const path = new NodePath(node, parent, parentPath, key, listKey);
visitorFuncs.enter && visitorFuncs.enter(path);
if(node.__shouldSkip) {
delete node.__shouldSkip;
return;
}
if (defination.visitor) {
defination.visitor.forEach(key => {
const prop = node[key];
if (Array.isArray(prop)) { // 如果该属性是数组
prop.forEach((childNode, index) => {
traverse(childNode, visitors, node, path, key, index);
})
} else {
traverse(prop, visitors, node, path, key);
}
})
}
visitorFuncs.exit && visitorFuncs.exit(path);
}
path.toString
toString 是把当前节点打印成目标代码,会调用 generator,generator 的实现在后面的章节会讲。
toString() {
return generate(this.node).code;
}
path.isXxx
我们记录了不同 ast 怎么遍历,那么也可以基于这些数据实现各种判断 AST 类型的 api:
const validations = {};
for (let name of astDefinationsMap.keys()) {
validations['is' + name] = function (node) {
return node.type === name;
}
}
这些会抽离到 types 包里面,然后在 path 中做相应的封装,通过 bind 给方法添加一个参数。
const types = require('../../types');
class NodePath {
constructor(node, parent, parentPath, key, listKey) {
this.node = node;
this.parent = parent;
this.parentPath = parentPath;
this.key = key;
this.listKey = listKey;
Object.keys(types).forEach(key => {
if (key.startsWith('is')) {
this[key] = types[key].bind(this, node);
}
})
}
}
实现了这些 API 之后我们就可以在 visitor 里使用 path 的 api 来操作 ast 了。
traverse(ast, {
Identifier(path) {
if(path.findParent(p => p.isCallExpression())) {
path.replaceWith({ type: 'Identifier', name: 'bbbbbbb' });
}
}
})
总结
path 的 api 就是对 AST 进行增删改,我们记录了 node(当前节点)、parent(父节点)、parentPath(父 path) 等信息,还会记录 key(父节点属性) 和 listkey(节点在数组中的下标)。基于这些就可以实现 replaceWith、remove、find、findParent、skip 等 api。
(代码在这里,建议 git clone 下来通过 node 跑一下)