AST相关API详解
一、准备工作
-
在线工具网站
- 在线ob混淆:https://obfuscator.io/
- 在线AST解析:https://astexplorer.net/
- babel官方手册:https://babeljs.io/docs/
-
NodeJS安装:
-
Babel相关组件安装
-
首先在项目目录下进行初始化
npm init -y
-
然后安装babel相关组件
npm i @babel/parser @babel/traverse @babel/types @babel/generator @babel/template
-
-
代码基本结构
cost fs = require('fs') const parser = require('@babel/parser') const traverse = require('@babel/traverse').default const types = require('@babel/types') // const t = require('@babel/types') 对于types,导包后的名字通常是types或者t const generator = require('@babel/generator').default // 读取js文件 const code = fs.readFileSync('./demo.js', {encoding: 'utf-8'}) // 解析为AST抽象语法树(解析) const ast = parser.parse(code) // 对AST进行一系列操作(转化) // traverse(ast, visitor) // ... // 根据转化后的AST,生成目标代码(生成) const newCode = generator(ast).code // 将目标代码写入新文件 fs.writeFile('./newDemo.js', newCode, err => {})
二、Babel中的组件
-
parser与generator
-
parser组件用来将JS转换成AST(即File节点)
- 需要注意parser.parse()方法有第二个参数
const ast = parser.parse(code, { sourceType: 'moudle', // 默认为script,当解析的代码中含有'import', 'export'等关键字时,需要指定为moudle,否则会报错 })
- 需要注意parser.parse()方法有第二个参数
-
generator组件用来将AST转换为JS
- generator()方法返回的是一个对象,其code属性才是代码
- generator()方法可通过第二个参数设置一些选项来影响输出结果
const newCode = generator(ast, { retainLines: false, // 是否使用与源代码相同的行号,默认false comments: false, // 是否保留注释,默认true compact: true, // 是否压缩代码 jsescOption: {minimal: true} // 能够还原unicode与十六进制字符串 }).code;
-
-
traverse与visitor
-
traverse组件用来遍历AST,通常需要配合visitor来使用
-
visitor是一个对象,可以定义一些方法来过滤节点
- 定义visitor的3种方式:
const visitor1 = { FunctionExpression: function (path){ console.log('test visitor1') } } // 最常用 const visitor2 = { FunctionExpression(path){ console.log('test visitor2') } } const visitor3 = { FunctionExpression: { enter(path){ console.log('test visitor3') }, // exit(path){ // console.log('test visitor3 exit...') // } } }
在遍历节点的过程中,有两次机会访问一个节点,即进入(enter)与退出(exit)节点时,traverse默认是在enter时处理,如果要在exit时处理,必须在visitor中声明
- 定义visitor的3种方式:
-
traverse与visitor使用示例:
- 示例1
const visitorFunction = { FunctionExpression(path){ console.log('test...') } } traverse(ast, visitorFunction)
首先声明visitor对象,名字可自定义(visitorFunction);该对象的方法名是traverse需要遍历处理的节点类型(FunctionExpression),遍历过程中,当节点类型匹配时,会执行相应的方法(比如有多个FunctionExpression,则代码会执行相应次),如果需要处理其它节点类型,可继续定义;visitor中的方法接收一个参数path,它指的是当前节点的Path对象,而非节点(Node)
- 示例2:同一个函数作用于多个节点
const visitorFunction = { 'FunctionExpression|BinaryExpression'(path){ console.log('test...') } } traverse(ast, visitorFunction)
方法名采用字符串形式,多个节点类型之间用'|'分隔
- 示例3:多个函数作用于同一个节点
let func1 = function (path){ console.log('执行func1...') } let func2 = function (path){ console.log('执行func2...') } const visitorFunction = { FunctionExpression: { enter: [func1, func2] } } traverse(ast, visitorFunction)
将赋值给enter的函数改为函数数组,执行时会按照顺序依次执行
- 示例4:对指定节点再次进行遍历
const updateParamNameVisitor = { Identifier(path){ if(path.node.name === this.paramName){ path.node.name = 'new_' + path.node.name // 修改当前节点的名称 } } } const visitor = { FunctionExpression(path){ if(path.node.params[0]){ const paramName = path.node.params[0].name // 函数第一个参数名 // path.traverse()方法,第一个参数是visitor对象,第二个参数指定visitor对象中的this path.traverse(updateParamNameVisitor, {paramName}) } } } traverse(ast, visitor)
traverse先遍历所有节点,然后根据visitor过滤FunctionExpress节点,调用path.traverse()方法,根据updateParamNameVisitor来遍历该节点下所有节点,如果函数有参数,则修改第一个参数名。path.node才是当前节点,所以path.node.params[0].name表示函数第一个参数名
- 示例1
-
-
types组件
-
types组件主要用来判断节点类型、生成新的节点等
- 判断节点类型
- t.isIdentifier(path.node) <=> path.node.type === 'Identifier'
- 传入第二个参数,可以进一步筛选节点:t.isIdentifier(path.node, {name: 'a'}) <=> path.node.type === 'Identifier' && path.node.name === 'a'
// 把所有标识符a改名为aaa const visitor = { enter(path){ if(path.node.type === 'Identifier' && path.node.name === 'a'){ path.node.name = 'aaa' } } } traverse(ast, visitor)
- t.isLiteral(path.node)
- 判断字面量(包括字符串、数值、布尔值字面量)
- 生成节点(Node)
- 以生成如下代码为例
let obj = { name: 'eliwang', add: function (a,b){ return a + b + 1000; } }
- 可以将如上代码放到在线AST解析网站查看节点结构,要生成这些节点,需要从内而外依次构造
- 生成节点时,注意节点名首字母小写,样式:t.首字母小写节点名(参数)
- 首先看到代码对应的节点是VariableDeclaration,可通过t.variableDeclaration(kind, declarations)方法生成,通过代码提示查看到源码:
declare function variableDeclaration(kind: "var" | "let" | "const" | "using", declarations: Array<VariableDeclarator>): VariableDeclaration;
kind的取值可以是"var" | "let" | "const" | "using",declarations是由VariableDeclarator组成的数组,因为有时一次性声明多个变量,返回值为VariableDeclaration
- 接着需要实现VariableDeclarator节点,通过t.variableDeclarator(id, init)方法,源码:
declare function variableDeclarator(id: LVal, init?: Expression | null): VariableDeclarator;
id为Identifier对象,init为Expression对象,默认为null
- Identifier对象实现:t.identifier(name),源码:
declare function identifier(name: string): Identifier;
- 该示例中的Expression对象为ObjectExpression对象,可通过t.objectExpression(properties)方法生成,源码:
declare function objectExpression(properties: Array<ObjectMethod | ObjectProperty | SpreadElement>): ObjectExpression;
对象的属性可以是多个,所以是数组
- 该示例中properties为2个ObjectProperty对象,可通过t.objectProperty(key, value, [computed, [shorthand,[decorators)方法生成,源码:
declare function objectProperty(key: Expression | Identifier | StringLiteral | NumericLiteral | BigIntLiteral | DecimalLiteral | PrivateName, value: Expression | PatternLike, computed?: boolean, shorthand?: boolean, decorators?: Array<Decorator> | null): ObjectProperty;
key和value为必选参数,其它可选
- 该示例中,第一个属性的值为字符串字面量(StringLiteral),通过t.stringLiteral(value)方法生成,源码:
declare function stringLiteral(value: string): StringLiteral;
- 第2个属性的值为函数表达式(FunctionExpression),通过t.functionExpression(id, params, body, [generator,[async)方法生成,源码:
declare function functionExpression(id: Identifier | null | undefined, params: Array<Identifier | Pattern | RestElement>, body: BlockStatement, generator?: boolean, async?: boolean): FunctionExpression;
id表示函数名,该示例中是一个匿名函数,所以为null,params表示参数列表,body是BlockStatement对象,其它参数可选
- BlockStatement对象可通过t.blockStatement(body, [directives)方法生成,源码:
declare function blockStatement(body: Array<Statement>, directives?: Array<Directive>): BlockStatement;
body是由Statement对象组成的数组
- 该示例中的Statement对象为ReturnStatement,通过t.returnStatement([argument)方法生成,源码:
declare function returnStatement(argument?: Expression | null): ReturnStatement;
argument是可选参数
- 该示例中ReturnStatement对象的argument属性是二项式(BinaryExpression),可通过t.binaryExpression(operator, left, right)方法生成,源码:
declare function binaryExpression(operator: "+" | "-" | "/" | "%" | "*" | "**" | "&" | "|" | ">>" | ">>>" | "<<" | "^" | "==" | "===" | "!=" | "!==" | "in" | "instanceof" | ">" | "<" | ">=" | "<=" | "|>", left: Expression | PrivateName, right: Expression): BinaryExpression;
operator即操作符,left表示操作符左侧的表达式,right表示操作符右侧的表达式
- 该示例中,BinaryExpression对象的left属性继续是BinaryExpression对象,其left和right属性是Identifier对象,right是数值字面量(NumericLiteral),可通过t.numericLiteral(value)方法生成,源码:
declare function numericLiteral(value: number): NumericLiteral;
- 梳理清楚后,我们从内到外逐层构造出示例中VariableDeclaration类型节点:
const t = require('@babel/types') const generator = require('@babel/generator').default let argLeft = t.binaryExpression('+', t.identifier('a'), t.identifier('b')) let argRight = t.numericLiteral(1000) let argument = t.binaryExpression('+', argLeft, argRight) let retStatement = t.returnStatement(argument) let funcBody = t.blockStatement([retStatement]) let funcParams = [t.identifier('a'), t.identifier('b')] let objPro2 = t.objectProperty(t.identifier('add'), t.functionExpression(null, funcParams, funcBody)) let objPro1 = t.objectProperty(t.identifier('name'), t.stringLiteral('eliwang')) let varDeclarator = t.variableDeclarator(t.identifier('obj'), t.objectExpression([objPro1, objPro2])) let varDeclaration = t.variableDeclaration('let', [varDeclarator]) let genCode = generator(varDeclaration).code console.log(genCode)
- 当生成较多字面量时,可通过t.valueToNode()方法来快速生成节点(Node):
console.log(JSON.stringify(t.valueToNode('hello'), null, 2)); console.log(JSON.stringify(t.valueToNode(['hello', 1, null, undefined, /\d+/g, true, {name: 'eliwang'}]), null ,2));
它支持各种类型,包括undefined, null, string, boolean, number, RegExp, ReadonlyArray, object等
- 以生成如下代码为例
- 判断节点类型
-
-
template与eval语句还原
-
template.statement.ast('字符串'):将字符串替换成单条语句(ExpressionStatement)- Node
-
template.statements.ast('字符串'):将字符串替换成多条语句(ExpressionStatement)- Node
-
示例:
var b = 20 eval('var a = 10;') eval(String.fromCharCode(99, 111, 110, 115, 111, 108, 101, 46, 108, 111, 103, 40, 97, 32, 42, 32, 98, 41)) console.log(a)
-
还原:
traverse(ast, { CallExpression(path) { let {callee, arguments} = path.node if (!t.isIdentifier(callee, {name: 'eval'}) || arguments.length != 1) return if(t.isStringLiteral(arguments[0])){ path.replaceInline(template.statements.ast(arguments[0].value)) }else{ let code = generator(arguments[0]).code path.replaceInline(template.statements.ast(eval(code))) } } }) /* 还原结果 var b = 20; var a = 10; console.log(a * b); console.log(a); */
-
三、Path对象
-
Path与Node的区别
const visitor = { FunctionExpression(path){ console.log(path.node) // 当前Node节点对象 console.log(path) // NodePath对象 } } traverse(ast, visitor)
-
path.stop()、path.skip()以及return之间的区别
-
path.stop()
- 将本次遍历执行完毕后,停止后续节点遍历(会将本次遍历代码完整执行)
-
path.skip()
- 执行节点替换操作后,traverse依旧能够遍历,使用path.skip()可以跳过替换后的节点遍历,避免不合理的递归调用,不影响后续节点遍历(skip位置后面代码会执行)
- 对于单次替换多个节点的情况,考虑是否可以使用path.stop()
-
return
- 跳过当前遍历的后续代码,不影响后续节点遍历(相当于continue)
-
示例代码
let a = 10; let b = 20; let c = 30; let d = 40;
-
AST处理步骤
const visitorTestStop = { Identifier(path){ if(path.node.name == 'c'){ path.stop(); } console.log(path.node.name) // 会依次打印:a b c } } const visitorTestReturn = { Identifier(path) { if(path.node.name == 'c'){ return; } console.log(path.node.name) // 会依次打印:a b d } } const visitorTestSkip = { NumericLiteral(path){ path.replaceWith(t.numericLiteral(100)); path.skip() // 不使用,则会无限递归造成死循环 } } traverse(ast, visitorTestStop) console.log('=====================') traverse(ast, visitorTestReturn) console.log('=====================') traverse(ast, visitorTestSkip) console.log(generator(ast).code) /* a b c ===================== a b d ===================== let a = 100; let b = 100; let c = 100; let d = 100; */
-
-
Path中的属性及方法
-
获取子节点/Path
console.log(path.node) // 获取当前节点 console.log(path.node.right) // 获取节点right属性值或Node // path.get()方法,可以传入节点中的属性值字符串,也可以通过'.'连接进行多级访问,返回的是包装后的Path对象 console.log(path.get('right')) // 获取该节点right属性值对应的Path对象(会将属性值包装成Path对象) console.log(path.get('left.left')) // 可以通过'.'的形式多级访问 // get()字符串参数的写法同节点操作后续一致,下面以节点值为数组类型为例 console.log(path.get('body.body')[0]); // NodePath console.log(path.get('body.body')); // [NodePath, NodePath, NodePath]
-
判断Path类型
// 判断path类型,返回true 或者 false console.log(path.isBinaryExpression()) // 不带参数形式 console.log(path.isBinaryExpression({ operator: '+', start: 140})) // 传入参数,注意是node节点中的属性-值 console.log(path.isLiteral()) // 判断字面量,包括字符串、数值、布尔值字面量 等价于path.isStringLiteral() || path.isNumericLiteral() || path.isBooleanLiteral()
用法类似于types组件中的类型判断,比如:path.isBinaryExpression({ operator: '+', start: 140}) <=> t.isBinaryExpression(path.node, {operator: '+', start: 140})
-
计算节点的值
- path.evaluate()
/*还原前代码 let a = 3; let b = 7; let c = a + b; let d = "hello" + "world"; let e = 777 ^ 888 console.log(!![]) */ const visitorRestorValue = { "BinaryExpression|UnaryExpression"(path){ // path.evaluate()返回的是一个对象,confident属性是一个布尔值,表示该节点是否可计算,value是计算的结果 let {confident, value} = path.evaluate() confident && path.replaceWith(t.valueToNode(value)) } } traverse(ast, visitorRestorValue) /* 还原后代码 let a = 3; let b = 7; let c = 10; let d = "helloworld"; let e = 113; console.log(true); */
- path.evaluate()
-
节点转代码
// 节点转代码 // 通常会利用该方法来排查错误,对节点遍历过程中的调试很有帮助以下3种方式均可以 console.log(generator(path.node).code) console.log(path.toString()) console.log(path + '')
-
替换节点属性
const visitor = { BinaryExpression(path){ path.node.left = t.valueToNode('x') path.node.right = t.identifier('y') // path.node.right.name = 'y' 修改属性值中的name属性 } } traverse(ast, visitor)
需要注意,替换的类型要在允许的类型范围之内
-
替换整个节点
- replaceWith():节点换节点(一换一)
const visitor = { BinaryExpression(path){ path.replaceWith(t.valueToNode('hello world')) } } traverse(ast, visitor)
- replaceWithMultiple():节点换节点(多换一)
const visitor = { ReturnStatement(path){ path.replaceWithMultiple([ // 当表达式语句单独在一行时(没有赋值),最好使用expressionStatement包裹 t.expressionStatement(t.stringLiteral('hello world!')), t.expressionStatement(t.numericLiteral(1000)), t.returnStatement() ]) // 替换后的节点,traverse也是能遍历到的(Babel会更新Path对象),上述return语句替换后会陷入死循环,所以使用path.stop()来停止遍历 path.stop() } } traverse(ast, visitor)
- replaceInline():节点换节点,根据参数类型,单个参数等同于replaceWith(),数组类型,则等同于replaceWithMultiple()
const visitor1 = { StringLiteral(path){ path.replaceInline(t.stringLiteral('Hello AST!')); path.stop() } } const visitor2 = { ReturnStatement(path){ path.replaceInline([ // 当表达式语句单独在一行时(没有赋值),最好使用expressionStatement包裹 t.expressionStatement(t.stringLiteral('hello world!')), t.expressionStatement(t.numericLiteral(1000)), t.returnStatement() ]) // 替换后的节点,traverse也是能遍历到的,上述return语句替换后会陷入死循环,所以使用path.stop()来停止遍历 path.stop() } } traverse(ast, visitor1) traverse(ast, visitor2)
- replaceWithSourceString():使用字符串源码替换节点
const visitor = { ReturnStatement(path){ // 获取Path对象 const argumentPath = path.get('argument'); // 格式串默认调用了argumentPath.toString()方法,即节点转代码 argumentPath.replaceWithSourceString(`function(){return ${argumentPath}}()`); // 因为内部有return语句,所以需要path.stop() path.stop() } } traverse(ast, visitor)
- replaceWith():节点换节点(一换一)
-
删除节点
const visitor = { EmptyStatement(path){ // 删除节点 path.remove(); } } traverse(ast, visitor)
删除多余的分号
-
插入节点
- insertBefore():当前节点前插入
- insertAfter():当前节点后插入
const visitor = { ReturnStatement(path){ // 节点前插入 path.insertBefore(t.expressionStatement(t.stringLiteral('Before'))); // 节点后插入 path.insertAfter(t.expressionStatement(t.stringLiteral('After'))) } } traverse(ast, visitor)
-
父级Path
- 属性
- path.parentPath:父级Path,类型为NodePath
- path.parent:父节点,类型为Node,等同于parent.parentPath.node
- 方法
- path.findParent():向上遍历语法树,直到满足相应条件,返回NodePath(不含当前节点)
const visitor = { ReturnStatement(path){ // 向上遍历语法树,参数p为每一级父级Path对象,直到满足相应的条件,返回该Path对象 let objProPath = path.findParent(p => p.isObjectProperty()) console.log(objProPath) } } traverse(ast, visitor)
- path.find():向上遍历语法树(含当前节点)
- path.getFunctionParent():查找最近的父函数Path
const visitor = { BlockStatement(path){ // 查找最近的父函数 let parentFunPath = path.getFunctionParent() console.log(parentFunPath.node.params) } } traverse(ast, visitor)
- path.getStatementParent():向上遍历,查找最近的父语句Path(含当前节点),比如声明语句、return语句、if语句、while语句等
const visitor = { ReturnStatement(path){ // 查找最近的父语句(含当前节点),return语句需要从parentPath中去调用 let a = path.parentPath.getStatementParent() console.log(a.node.type) } } traverse(ast, visitor)
- path.findParent():向上遍历语法树,直到满足相应条件,返回NodePath(不含当前节点)
- 属性
-
同级Path
- 需要先了解容器(container),一般只有容器为数组时,才有同级节点
- 属性:
- path.inList:是否有同级节点
- path.container:获取容器(包含所有同级节点的数组),如果没有同级节点,则返回Node对象
- path.key:获取当前节点在容器中的索引,如果没有同级节点,则返回该节点对应的属性名(字符串)
- path.listKey:获取容器名,如果没有容器,则返回undefined
const visitor = { ReturnStatement(path){ console.log(path.inList); // 判断是否有同级节点:true console.log(path.container); // 获取容器:[Node{type: 'ReturnStatement'...}] console.log(path.listKey) // 获取容器名:body console.log(path.key) // 获取当前节点在容器中的索引:0 } } traverse(ast, visitor)
- 方法:
- path.getSibling(index):根据容器数组中的索引,来获取同级Path,index可以通过path.key来获取
- path.unshiftContainer()与path.pushContainer():此处的path必须是容器,比如body
- 往容器最前面或者后面加入节点
- 第一个参数为listKey
- 第二个参数为Node或者Node数组
- 返回值是一个数组,里面元素是刚加入的NodePath对象
const visitor = { ReturnStatement(path){ console.log(path.getSibling(path.key - 1)) // 根据索引来获取同级Path // 容器前面插入单个节点 console.log(path.parentPath.unshiftContainer('body', t.expressionStatement(t.stringLiteral('Before...')))); // [NodePath{...}] // 容器后面插入2个节点 console.log(path.parentPath.pushContainer('body', [t.expressionStatement(t.stringLiteral('After1...')), t.expressionStatement(t.stringLiteral('After2...'))])); // [NodePath{...}, NodePath{...}] } } traverse(ast, visitor)
-
四、Scope对象
-
scope提供了一些属性和方法,可以方便地查找标识符的作用域,获取并修改标识符的所有引用,以及判断标识符是否为参数或常量
-
Identifier下某标识符的path.scope 大多数情况下可使用path.scope.getBinding('某标识符').scope来代替
- 注意两者目标节点不同,前者主要针对Identifier节点,而后者则针对诸如FunctionDeclaration这种内部含Identifier节点的节点
-
本部分以下面代码为例:
const a = 1000; let b = 2000; let obj = { name: 'eliwang', add: function (a) { a = 400; b = 300; let e = 700; function demo(){ let d = 600; } demo(); return a + b + 1000 + obj.name } }
-
获取标识符作用域
-
path.scope.block
- 该属性可以获取标识符作用域,返回Node对象
-
标识符分为变量和函数:
- 标识符为变量:
const visitor = { Identifier(path) { if(path.node.name === 'e'){ console.log(generator(path.scope.block).code) } } } traverse(ast, visitor) /* function (a) { a = 400; ... return a + b + 1000 + obj.name; } */
- 标识符为函数:
const visitor = { FunctionDeclaration(path) { if(path.node.id.name === 'demo'){ // 该示例中scope.block只能获取到demo函数,而实际作用域应该是父级函数 console.log(generator(path.scope.parent.block).code) } } } traverse(ast, visitor)
- 标识符为变量:
-
-
path.scope.getBinding('标识符')
-
获取当前节点下能够引用到的标识符的绑定(含父级作用域中定义的标识符),返回Binding对象,引用不到则返回undefined
-
获取Binding对象:
traverse(ast, { FunctionDeclaration(path) { let binding = path.scope.getBinding('a'); console.log(binding); } }); /* Binding { identifier: Node {type: 'Identifier',...name: 'a'}, scope: Scope {uid: 1,path: NodePath {...},block: Node {...type: 'FunctionExpression'}...}, path: NodePath {...}, kind: 'param', constantViolations: [...], constant: false, referencePaths: [NodePath {...}], referenced: true, references: 1 } */
Binding中的关键属性:
- identifier:a标识符的Node对象
- path:a标识符的NodePath对象
- scope:a标识符的scope,其中的block节点转为代码后,就是它的作用域范围add,假如获取的是函数标识符,也可以获取其作用域(获取作用域)
- kind:表明标识符的类型,本例中是一个参数(判断是否为参数)
- constant:是否常量
- referencePaths:所有引用该标识符的节点Path对象数组(元素type为Identifier)
- constantViolations:存放所有修改该标识符节点的Path对象数组(长度不为0,表示该标识符有被修改,元素type为AssignmentExpression,即赋值表达式)
/* function test(){ return 100 } test = 10 */ traverse(ast, { FunctionDeclaration(path){ let name = path.node.id.name let binding = path.scope.getBinding(name) if(binding && binding.constantViolations.length > 0){ console.log(binding.constantViolations[0].node.type) // AssignmentExpression console.log(generator(binding.constantViolations[0].node).code) // test = 10 } } })
- referenced:是否被引用
- references:被引用的次数
-
获取函数作用域:
traverse(ast, { FunctionExpression(path) { let bindingA = path.scope.getBinding('a'); // 获取当前节点下a的Binding对象 let bindingDemo = path.scope.getBinding('demo'); // 获取当前节点下demo函数的Binding对象 console.log(bindingA.referenced); // true -- a是否被引用 console.log(bindingA.references); // 1 -- a被引用次数 console.log(generator(bindingA.constantViolations[0].node).code) // a = 400 -- 被重新赋值处的代码 console.log(generator(bindingA.referencePaths[0].node).code) // a -- 被引用处的代码 console.log(bindingDemo.references) // 1 -- demo函数被引用的次数 console.log(generator(bindingA.scope.block).code); // 变量a作用域返回的是add方法,即function (a) {...} console.log(generator(bindingDemo.scope.block).code); // 函数demo作用域返回的也是add方法 } });
可通过Binding对象.scope.block来获取标识符作用域Node
-
-
path.scope.getOwnBinding('标识符')
-
获取当前节点的标识符绑定(不含父级作用域中定义的标识符、子函数中定义的标识符)
const TestOwnBindingVisitor = { Identifier(p) { let name = p.node.name; // this.path获取的是传递过来的FunctionExpression节点NodePath console.log( name, !!this.path.scope.getOwnBinding(name) ); } } traverse(ast, { FunctionExpression(path){ path.traverse(TestOwnBindingVisitor,{path}); } }); /* a true a true b false e true demo true d false demo true a true b false obj false name false */
-
通过path.scope.getBinding()方法 + 标识符作用域是否与当前函数一致,来获取当前节点的标识符绑定
const TestOwnBindingVisitor = { Identifier(p) { let name = p.node.name; // this.path获取的是传递过来的FunctionExpression节点NodePath let binding = this.path.scope.getBinding(name) // 判断当前节点下定义的标识符:判断标识符作用域是否与当前函数一致 if (binding && generator(binding.scope.block).code == this.path.toString()){ console.log(name) } } } traverse(ast, { FunctionExpression(path){ path.traverse(TestOwnBindingVisitor,{path}); } }); /* a a e demo demo a */
-
-
遍历作用域中的节点
-
scope.traverse():【path.scope.traverse() 或者 path.scope.getBinding('xx').scope.traverse()】
- 既可以使用Path对象中的scope,也可以使用Binding对象中的scope,推荐使用Binding中的
traverse(ast, { FunctionDeclaration(path) { let binding = path.scope.getBinding('a'); // binding.scope.block就是add方法函数,因此下面就是遍历add方法中的AssignmentExpression节点 binding && binding.scope.traverse(binding.scope.block, { AssignmentExpression(p) { if (p.node.left.name == 'a') // 替换a变量的值 p.node.right = t.numericLiteral(600); // p.node.right.value = 600; } }); } });
- 既可以使用Path对象中的scope,也可以使用Binding对象中的scope,推荐使用Binding中的
-
-
标识符重命名
-
scope.rename(原名,新名):会同时修改所有引用该标识符的地方
- 使用binding.scope()操作
const visitor = { FunctionExpression(path){ // 将add函数中的a标识符更名为'x' let binding = path.scope.getBinding('a') binding && binding.scope.rename('a','x') } } traverse(ast, visitor) /* const a = 1000; let b = 2000; let obj = { name: 'eliwang', add: function (x) { x = 400; b = 300; let e = 700; function demo() { let d = 600; } demo(); return x + b + 1000 + obj.name; } }; */
- 使用path.scope()操作
const visitor = { Identifier(path){ // path.scope.generateUidIdentifier('xxx').name => 可以生成一个与现有标识符名不冲突的标识符 path.scope.rename(path.node.name, path.scope.generateUidIdentifier('uid').name) } } traverse(ast, visitor) /* const _uid = 1000; let _uid14 = 2000; let _uid15 = { name: 'eliwang', add: function (_uid13) { _uid13 = 400; _uid14 = 300; let _uid9 = 700; function _uid12() { let _uid11 = 600; } _uid12(); return _uid13 + _uid14 + 1000 + _uid15.name; } }; */
- 使用binding.scope()操作
-
-
scope的其他方法
-
scope.hasBinding('a')
- 是否有标识符a的绑定,返回true或者false,返回false时等同于scope.getBindings('a')的值为undefined
-
scope.hasOwnBinding('a')
- 当前节点是否有自己标识符a的绑定,返回true或则false
-
scope.getAllBindings()
- 获取当前节点的所有绑定,返回一个对象,该对象以标识符为属性名,对应的Bingding对象为属性值
-
scope.hasReference('a')
- 查询当前节点中是否有a标识符的引用,返回true或则false
-
scope.getBindingIdentifier('a')
- 获取当前节点中绑定的a标识符,返回Identifier的Node对象,等同于scope.getBinding('a').identifier
-
五、遍历总结
-
1、遍历整个ast
traverse(ast, visitor) => 根据visitor中的节点类型进行遍历整个ast
-
2、遍历指定NodePath
path.traverse(visitor, {xx: yy}) => 遍历当前path,第二个参数指定func中的this指针
-
遍历指定标识符作用域
let binding = path.scope.getBinding('xxx'); binding && binding.scope.traverse(binding.scope.block, visitor);
六、练习
-
1、使用types组件生成如下代码:
function test(a) { return a + 1000; }
-
// 思路:将代码放到AST Explorer网站中解析,然后由内到外构建节点 let left = t.identifier('a') let right = t.numericLiteral(1000) let binaryExpress = t.binaryExpression('+', left, right) let returnStatement = t.returnStatement(binaryExpress) let funcBlockStatement = t.blockStatement([returnStatement]) let funcIdenti = t.identifier('test') let funcParams = [t.identifier('a')] let functionDeclaration = t.functionDeclaration(funcIdenti, funcParams, funcBlockStatement) console.log(generator(functionDeclaration).code)
-
-
2、使用Babel的API将题1代码改为如下形式:
function test(a) { return function () { a + 1000; }() }
-
const visitor = { BinaryExpression(path) { let newExpressionCode = ` function () { a + 1000; }()` path.replaceWithSourceString(newExpressionCode) // 直接使用源码进行替换 path.stop() // 停止遍历,否则会无限调用 } } traverse(ast,visitor)
-
-
3、以题2代码为源码,使用Babel的API将其改写为如下形式:
function test(a) { b = 2000; return function () { a + b, a + 1000; }(); }
-
const visitorBeforeReturn = { ReturnStatement(path){ // 在return前面添加:b = 2000 let assignLeft = t.identifier('a'); let assignRight = t.numericLiteral(2000); let assignmentExpression = t.assignmentExpression('=', assignLeft, assignRight); path.insertBefore(t.expressionStatement(assignmentExpression)) } } const visitorReplaceToSequnenceExpression = { BinaryExpression(path){ // 将 a + 1000替换为:a + b, a + 1000 let binaryExpression1 = t.binaryExpression('+', t.identifier('a'), t.identifier('b')) let binaryExpression2 = t.binaryExpression('+', t.identifier('a'), t.numericLiteral(1000)) let sequenceExpression = t.sequenceExpression([binaryExpression1, binaryExpression2]) path.replaceWith(sequenceExpression) path.stop() } } traverse(ast, visitorBeforeReturn) traverse(ast, visitorReplaceToSequnenceExpression)
-
-
4、使用Babel的API将题3中的变量a改为x
// 方式一 const visitorReplaceA = { FunctionDeclaration(path){ let binding = path.scope.getBinding('a'); binding && binding.scope.rename('a','x') } } /*方式二 const visitorReplaceA = { Identifier(path){ // 替换节点属性 if(path.node.name == 'a'){ path.scope.rename(path.node.name, 'x') // path.node.name = 'x' 方式三 } } } */ traverse(ast, visitorReplaceA)
-
5、编写一个visitor,判断标识符是否为当前函数参数
let d = 1000 function test(a){ let b,c d = 2000 }
-
visitor = { FunctionDeclaration(path) { // 继续遍历当前节点 path.traverse({ Identifier(p) { let name = p.node.name; let binding = this.path.scope.getBinding(name); if(binding == undefined) return if(binding.kind == 'param'){ // 判断是否参数 console.log(`参数:${name}`) }else{ console.log(`非参数:${name}`) } } }, {path}) } } traverse(ast, visitor) /* 非参数:test 参数:a 非参数:b 非参数:c 非参数:d */
-
七、AST附录
-
常见类型对照表
序号 | 类型原名称 | 中文名称 | 描述 |
---|---|---|---|
1 | Program | 程序主体 | 整段代码的主体 |
2 | VariableDeclaration | 变量声明 | 声明一个变量,例如 var、let、const |
3 | FunctionDeclaration | 函数声明 | 声明一个函数,例如 function |
4 | EmptyStatement | 空语句 | 函数声明后面的分号';' |
5 | ExpressionStatement | 表达式语句 | 通常是调用一个函数,例如 console.log() |
6 | BlockStatement | 块语句 | 包裹在 {} 块内的代码,例如 if (condition){var a = 1;} |
7 | BreakStatement | 中断语句 | 通常指 break |
8 | ContinueStatement | 持续语句 | 通常指 continue |
9 | ReturnStatement | 返回语句 | 通常指 return |
10 | SwitchStatement | Switch 语句 | 通常指 Switch Case 语句中的 Switch |
11 | IfStatement | If 控制流语句 | 控制流语句,通常指 if(condition){}else{} |
12 | Identifier | 标识符 | 标识,例如声明变量时 var identi = 5 中的 identi |
13 | CallExpression | 调用表达式 | 通常指调用一个函数,例如 console.log() |
14 | BinaryExpression | 二进制表达式 | 通常指运算,例如 1+2 |
15 | MemberExpression | 成员表达式 | 通常指调用对象的成员,例如 console 对象的 log 成员 |
16 | ArrayExpression | 数组表达式 | 通常指一个数组,例如 [1, 3, 5] |
17 | NewExpression | New 表达式 | 通常指使用 New 关键词 |
18 | AssignmentExpression | 赋值表达式 | 通常指将函数的返回值赋值给变量 |
19 | UpdateExpression | 更新表达式 | 通常指更新成员值,例如 i++ |
20 | Literal | 字面量 | 字面量 |
21 | BooleanLiteral | 布尔型字面量 | 布尔值,例如 true false |
22 | NumericLiteral | 数字型字面量 | 数字,例如 100 |
23 | StringLiteral | 字符型字面量 | 字符串,例如 vansenb |
24 | SwitchCase | Case 语句 | 通常指 Switch 语句中的 Case |
-
Path常见属性及方法