介绍

AST(AbstractSyntaxTree,抽象语法树)是一种树状结构,用于表示代码的语法结构.它是源代码在编译或解释执行之前的中间表示,能够捕获代码的语法规则和层级关系.

在AST中,代码的每个语法元素(如变量、函数、运算符等)都会被解析成一个节点,节点之间的层次结构表现出代码的嵌套关系.

demo

1
2
3
4
5
6
7
8
9
let obj = {
name:'John',
add:function(a,b){
return a+b+1000;
},
mul:function(a,b){
return a*b;
},
};

解析成语法树如下https://astexplorer.net/

可以换成json来看

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
{
"type": "Program",
"start": 0,
"end": 118,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 118,
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 117,
"id": {
"type": "Identifier",
"start": 4,
"end": 7,
"name": "obj"
},
"init": {
"type": "ObjectExpression",
"start": 10,
"end": 117,
"properties": [
{
"type": "Property",
"start": 14,
"end": 25,
"method": false,
"shorthand": false,
"computed": false,
"key": {
"type": "Identifier",
"start": 14,
"end": 18,
"name": "name"
},
"value": {
"type": "Literal",
"start": 19,
"end": 25,
"value": "John",
"raw": "'John'"
},
"kind": "init"
},
{
"type": "Property",
"start": 29,
"end": 72,
"method": false,
"shorthand": false,
"computed": false,
"key": {
"type": "Identifier",
"start": 29,
"end": 32,
"name": "add"
},
"value": {
"type": "FunctionExpression",
"start": 33,
"end": 72,
"id": null,
"expression": false,
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"start": 42,
"end": 43,
"name": "a"
},
{
"type": "Identifier",
"start": 44,
"end": 45,
"name": "b"
}
],
"body": {
"type": "BlockStatement",
"start": 46,
"end": 72,
"body": [
{
"type": "ReturnStatement",
"start": 52,
"end": 68,
"argument": {
"type": "BinaryExpression",
"start": 59,
"end": 67,
"left": {
"type": "BinaryExpression",
"start": 59,
"end": 62,
"left": {
"type": "Identifier",
"start": 59,
"end": 60,
"name": "a"
},
"operator": "+",
"right": {
"type": "Identifier",
"start": 61,
"end": 62,
"name": "b"
}
},
"operator": "+",
"right": {
"type": "Literal",
"start": 63,
"end": 67,
"value": 1000,
"raw": "1000"
}
}
}
]
}
},
"kind": "init"
},
{
"type": "Property",
"start": 76,
"end": 114,
"method": false,
"shorthand": false,
"computed": false,
"key": {
"type": "Identifier",
"start": 76,
"end": 79,
"name": "mul"
},
"value": {
"type": "FunctionExpression",
"start": 80,
"end": 114,
"id": null,
"expression": false,
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"start": 89,
"end": 90,
"name": "a"
},
{
"type": "Identifier",
"start": 91,
"end": 92,
"name": "b"
}
],
"body": {
"type": "BlockStatement",
"start": 93,
"end": 114,
"body": [
{
"type": "ReturnStatement",
"start": 99,
"end": 110,
"argument": {
"type": "BinaryExpression",
"start": 106,
"end": 109,
"left": {
"type": "Identifier",
"start": 106,
"end": 107,
"name": "a"
},
"operator": "*",
"right": {
"type": "Identifier",
"start": 108,
"end": 109,
"name": "b"
}
}
}
]
}
},
"kind": "init"
}
]
}
}
],
"kind": "let"
}
],
"sourceType": "module"
}

program属性的body属性是一个数组,该数组存放js代码的代码行

VariableDeclaration是变量声明,意思是这段js代码是变量声明语句,kind意思是变量声明使用的关键字,这里是let

这里的declarations是个数组,数组元素的个数和声明变量的个数一致

关注declarations下的idinitid里面的name属性是变量名,init内是变量初始化的值,initproperties是键值对,是属性名和属性值

关注a+b+1000在AST中怎么表示的,按右边的+分隔,分成a+b1000,再把a+b+分隔,分成ab

babel库可以对AST语法进行解析,后面都是用babel库进行操作的

代码的基本结构

demo

把原始代码保存成一个文件,名为demo.js,注意保存成utf-8编码的,另外新建一个文件,用来解析demo.js

这里的demo.js就是

1
2
3
4
5
6
7
8
9
let obj = {
name:'John',
add:function(a,b){
return a+b+1000;
},
mul:function(a,b){
return a*b;
},
};

安装babel库

1
npm install @babel/core

AST.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const fs = require('fs');
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

const jscode = fs.readFileSync("./demo.js", {
encoding: "utf-8"
});
let ast = parser.parse(jscode);

//在这里对AST进行一系列的操作

let code = generator(ast).code;
fs.writeFile('./demoNew.js', code, (err)=>{});

总的来说逻辑如下

1
2
3
4
5
6
(1)fs用来读写本地文件,require之后赋值给了fs.
(2)@babel/parser用来将JS代码转换成AST,require之后赋值给了parser.
(3)@babel/traverse用来遍历AST中的节点,require之后把其中的default赋值给了traverse.
(4)@babel/types判断节点类型和生成新的节点等,require之后赋值给了t.
(5)@babel/generator用来把AST转换成JS代码,require之后把其中的default赋值给generator.
从上述代码中,可以看出AST处理JS文件的基本步骤,读取JS文件-->解析成AST-->对节点进行一系列的增删改査-->生成JS代码->保存到新文件中.本书后续的内容中,如果没有特别说明,就以这个结构为准,代码只贴出中间对AST有操作的部分

parser和generator

这两个组件的作用刚好是相反的.parser组件用来将JS代码转换成AST,generator用来将AST转换成JS代码.

使用letast=parser.parse(jscode);即可完成JS代码转换到AST的过程,这时候把ast输出来,就是跟网页中解析出来的一样的结构,输出前通常先使用JSON.stringify把对象转json数据,例如,console.log(JSON.stringify(ast,null,2)).另外,parserparse方法,其实是有第二个参数的

1
2
3
letast=parser.parse(jscode,{
sourceType:"module"
});

sourceType默认是script,但是当解析的JS代码中,含有importexport等关键字的时候,需指定sourceTypemodule.不然会报错

letcode=generator(ast).code;,这里的generator其实也有其他参数

1
2
3
4
5
6
7
8
9
//retainLines表示是否使用与源代码相同的行号,默认true
//comments表示是否保留注释,默认true
//compact表示是否压缩代码,默认false
let code=generator(ast,{
retainLines:false
comments:false
compact:true
}).code;
console.log(code);

traverse与visitor

demo

先看demo,需要把下面代码中的a变量改成x变量

1
2
3
4
5
6
7
8
9
let obj = {
name:'John',
add:function(a,b){
return a+b+1000;
},
mul:function(a,b){
return a*b;
},
};

把上面的代码拿到ast网站上解析一下,需要知道a变量在ast中长什么样,在ast语法树中,a变量是使用Identifier节点表示的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"params": [
{
"type": "Identifier",
"start": 42,
"end": 43,
"name": "a"
},
{
"type": "Identifier",
"start": 44,
"end": 45,
"name": "b"
}
],

traverse用来遍历语法树中所有的节点的,节点可以理解为数组中的成员,visitor则定义了遇到什么节点该做什么修改

1
2
3
4
5
6
7
8
letvisitor={};
visitor.Identifier=function(path){
//console.log(path.node.name);
if(path.node.name=='a'){
path.node.name='x';
}
}
traverse(ast,visitor);

traverse的第一个参数是ast语法树,visitor是一个自定义的对象,该对象可以自己定义一些方法,方法名和节点名一样,比如要处理的是Identifer节点,那么方法名就是Identifer,属性是一个函数,函数的参数是path对象,什么是path对象后面会说,通过path对象可以对节点进行修改

当然上面的代码是不严谨的,因为不可能只根据变量名去改,作用域怎么办?

两者的作用

traverse组件用来遍历AST,简单地说就是把AST上的各个节点都走一遍,但是单纯的把节点都走一遍是没有意义的,所以taverse需要配合visitor使用.
visitor是一个对象,里面可以定义一些方法,用来过滤节点.概念是抽象的,接下来用个实际案例试一下traversevisitor的效果:

1
2
3
4
5
letvisitor={};
visitor.FunctionExpression=function(path){
console.log("john");
};
traverse(ast,visitor);

先是声明对象,对象的名字可以随意,然后给对象增加了一个名为FunctionExprcssion的方法,这个名字是需要遍历的节点类型,注意大小写.

traverse会遍历所有的节点,当节点类型为FunctionExpression时,调用visitor中相应的方法,如果想要处理其他节点类型例如Identifier.可以在visitor中继续定义方法,以Identifer命名即可.

visitor中的方法接收一个参数,traverse在遍历的时候,会把当前节点的Path对象传给它,传过来的是Path对象而非节点(node)

最后把visitor作为第二个参数传到traverse里面,传给traverse的第一个参数是整个ast.这段代码的意思是,从头开始遍历ast中的所有节点,过滤出FunctionExpression节点,执行相应的方法,在原始代码中有两个FunctionExpression节点,因此会输出两次John.

visitor的三种定义方式

visitor的定义方式还有三种

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
constvisitor1={
FunctionExpression:functicn(path){
console.log("john");
}
}

constvisitor2={
FunctionExpression(path){
console.log("john");
}
};

constvisitor3={
FunctionExpression:{
enter(path}{
console.log("john"};
}
}
}

visitor3中,存在一个重要的enter,在遍历节点过程中,实际上有两次机会来访问一个节点,即进入节点时(enter)与退出节点时(exit),比如这样定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let visitor = {};
visitor.Identifier = {
enter: function (path) {
//console.log(path.node.name);
if (path.node.name == 'a') {
path.node.name = 'x';
}
console.log(path.node.name, 'xiaojianbang enter');
},
exit: function (path) {
console.log(path.node.name, 'xiaojianbang exit');
}
};
traverse(ast, visitor);

以原始代码中的add函数为例,节点的遍历过程可描述如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
进入FunctionExpression
进入 Identifier(params[0])走到尽头
退出 Identifier(params[0])
进入 Identifier(parens[1])走到尽头
退出 Identifier(params[1])
进入 BlockStatement(body)
进入 ReturnStatement(body)
进入 BinaryExpression(argument)
进入 BinaryExpression(left)
进入 Identifier(left)走到尽头
退出 Identifier(left)
进入 Identifier(rignt)走到尽头
退出 Identifier(rignt)
退出 BinaryExpression(left)
进入 NumericLiteral(right)走到尽头
退出 NumericLiteral(right)
退出 BinaryExpression(argument)
退出 ReturnStatement(body)
退出 BlockStatement(body)
退出FunctionExpression

可以看出来是深度优先的搜索,正确的选择节点处理时机,有助于提高代码效率,可以看出traverse是一个深度优先的遍历过程,因此,如果存在父子节点,那么enter的处理时机是先处理父节点,再处理子节点,与此相反,exit的处理时机是先处理子节点,再处理父节点,traverse 默认就是在 enter时候处理,如果要在exit时候处理,必须在visitor中写明.

还可以把同一个函数用在多个节点上

1
2
3
4
5
6
const visitor = {
"FunctionExpression|BinaryExpression"(path){
console.log(path.node.type, "xiaojianbang");
}
};
traverse(ast, visitor);

退出和进入的时候对同一个节点可以执行多个函数

1
2
3
4
5
6
7
8
9
10
11
function test1(path) {
console.log(path.node.name, 'xiaojianbang exit', 'test1');
}
function test2(path) {
console.log(path.node.name, 'xiaojianbang exit', 'test2');
}
let visitor = {};
visitor.Identifier = {
exit: [test1, test2]
};
traverse(ast, visitor);

现在还有一个问题,如果要修改变量的名有重名怎么办?我想修改指定作用域内的变量名,比如我只想修改函数内部的a变量

1
2
3
4
5
6
7
8
9
10
11
let a=1000;
let obj = {
a:200,
name:'John',
add:function(a,b){
return a+b+1000;
},
mul:function(a,b){
return a*b;
},
};

指定一下遍历的是FunctionExpression节点内的子节点,并且是Identifier子节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let visitor = {};
visitor.FunctionExpression = {
enter: function (path) {
let myVisitor = {
Identifier(path){
if (path.node.name === 'a') {
path.node.name = 'x';
}
}
};
path.traverse(myVisitor);
}
};
traverse(ast, visitor);

现在来解释一下上面的语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1)为什么enter里又定义了myVisitor?
原因:
enter只会在进入某个FunctionExpression作用域时触发。
但函数内部的Identifier(变量名)不会自动被FunctionExpression访问到,所以需要手动调用path.traverse(myVisitor)再次遍历函数体内部。
myVisitor 里定义了 Identifier(path) {},用于处理 函数内部的所有标识符(变量名)
如果不这样做,path.node遍历到的就是FunctionExpression,这个节点没有name属性

2Identifier(path){} 这个语法是什么?
Identifier(path) {
if (path.node.name === 'a') {
path.node.name = 'x';
}
}
这是 Babel AST 访问者模式(Visitor Pattern) 的一种写法,相当于:
{
Identifier: function(path) {
if (path.node.name === 'a') {
path.node.name = 'x';
}
}
}
Identifier 代表 访问 AST 里所有 Identifier 类型的节点(即所有变量名)。
path 是 Babel 提供的 路径对象(NodePath),用于操作 AST
path.node.name 就是变量名,如 a、b、name。

path.traverse还有第二个参数,是参数列表,比如现在想把代码里函数的第一个参数都修改为x,怎么办?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let visitor = {};
visitor.FunctionExpression = {
enter: function (path) {
let myVisitor = {
Identifier(path){
if (path.node.name === this.paramName) {
path.node.name = 'x';
}
console.log('Identifier', this.paramName);
}
};
let paramName = path.node.params[0].name;
path.traverse(myVisitor, {paramName});
}
};
traverse(ast, visitor);

现在对上面的代码进行解释

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
1)为什么不能直接用 path.node.params[0].name,而要提前存入 paramName 并通过 path.traverse(myVisitor, { paramName }) 传递
主要原因是:

path 在 traverse 内部会改变,指向不同的节点
FunctionExpression 的 enter 里:path 指向 函数本身的 AST 节点(FunctionExpression),path.node.params[0].name 获取的是函数的第一个参数名。
但是,在 traverse(myVisitor) 之后,path 变成了 Identifier 节点的路径(变量名的路径),path.node.name 代表当前正在访问的变量名,而不是 FunctionExpression 的参数列表。

举个例子,假设代码
function sum(a, b) {
return a + b + 100;
}

AST结构如下:
FunctionExpression
├── params: [Identifier(name="a"), Identifier(name="b")]
└── body
├── ReturnStatement
│ ├── BinaryExpression
│ │ ├── left: Identifier(name="a")
│ │ ├── right: BinaryExpression
│ │ ├── left: Identifier(name="b")
│ │ ├── right: NumericLiteral(100)

2)为什么不能用 path.node.params[0].name 直接比较?
let visitor = {};
visitor.FunctionExpression = {
enter: function (path) {
let myVisitor = {
Identifier(path) {
if (path.node.name === path.node.params[0].name) { // 错误
path.node.name = 'x';
}
}
};
path.traverse(myVisitor);
}
};
错误原因:
path.nodeIdentifier访问时,代表的是变量,而不是FunctionExpression
path.node.paramsIdentifier节点根本不存在,会导致Cannot read properties of undefined 错误。

正确做法
let visitor = {};
visitor.FunctionExpression = {
enter: function (path) {
let paramName = path.node.params[0].name; // 获取函数的第一个参数名
let myVisitor = {
Identifier(path) {
if (path.node.name === this.paramName) { // 通过 this 访问 paramName
path.node.name = 'x';
}
}
};
path.traverse(myVisitor, { paramName }); // 把 paramName 传进去
}
};
traverse(ast, visitor);

3)path.traverse(myVisitor, { paramName });是什么语法?
是简化的,其实是path.traverse(myVisitor, { paramName:paramName });

types组件

types判断节点类型

该组件主要用来判断节点类型,生成新的节点等,判断节点类型很简单,例如t.isldentifier(path.node),它等价于 path.node.type=="Identifier",还可以在判断类型的同时附加条件,演示案例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const t = require("@babel/types");
traverse(ast, {
enter(path, state) {

if (path.node.type === 'Identifier' && path.node.name === 'a') {
path.node.name = 'x';
}
// path.node, {name: 'a'}是判断当前节点的name属性是否为a
if(t.isIdentifier(path.node, {name: 'a'})){
path.node.name = 'x';
}
// 作用和上面的等价
if(path.isIdentifier({name: 'a'})){
path.node.name = 'x';
}
}
});

types生成新节点

直接上代码,很好理解,也就是对着AST语法树的节点看需要添加什么节点,节点的属性是什么,子节点是什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let var_a = types.identifier('a');
let vardoc = types.variableDeclarator(var_a, types.numericLiteral(100));
let myCode = types.variableDeclaration('let', [vardoc]);
console.log(generator(myCode).code);

let var_name = types.identifier('name');
let objpro = types.objectProperty(var_a, types.numericLiteral(200));
let objpro2 = types.objectProperty(var_name, types.stringLiteral('xiaojianbang'));

let var_add = types.identifier('add');
let var_b = types.identifier('b');
let binar = types.binaryExpression('+', var_a, var_b);
let binar2 = types.binaryExpression('+', binar, types.numericLiteral(1000));
let funcbody = types.blockStatement([types.returnStatement(binar2)]);
let funcexpr = types.functionExpression(null, [var_a, var_b], funcbody);
let objpro3 = types.objectProperty(var_add, funcexpr);

let objexp = types.objectExpression([objpro, objpro2, objpro3]);
let var_obj = types.identifier('obj');
let vardoc2 = types.variableDeclarator(var_obj, objexp);
let myCode2 = types.variableDeclaration('let', [vardoc2]);
console.log(generator(myCode2).code);

valueToNode方法也可以生成各种字面量,和上面的代码功能一样,但是不能生成FunctionExpression节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
console.log(types.valueToNode([1, "2", false, null, undefined, /\w\s/g, {x: '1000', y: 2000}]));
//返回如下
//{
// type: 'ArrayExpression',
// elements: [
// { type: 'NumericLiteral', value: 1 },
// { type: 'StringLiteral', value: '2' },
// { type: 'BooleanLiteral', value: false },
// { type: 'NullLiteral' },
// { type: 'Identifier', name: 'undefined' },
// { type: 'RegExpLiteral', pattern: '\\w\\s', flags: 'g' },
// { type: 'ObjectExpression', properties: [Array] }
// ]
//}
console.log(generator(types.valueToNode({
a: 200,
name: 'xiaojianbang',
})).code);
// 直接生成字面量,也就是对象
//{
// a: 200,
// name: "xiaojianbang"
//}

path对象

先看这段代码

1
2
3
4
5
6
7
8
9
let obj = {
name:'John',
add:function(a,b){
return a+b+1000;
},
mul:function(a,b){
return a*b;
},
};

直接输出path

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const fs = require('fs');
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;

const jscode = fs.readFileSync("./demo.js", {
encoding: "utf-8"
});
let ast = parser.parse(jscode);

//在这里对AST进行一系列的操作
traverse(ast, {
Identifier(path) {
console.log(path);
path.stop();
}
});
let code = generator(ast).code;
fs.writeFile('./demoNew.js', code, (err)=>{});

结果可以看到path的属性是NodePathNodePath下有node属性,从而得知node其实是path的一个属性

parentPath是当前节点的父节点的path对象

container,当container的值是一个数组的时候,代表当前节点是存在兄弟节点的

scope代表作用域

访问子节点属性

path对象提供了一系列api,可以方便的操作节点,还是以上面的demo为例,尝试访问BinaryExpression的子节点,就有两种方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
traverse(ast, {
BinaryExpression(path){
console.log(path.node.left);
console.log('=========================');
console.log(path.node.right);
console.log('=========================');
console.log(path.node.operator);
console.log('=========================');
path.stop();
}
})


traverse(ast, {
BinaryExpression(path){
console.log(path.get('left.left.name'));
console.log('=========================');
console.log(path.get('right'));
console.log('=========================');
console.log(path.get('operator'));
console.log('=========================');
path.stop();
}
})

语法转代码

1
2
3
4
console.log(generator(path.node).code);
console.log(path.toString());
console.log(path + '');
//console.log(path.node + ''); 这个不行

替换节点属性

比如要替换代码中的a和b为x和y

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
traverse(ast, {
BinaryExpression(path) {
path.node.left = types.identifier('x');
path.node.right = types.identifier('y');
path.stop();

let node = path.node;
node.left = types.identifier('x');
node.right = types.identifier('y');
path.stop();

// 但是下面这种不可以,left和right是局部变量,修改的是局部变量的属性
let left = path.node.left;
let right = path.node.right;
left = types.identifier('x');
right = types.identifier('y');
console.log(path.node.left);
console.log(path.node.right);
path.stop();
}
})

替换节点,比如要把上面的函数return改为返回字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
traverse(ast, {
BinaryExpression(path){
path.replaceWith(types.valueToNode('xiaojianbang'));
// 用replaceWithMultiple和replaceInline可以替换为返回多个字符串
path.replaceWithMultiple([
types.valueToNode('xiaojianbang'),
types.valueToNode('ruyi'),
types.valueToNode('ast'),
]);
path.replaceInline(types.valueToNode('xiaojianbang'));
path.replaceInline([
types.valueToNode('xiaojianbang'),
types.valueToNode('ruyi'),
types.valueToNode('ast'),
]);

}
})

替换代码、删除和插入节点

替换return语句为其他语句,使用replaceWithSourceString方法

1
2
3
4
5
6
7
8
9
10
11
12
13
traverse(ast, {
ReturnStatement(path) {
// let argu = path.get('argument');
// argu.replaceWithSourceString('function(){ return ' + argu + '}()');
// 这里如果不加path.skip()的话,会出现死循环,因为新增的语句是return,会被遍历到,然后遍历到之后又加return
// path.skip();
// path.remove();删除节点
path.insertBefore([types.stringLiteral("Before"),
types.expressionStatement(types.stringLiteral("Before"))]);
path.insertAfter([types.expressionStatement(types.stringLiteral("After")),
types.expressionStatement(types.stringLiteral("After"))]);
}
})

父级path

前面输出的Path对象,可以看到有 parentPathparent 两个属性。其中parentPath 类型为NodePah ,所以它是父级 Pathparent类型为 Node,所以它是父节点,因而只要获取到父级Path,就可以调用Path对象的各种方法去操作父节点了,父级Path 的获取可以使用 path.parentPath.

常用方法

path.findParent,向上遍历语法树,直到满足相应的条件返回

1
2
3
4
5
6
7
traverse(ast, {
ReturnStatement(path) {
console.log(path.findParent((p) => p.isFunctionExpression()) + '');
//path.findParent(function(p){return p.isObjectExpression()});这是另一种写法
//console.log(path.getFunctionParent() + '');
}
});

Path 对象的 findParent 接收一个回调函数,在向上遍历每一个父级 Path 时,会调用该回调函数,并传入对应的父级Path 对象作为参数。当该回调函数返回真值时,则将对应的父级 Path 返回。上述代码会遍历 ReturnStatement,然后向上找父级 Path,当找到 Path 对象类型为 ObjectExpression的情况时,就返回该Path 对象.

path.getFunctionParent,向上查找与当前节点最接近的父函数 path.getFunctionParent 返回的也是 Path 对象

path.getStatementParent,向上遍历语法树,直到找到语句父节点,Statement语句包含很多,例如,声明语句、return语句、if语句、switch语句、while语句等等,返回的也是Path对象,该方法从当前节点开始找起,如果想要找到return 语句的父语句,就需要从parentPath中去调用,代码如下:

1
console.log(path.parentPath.getStatementParent());

同级path

container

在介绍同级path之前,先介绍一下container

对于这段代码

1
2
3
4
5
6
7
8
9
10
11
let obj = {
name:'John',
add:function(a,b){
let x = 200;
let y = 100;
return a+b+x+y+1000;
},
mul:function(a,b){
return a*b;
},
};

运行

1
2
3
4
5
traverse(ast, {
ReturnStatement(path){
console.log(path);
}
});

这段解析的代码作用就是在 AST 遍历过程中,每当遇到 ReturnStatement 节点时,打印当前节点的 path

对于这一段函数代码

1
2
3
4
5
add:function(a,b){
let x = 200;
let y = 100;
return a+b+x+y+1000;
},

container如下

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
container: [
Node {
type: 'VariableDeclaration',
start: 63,
end: 75,
loc: [SourceLocation],
declarations: [Array],
kind: 'let'
},
Node {
type: 'VariableDeclaration',
start: 85,
end: 97,
loc: [SourceLocation],
declarations: [Array],
kind: 'let'
},
Node {
type: 'ReturnStatement',
start: 107,
end: 127,
loc: [SourceLocation],
argument: [Node]
}
],
listKey: 'body',
key: 2,

可以得出:

1
2
3
container是一个数组,它存放了一组Node节点(AST 节点)。
每个元素是一个 AST Node,比如 VariableDeclaration(变量声明)和ReturnStatement(返回语句)。
这些 Node 共同组成了一个代码块(BlockStatement { ... })。

几个问题

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
(1)key是什么?
key: 2 可以看出:
key 表示当前 ReturnStatement 在 container 数组中的索引位置。
key = 2 说明 ReturnStatement 是 container 里的第三个元素(索引从 0 开始)。
比如这段代码
function add(a, b) {
let x = 10;
let y = 20;
return a + b + x + y;
}
AST结构如下:
FunctionDeclaration
└── BlockStatement (body: [])
├── VariableDeclaration (let x = 10;)
├── VariableDeclaration (let y = 20;)
└── ReturnStatement (return a + b + x + y;)

(2)listKey 和 container 的层级关系
简单来说:
container 是 AST 结构中的某个属性值,它是一个数组,存放多个 Node。
listKey 是 指向 container 这个数组的属性名称。
listKey 不是 container 的成员,而是 path 的成员。
path.parent[listKey] 就是 container
比如这段代码
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

const code = `
function add(a, b) {
let x = 10;
let y = 20;
return a + b + x + y;
}`;

const ast = parser.parse(code, { sourceType: "module" });

traverse(ast, {
ReturnStatement(path) {
console.log("Node:", path.node);
console.log("Parent:", path.parent);
console.log("Container:", path.container);
console.log("listKey:", path.listKey);
console.log("Parent[listKey] === Container:", path.parent[path.listKey] === path.container);
}
});
//输出如下
Node: { type: 'ReturnStatement', argument: { ... } }
Parent: { type: 'BlockStatement', body: [Array] }
Container: [
{ type: 'VariableDeclaration', kind: 'let' },
{ type: 'VariableDeclaration', kind: 'let' },
{ type: 'ReturnStatement' }
]
listKey: body
Parent[listKey] === Container: true
//说明了什么
这说明:
path.parent 是 BlockStatement(即 { let x = 10; let y = 20; return ... })。
path.parent.body 是 一个数组,存放了 VariableDeclaration 和 ReturnStatement 这些节点。
container 就是 path.parent.body,也就是 BlockStatement 里的所有语句。
listKey 是 "body",它告诉我们 container 其实是 path.parent 的 body 属性。

(3)listKey 是怎么来的
当 Babel 解析代码并创建 AST 时,每个 Node 都有自己的 结构。
某些 Node 类型(如 BlockStatement)会有 数组属性 来存储多个子节点,比如 body。
当 traverse 遍历 AST 并到达 ReturnStatement 这种节点时,它会:
获取 ReturnStatement 的父节点(parent)。
检查 parent 里有哪些属性存放了多个 Node(即数组)。
找到 ReturnStatement 在 parent 的哪个数组属性里,并把这个属性名存入 listKey。
所以,listKey 的值是 ReturnStatement 在 parent 里的属性名。
代码:
function add(a, b) {
let x = 10;
let y = 20;
return a + b + x + y;
}
如下AST:
FunctionDeclaration
└── BlockStatement (body: [])
├── VariableDeclaration (let x = 10;)
├── VariableDeclaration (let y = 20;)
└── ReturnStatement (return a + b + x + y;)
解析代码:
traverse(ast, {
ReturnStatement(path) {
console.log("Node:", path.node); // 当前节点
console.log("Parent:", path.parent); // 父级 BlockStatement
console.log("Container:", path.container); // 存放 return 语句的数组
console.log("listKey:", path.listKey); // "body",表示 container 属于 parent.body
}
});
打印结果
Node: { type: 'ReturnStatement', argument: { ... } }
Parent: { type: 'BlockStatement', body: [Array] }
Container: [
{ type: 'VariableDeclaration', kind: 'let' },
{ type: 'VariableDeclaration', kind: 'let' },
{ type: 'ReturnStatement' }
]
listKey: "body"
可以发现Parent的body的内容就是container的内容,这里是输出的时候被console.log省略了

(4)listKey怎么来的
接上面
listKey 的工作原理
Babel 在 path 创建时,会自动查找 node 在 parent 里的位置:
1.找到 path.node 的父节点:
path.node 是 ReturnStatement。
path.parent 是 BlockStatement(函数体 {})。
2.找出 path.node 在 path.parent 里的属性:
BlockStatement 有一个 body 属性,它是一个 数组,存放所有代码块里的语句。
body 里有 { let x = 10; }、{ let y = 20; } 和 { return a + b + x + y; }。
ReturnStatement 是 body 数组的第三个元素。
3.存入 listKey:
Babel 发现 ReturnStatement 在 BlockStatement.body 这个数组里。
listKey = "body",表示 ReturnStatement 来自 parent.body 这个数组。

所以container存储的是当前节点的同级节点

当然container不一定都是数组,也有不是数组的情况,对于这种情况,可以理解为当前节点没有兄弟节点

同级节点有关方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 + 1).node );
console.log("====================================");
console.log('getAllNextSiblings', path.getAllNextSiblings());
console.log('getAllPrevSiblings', path.getAllPrevSiblings());
console.log('getNextSibling', path.getNextSibling());
console.log('getPrevSibling', path.getPrevSibling());
// node {type: 'ReturnStatement' ... }
path.stop();
}
});
1
2
3
4
5
6
7
path.inList 判断是否有同级节点
path.container 获取容器,包括所有同级节点的数组
path.listKey 获取容器名
path.key 获取当前节点在container中的索引
path.getSibling 用于获取同级 Path,其中参数index 即容器数组中的索引,index 可以通过 path.key 来获取,可以对 path.key 进行加减操作,来定位到不同的同级Path.
getAllNextSiblings 获取所有后面的节点
getAllPrevSiblings 获取所有前面的节点

container中插入节点,unshiftContainer是在容器头部插入节点,pushContainer是在容器后面插入节点

1
2
3
4
5
6
7
8
9
traverse(ast, {
ReturnStatement(path) {
path.parentPath.unshiftContainer('body', [
types.expressionStatement(types.stringLiteral('Before1')),
types.expressionStatement(types.stringLiteral('Before2'))]);
console.log(path.parentPath.pushContainer('body',
types.expressionStatement(types.stringLiteral('After'))));
}
});

scope对象

介绍

scope 提供了一些属性和方法,可以方便的查找标识符的作用域,获取标识符的所有引用,修改标识符的所有引用,以及知道标识符是否参数,标识符是否为常量,如果不是常量也可以知道在哪里修改了它,以下面的代码为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const a = 1000;
let b = 2000;
let obj = {
name: 'xiaojianbang',
add: function (a) {
a = 400;
b = 300;
let e = 700;
function demo() {
let d = 600;
}
demo();
return a + a + b + 1000 + obj.name;
}
};
obj.add(100);

ast.js

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
// 获取e变量的作用域范围,打印出作用域内的代码
traverse(ast, {
Identifier(path) {
if (path.node.name == 'e') {
console.log(generator(path.scope.block).code);
}
}
});

// 获取函数的作用域范围,打印代码
// 对于函数得从path.parentPath获取作用域
traverse(ast, {
FunctionDeclaration(path) {
console.log(generator(path.parentPath.scope.block).code);
}
});

// 获取标识符的绑定
traverse(ast, {
FunctionDeclaration(path) {
// 获取变量的绑定
let binding1 = path.scope.getBinding('a');
//console.log(binding);
console.log(generator(binding1.scope.block).code);
// 获取函数的绑定
let binding2 = path.scope.getBinding('demo');
console.log(generator(binding2.scope.block).code);
}
});

// 该函数用于获取当前节点自己的绑定,也就是不包含父级作用域中定义的标识符的绑定
// 但是该函数会得到子函数中定义的标识符的绑定
// 简而言之就是判断标识符是否是在自己的作用域范围内定义的,比如函数内用到的b是全局变量b,那么b就不是自己的绑定
function TestOwnBinding(path){
path.traverse({
Identifier(p){
let name = p.node.name;
console.log( name, !!p.scope.getOwnBinding(name) );
}
});
}
traverse(ast, {
FunctionExpression(path){
TestOwnBinding(path);
}
});

// 判断标识符的作用域是否为当前作用域
// 比如function a内定义一个function b,b内定义了一个变量d,如果用上面的代码取获取a内的标识符,d也会被输出
// 下面的方法就是筛掉子函数内定义的标识符的
// 通过path + ''将path转换为js代码和scope转换的js代码比较,一致的话就是当前函数内定义的标识符
function TestOwnBinding(path){
path.traverse({
Identifier(p){
let name = p.node.name;
let binding = p.scope.getOwnBinding(name);
//console.log(name, binding);
binding && console.log( name, generator(binding.scope.block).code == path + '' );
}
});
}
traverse(ast, {
FunctionExpression(path){
TestOwnBinding(path);
}
});

traverse(ast, {
FunctionDeclaration(path) {
let binding = path.scope.getBinding('a');
//path.traverse
binding.scope.traverse(binding.scope.block, {
AssignmentExpression(p) {
if (p.node.left.name == 'a')
p.node.right = types.numericLiteral(500);
}
});
}
});

traverse(ast, {
FunctionExpression(path) {
let binding = path.scope.getBinding('b');
binding.scope.rename('b', path.scope.generateUidIdentifier("_0x2ba6ea").name);

// let uid1 = path.scope.generateUidIdentifier("uid");
// console.log(uid1);
// let uid2 = path.scope.generateUidIdentifier("uid");
// console.log(uid2);

}
});


traverse(ast, {
Identifier(path){
path.scope.rename(path.node.name, path.scope.generateUidIdentifier('_0x2ba6ea').name);
}
});

几个问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(1)什么是绑定
在 Babel AST 解析中,"绑定"(Binding) 指的是 变量(或函数)在作用域中的定义及其相关信息。
path.scope.getBinding(name)用于查找 当前作用域及其父作用域 中变量 name 的绑定信息。
它返回一个 Binding 对象,其中包含该变量的 定义位置、引用位置、常量性 等信息。

(2)Binding 绑定信息包含哪些内容?
Babel 返回的 Binding 对象通常包含:
identifier:变量的定义标识符(即 let x = 10; 里的 x)。
scope:该变量的作用域对象(Scope)。
path:指向变量声明的 NodePath。
constant:是否是常量(true/false)。
references:被引用的次数。
referencePaths:所有引用的 NodePath 数组。


referencePaths与constantViolations

在 Babel 的 AST 处理过程中,referencePathsconstantViolationsBinding 对象的属性,主要用于追踪变量的引用情况和是否被修改。它们是在 Babel 的作用域分析(Scope Analysis)阶段被定义的。

referencePaths,存储当前绑定(binding)的所有引用位置。定义在 Binding 对象中,包含所有引用了该变量的 Identifier 节点(但不包括声明)和不会包含对该变量的赋值位置(赋值的位置属于 constantViolations

比如

1
2
3
4
5
function test() {
let x = 10;
console.log(x);
return x + 5;
}

xbinding 可能类似于

1
2
3
4
5
6
7
8
9
{
"identifier": { "name": "x" },
"path": { "type": "VariableDeclarator" },
"referencePaths": [
{ "type": "Identifier", "name": "x" }, // console.log(x);
{ "type": "Identifier", "name": "x" } // return x + 5;
],
"constantViolations": []
}

referencePaths 包含 console.log(x)return x + 5Identifier 节点,但不包括 let x = 10;,因为 let 语句是变量的定义,不是引用。

constantViolations,存储所有可能修改变量值的位置。定义同样在 Binding 对象中,包含变量的重新赋值位置(如 x = 20;)、通过 ++, --, += 等操作修改变量的地方、如果是 const 声明,理论上 constantViolations 应该为空

比如

1
2
3
4
5
6
function test() {
let x = 10;
x = 20;
x += 5;
console.log(x);
}

xbinding 可能类似于

1
2
3
4
5
6
7
8
9
10
11
{
"identifier": { "name": "x" },
"path": { "type": "VariableDeclarator" },
"referencePaths": [
{ "type": "Identifier", "name": "x" } // console.log(x);
],
"constantViolations": [
{ "type": "AssignmentExpression", "operator": "=", "right": 20 }, // x = 20;
{ "type": "AssignmentExpression", "operator": "+=", "right": 5 } // x += 5;
]
}

什么时候使用 referencePathsconstantViolations

1
2
如果你想找到变量在代码中的所有引用(如 console.log(x)),用referencePaths。
如果你想找出变量在哪里被修改了(如 x = 20;),用 constantViolations。

查看绑定的referencePaths

1
2
3
4
5
6
traverse(ast, {
FunctionExpression(path) {
let binding = path.scope.getBinding('a');
console.log(binding.referencePaths);
}
})

输出大概长这样

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
[
NodePath {
parentPath: NodePath {
parentPath: [NodePath],
container: [Node],
listKey: undefined,
key: 'left',
node: [Node],
type: 'BinaryExpression',
parent: [Node],
},
container: Node {
type: 'BinaryExpression',
left: [Node],
operator: '+',
right: [Node]
},
listKey: undefined,
key: 'left',
node: Node {
type: 'Identifier',
name: 'a'
},
type: 'Identifier',
parent: Node {
type: 'BinaryExpression',
left: [Node],
operator: '+',
right: [Node]
},
hub: undefined,
data: null,
scope: Scope {
path: [NodePath],
block: [Node],
inited: true,
labels: Map(0) {},
bindings: [Object: null prototype],
references: [Object: null prototype] {},
}
},
NodePath {
parentPath: NodePath {
parentPath: [NodePath],
container: [Node],
listKey: undefined,
key: 'left',
node: [Node],
type: 'BinaryExpression',
parent: [Node],
},
container: Node {
type: 'BinaryExpression',
left: [Node],
operator: '+',
right: [Node]
},
listKey: undefined,
key: 'right',
node: Node {
type: 'Identifier',
name: 'a'
},
type: 'Identifier',
parent: Node {
type: 'BinaryExpression',
left: [Node],
operator: '+',
right: [Node]
},
hub: undefined,
data: null,
context: TraversalContext {
parentPath: [NodePath],
scope: [Scope],
},
scope: Scope {
path: [NodePath],
block: [Node],
inited: true,
bindings: [Object: null prototype],
references: [Object: null prototype] {},
globals: [Object: null prototype] {},
}
}
]

先说明几个问题

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
(1)为什么输出有两个Node?
因为a被引用的地方有两个
这个 return 语句可以拆解为:
return (a + a) + b + 1000 + obj.name;
a + a 是 一个 BinaryExpression,其中 a 作为 BinaryExpression 的子节点

结构大致是这样的
{
"type": "BinaryExpression",
"operator": "+",
"left": {
"type": "Identifier",
"name": "a"
},
"right": {
"type": "Identifier",
"name": "a"
}
}

a 在 left 和 right 里被引用。
BinaryExpression 是 a 的 parentPath
所以 binding.referencePaths 里的 a:
它的 parentPath 是 BinaryExpression(因为 a 是 a + a 这个表达式的一部分)。
BinaryExpression 的 parentPath 才是 return 语句。

(2)binding.path和referencePaths和parentPath
binding.path是变量的定义 即 a 在 function (a) {...} 的参数
referencePaths是变量的所有引用 即 a 在 a + a + b + 1000 里
parentPath是引用的a的地方的父path,在这里的话,a 在 a + a 里面,所以 parentPath 是 BinaryExpression

(3)引用(reference)和定义(binding)
结论:引用(reference)一定是 Identifier,但 Identifier 不一定是引用。
引用(reference)指的就是AST里Identifier节点的位置,比如a在return a + a + b + 1000; 里的每个 a,它们都属于referencePaths 里的节点。
但定义(binding)不一定是Identifier节点,它的具体类型取决于a是在哪种语法结构中被定义的。
用如下代码可以查看变量定义的位置
let binding = path.scope.getBinding('a');
console.log(binding.path.node);
这个binding.path.node就是a定义的节点类型,它的类型不一定是Identifier。
如下:
定义方式 | binding.path.node的类型 | 示例代码
变量声明 | VariableDeclarator | let a = 100;
函数参数 | Identifier | function fn(a) {}
函数声明 | FunctionDeclaration | function a() {}
类声明 | ClassDeclaration | class a {}
导入语句 | ImportSpecifier | import { a } from 'mod';

(4)为什么第一个NodePath长这样
NodePath {
parentPath: NodePath {
parentPath: [NodePath],
container: [Node],
listKey: undefined,
key: 'left',
node: [Node],
type: 'BinaryExpression',
parent: [Node]
},
container: Node {
type: 'BinaryExpression',
left: [Node],
operator: '+',
right: [Node]
},
listKey: undefined,
key: 'left'
}
这里的container和key的值是left怎么理解
先解决container为什么是BinaryExpression
return语句是 return a + a + b + 1000 + obj.name;
它的AST结构如下:
ReturnStatement
└── BinaryExpression (a + a + b + 1000 + obj.name)
├── BinaryExpression (a + a)
│ ├── Identifier (a) <-- left
│ ├── "+"
│ ├── Identifier (a) <-- right
├── "+"
├── Identifier (b)
├── "+"
├── NumericLiteral (1000)
├── "+"
├── MemberExpression (obj.name)
只看a的NodePath
BinaryExpression (a + a)
├── left: Identifier (a)
├── operator: "+"
├── right: Identifier (a)
这里 a 在 BinaryExpression 的 left 和 right 中
当 Babel 遍历 a 的 NodePath 时:
a的 container 是它所在的父节点,也就是BinaryExpression

其次,parentPath.key = "left" 是什么意思
在上面的AST结构中
BinaryExpression (a + a)
├── left: Identifier (a) <-- 输出的 `parentPath.key === "left"`
├── operator: "+"
├── right: Identifier (a)
第一个a在BinaryExpression里的left位置:BinaryExpression.left = Identifier(a);

总结:
container 代表当前节点所在的父级表达式
parentPath.key 指的是当前Node在parentPath.node里的属性名

constantViolations也同理了,所以可以根据标识符的binding去修改代码

1
2
3
4
5
6
7
8
9
10
traverse(ast, {
FunctionExpression(path) {
let binding = path.scope.getBinding('a');
console.log(binding.referencePaths);
console.log(binding.constantViolations[0].node);
binding.constantViolations[0].node.right = types.valueToNode(500);
binding.referencePaths[0].replaceWithSourceString('y + h');
path.stop();
}
})

遍历作用域

前面说过,使用path.scope.block可以获取作用域,但是针对函数的话,需要path.scope.parent.block获取作用域,而binding.scope可以直接获取,不需要考虑是变量还是函数

1
2
3
4
5
6
7
8
9
10
11
12
13
traverse(ast, {
FunctionDeclaration(path) {
let binding = path.scope.getBinding('a');
//path.traverse
//在标识符a的作用域内遍历,其实就是遍历函数内部
binding.scope.traverse(binding.scope.block, {
AssignmentExpression(p) {
if (p.node.left.name == 'a')
p.node.right = types.numericLiteral(500);
}
});
}
});

标识符重命名

scope.generateUidIdentifier可以返回不容易重名的变量名

1
2
3
4
5
6
7
8
9
10
11
12
traverse(ast, {
FunctionExpression(path) {
let binding = path.scope.getBinding('b');
binding.scope.rename('b', path.scope.generateUidIdentifier("_0x2ba6ea").name);

// let uid1 = path.scope.generateUidIdentifier("uid");
// console.log(uid1);
// let uid2 = path.scope.generateUidIdentifier("uid");
// console.log(uid2);

}
});