出处 @蔡老板 的文章
第1篇:简单介绍path和node
关于path的一些操作
path,翻译过来是路径,理解为路径就好了。比如有这么一段代码:
var a = 123;
在线网站解析的情况如下:
小技巧:鼠标点击 var 这三个字母中间,可以看到网站直接将相关代码标黄了。
可以看到,上面就是一个个的节点,这也是我们再熟悉不过的JSON结构的数据。
上图中红色方框处就是它的path类型。我们在遍历的时候就这样写插件:
const visitor = {
VariableDeclaration(path)
{
//to do something;
},
}
小疑问: VariableDeclaration 和 VariableDeclarator 有啥区别?
可以看到,VariableDeclaration 是 VariableDeclarator 的祖先节点,这两者肯定不一样的。我们将代码变成下面这样,再进行解析:
Declaration:声明
Declarator:声明人
var a = 123,b = 456;
解析的结果如下:
这样就明白了。原来 一个 VariableDeclarator 只对应一个 变量的定义,理解这些,在我们插入节点的时候非常的有帮忙,比如我们想要在
var a = 123;
代码后面插入
b = 456;
你就知道应该遍历 VariableDeclarator 路径,判断变量名是否为 “a”,然后构造节点插入即可。
我们注意到它的参数是:path,那path究竟有那些常见的方法呢?
- 当前路径所对应的源代码,使用的是toString这个方法,在我解混淆的时候会经常的用到,是比较重要的,示例代码如下:
const visitor = {
VariableDeclaration(path)
{
console.log(path.toString());
},
}
- 判断path是什么type,使用path.isXXX 这个方法,比如我们需要判断路径是否为 StringLiteral 类型:
if(path.isStringLiteral())
{
//do something;
}
- 获取path的上一级路径,你可以像这样来获取:
let parent = path.parentPath;
- 获取path的子路径,你可以是使用 get方法,比如定义了如下变量:
var a = 123;
当前访问的是 VariableDeclarator ,其结构如下:
现在需要访问 id 这个路径,你可以这么操作:
path.get('id');
- 删除path,使用remove方法,当你觉得你访问的路径已经完成了改完成的事,对代码已经没什么作用了,可以将其删除
path.remove()
- 替换path,单路径可以使用replaceWith方法,多路径则使用replaceWithMultiple方法,比如你想将 1 + 2 替换成 3,你可以像下面这样的代码进行替换:
path.replaceWith({type:"NumericLiteral",value:3});
当然,你也可以引入 const type = require(“@babel/types”);然后再进行替换:
path.replaceWith(type.NumericLiteral(3));
注意,这里的3是我口算出来的,在实际操作过程中,是需要获取 “+” 两边的值并计算其结果的。
如果你想获取更多path相关的方法,请直接阅读源码,阅读源码是你学习最快,也是最好的办法。path相关的源码在这里:
@babel\traverse\lib\path
关于node的一些操作
这里的node,其实是path的一个属性:
const node = path.node;
可以将其理解为节点,我们可以打印node,看看是些什么内容。这里还是以代码var a = 123;为例,访问 VariableDeclarator 路径,打印出当前的node:
console.log(path.node);
结果如图:
其实就是一个 json结构的数据。
- 如何获取当前节点所对应的源代码:
const generator = require("@babel/generator").default;
let {code} = generator(node);
- 如何删除节点,使用系统的 delete 方法。比如你想删除
init这个节点,可以使用下面的方式:
delete path.node.init;
操作过后,代码 var a = 123;将会变成 var a;,即将初始化的值进行删除。
当然也可以直接赋值,比如:
path.node.init = undefined;
- 访问子节点,比如你想得到初始化的值,你可以这么操作:
const node = path.node;
const value = node.init.value;
console.log(value);
嗯,你把它理解为 JSON就好了,比如,它是可以这么操作的:
const node = path.node;
console.log(JSON.stringify(node));
第2篇:babel库中path常用属性总结
path常用属性总结:
path相关的源代码在这个js文件中,大家可以直接照着源码学习:
\node_modules\@babel\traverse\lib\path
在本文中,选出部分常用的属性大家参考,更多的知识请自行学习源码。
path的属性定义:
class NodePath {
constructor(hub, parent) {
this.contexts = [];
this.state = null;
this.opts = null;
this._traverseFlags = 0;
this.skipKeys = null;
this.parentPath = null;
this.container = null;
this.listKey = null;
this.key = null;
this.node = null;
this.type = null;
this.parent = parent;
this.hub = hub;
this.data = null;
this.context = null;
this.scope = null;
}
......
}
path.node
表示当前path下的node节点,通常我写插件,函数体的第一行代码就是:
let {node,scope} = path;
path.scope
表示当前path下的作用域,这个也是写插件经常会用到的。
path.parentPath
用于获取当前path下的父path,多用于判断节点类型。
path.parent
用于获取当前path下的父node,多用于判断节点类型。其中:
path.parent == path.parentPath.node;//这两者是等价的
path.container
用于获取当前path下的所有兄弟节点(包括自身),container翻译过来是容器的意思,它是一个Array类型,可以写个简单的插件来看看效果:
let jscode = "var a = 1,b = 2;var c = 3;";
const getcontainer =
{
VariableDeclarator(path)
{
console.log(path.container.length);
}
}
traverse(ast,getcontainer);
输出结果:
代码中a,b互为兄弟节点,c只有一个节点,所以结果显示2,2,1。
这里要注意,是获取所有的所有兄弟节点(包括自身),不是path类型。
path.type
用于获取当前path的节点类型,写个简单的插件来看看效果 :
let jscode = "var a = 1 + 2;";
const getType =
{
"VariableDeclarator|BinaryExpression|Identifier"(path)
{
console.log(path.type);
console.log(path.toString());
}
}
traverse(ast,getType);
输出结果:
path.key
用于获取当前path的key值,key通常用于path.get函数,当然还有更多的用法等待大家去挖掘 ,写个简单的插件来看看效果 :
let jscode = "var a = 1 + 2;";
const getPathKey =
{
"VariableDeclarator|BinaryExpression|Identifier"(path)
{
console.log(path.key);
console.log(path.toString());
}
}
traverse(ast,getPathKey);
输出结果:
输出结果可能大家看不懂,我在这里解释下。
第一个遍历的 path 是 VariableDeclarator 类型,对应源代码 是 a = 1+2 ;
它的父节点是 VariableDeclaration 类型,而它处在 declarations 的第0的位置,因此输出是 0;
第二个遍历的 path 是 Identifier类型,对应源代码 是 a ;它是 id 节点,因此它的输出是 id;
第三个遍历的 path 是 BinaryExpression 类型,对应源代码 是 1+2 ;它是 init 节点,因此它的输出是 init;
还有其他的属性,因为不常用,因此不再列举出来。大家可以自行看源码了解其功能。
第3篇:babel库中path常用方法总结
path源码学习:
path相关的源代码在这个js文件中,大家可以直接照着源码学习:
\node_modules\@babel\traverse\lib\path
当然,第一个需要学习的文件应该是这个:
node_modules\@babel\traverse\lib\path\index.js
在本文中,选出部分常用的方法大家参考,更多的知识请自行学习源码。
path常用方法介绍
path.toString
用来获取当前遍历path的js源代码,调用方式:
let sourceCode = path.toString();
这个非常有用,经常用于定于插件遍历的path,以便分析问题。如果想通过 path.node来获取源代码,可以使用 generator 函数来获取:
let sourceCode = generator(path.node).code;
path.replaceWith
(单)节点替换函数,调用方式:
path.replaceWith(newNode);
实参一般是 node 类型,即将当前遍历的path替换为 实参里的 新节点。
注意,它不能用于 Array 的替换,即实参不能是Array类型。
path.replaceWithMultiple
(多)节点替换函数,调用方式:
path.rreplaceWithMultiple(ArrayNode);
实参一般是 Array 类型,它只能用于 Array 的替换。即所有需要替换的节点在一个Array里面,常见的替换如 Block 类型节点里的 body.body。
path.remove
节点的删除,调用方式:
path.remove();
直接调用即可将当前遍历的所有符合条件的路径全部删除,所以使用的时候需要注意。
path.insertBefore
在当前节点前面插入新的节点。调用方式:
path.insertBefore(newNode);
path.insertAfter
在当前节点后面插入新的节点。调用方式:
path.insertAfter(newNode);
path.traverse
在当前节点下遍历其他的节点,比如有如下for循环:
for(;;)
{
a = b;
b = c;
c =d;
}
想要遍历这个for循环下面的 赋值语句,可以借助该函数进行遍历:
const visitFor =
{
ForStatement(path)
{
......
path.traverse({
AssignmentExpression(_path)
{
......
},
}),
......
},
}
path.get
获取当前路径下的子孙节点。调用方式:
let newPath = path.get(key)
形参key是一个字符串,以 . 隔开。 其源代码:
function get(key, context) {
if (context === true) context = this.context;
const parts = key.split(".");
if (parts.length === 1) {
return this._getKey(key, context);
} else {
return this._getPattern(parts, context);
}
}
假如当前的代码如下:
var a = 1;
你遍历了 VariableDeclaration 类型,想要获取 右边的这个 1,可以写如下代码:
const visitVar =
{
VariableDeclaration(path)
{
let initPath = path.get('declarations.0.init');
......
},
}
path.getSibling
根据key值来获取节点,这个方法我很少用,大家可以试试。
path.getPrevSibling
获取当前path的前一个兄弟节点,返回的是path类型。
path.getAllPrevSiblings
获取当前path的所有前兄弟节点,返回的是Array类型,其元素都是path类型。
path.getNextSibling
获取当前path的后一个兄弟节点,返回的是path类型。
path.getAllNextSiblings
获取当前path的所有后兄弟节点,返回的是Array类型,其元素都是path类型。
path.evaluate
用于计算表达式的值,大家可以参考 constantFold 插件的写法。
//常量折叠插件更新
const constantFold = {
"BinaryExpression|UnaryExpression"(path) {
if(path.isUnaryExpression({operator:"-"}) ||
path.isUnaryExpression({operator:"void"}))
{
return;
}
const {confident, value} = path.evaluate();
if (!confident)
return;
if (typeof value == 'number' && (!Number.isFinite(value))) {
return;
}
path.replaceWith(types.valueToNode(value));
},
}
traverse(ast, constantFold);
path.findParent
向上查找满足回调函数特征的path,即判断上级路径是否包含有XXX类型的节点:
function findParent(callback) {
let path = this;
while (path = path.parentPath) {
if (callback(path)) return path;
}
return null;
}
很简单,先从父级节点开始进行遍历,然后一直循环,直到满足回调函数的条件为止。如果找不到则返回null。回调函数怎么写,可以参考下面的代码:
function getFunctionParent() {
return this.findParent(p => p.isFunction());
}
path.find
功能与 path.findParent 方法一样,只不过从当前path开始进行遍历。
path.getFunctionParent
获取函数类型父节点,如果不存在,返回 null。
path.getStatementParent
获取Statement类型父节点,这个基本上都会有返回值,如果当前遍历的是 Program 或者 File 节点,则会报错。
path.getAncestry
获取所有的祖先节点,没有实参,返回的是一个Array对象。源码也很简单:
function getAncestry() {
let path = this;
const paths = [];
do {
paths.push(path);
} while (path = path.parentPath);
return paths;
}
path.isAncestor
判断当前遍历的节点是否为实参的祖先节点,调用方式:
path.isAncestor(maybeDescendant)
实参传入 path 类型即可。
path.isDescendant
判断当前遍历的节点是否为实参的子孙节点,调用方式:
path.isDescendant(maybeAncestor)
实参传入 path 类型即可。
第4篇:Scope和Binding常用方法及属性总结
scope常用方法及属性总结:
scope相关的源代码在这个js文件中,大家可以直接照着源码学习:
node_modules\@babel\traverse\lib\scope\index.js
scope的属性定义:
class Scope {
constructor(path) {
this.uid = void 0;
this.path = void 0;
this.block = void 0;
this.labels = void 0;
this.inited = void 0;
this.bindings = void 0;
this.references = void 0;
this.globals = void 0;
this.uids = void 0;
this.data = void 0;
this.crawling = void 0;
const {
node
} = path;
const cached = _cache.scope.get(node);
if ((cached == null ? void 0 : cached.path) === path) {
return cached;
}
_cache.scope.set(node, this);
this.uid = uid++;
this.block = node;
this.path = path;
this.labels = new Map();
this.inited = false;
}
......
}
在本文中,选出部分常用的属性和方法供大家参考,更多的知识请自行学习源码。
scope.block
表示当前作用域下的所有node,参考上面的 this.block = node;
scope.dump()
输出当前每个变量的作用域信息。调用后直接打印,不需要加打印函数
scope.crawl()
重构scope,在某种情况下会报错,不过还是建议在每一个插件的最后一行加上。
scope.rename(oldName, newName, block)
修改当前作用域下的的指定的变量名,oldname、newname表示替换前后的变量名,为字符串。注意,oldName需要有binding,否则无法重命名。
scope.traverse(node, opts, state)
遍历当前作用域下的某些(个)插件。和全局的traverse用法一样。
scope.getBinding(name)
获取某个变量的binding,可以理解为其生命周期。包含引用,修改之类的信息
binding常用方法及属性总结:
binding相关的源代码在这个js文件中,大家可以直接照着源码学习:
node_modules\@babel\traverse\lib\scope\binding.js
binding的属性定义:
class Binding {
constructor({
identifier,
scope,
path,
kind
}) {
this.identifier = void 0;
this.scope = void 0;
this.path = void 0;
this.kind = void 0;
this.constantViolations = [];
this.constant = true;
this.referencePaths = [];
this.referenced = false;
this.references = 0;
this.identifier = identifier;
this.scope = scope;
this.path = path;
this.kind = kind;
this.clearValue();
}
......
}
目前我看到的只有 变量定义 和 函数定义 拥有binding,其他的获取binding都是undefined。
let binding = scope.getBinding(name);
例如:
var a = 123; 这里的 a 就拥有 binding。
而 function test(a,b,c) {};
函数名test以及形参a,b,c均拥有 binding。
binding.path
用于定位初始拥有binding的path;
binding.constant
用于判断当前变量是否被更改,true表示未改变,false表示有更改变量值。
binding.referenced
用于判断当前变量是否被引用,true表示代码下面有引用该变量的地方,false表示没有地方引用该变量。注意,引用和改变是分开的。
binding.referencePaths
它是一个Array类型,包含所有引用的path,多用于替换。
binding.constantViolations
它是一个Array类型,包含所有改变的path,多用于判断。
你可以参考下面的这篇文章,学习更多scope相关的基础知识,感谢群友的总结: