AST学习笔记
基础部分
安装: npm install @babel/node
1. 常见节点类型
2. path和node
ast 实际上就是node,它是 构造函数的 Node 的实例化对象,它只具有对象属性(可以说,它就是个对象,除了有__clone方法之外),所以它可以利用对象去取值,直接修改等任何一系列的操作
//而path则不同
let {parse} = require("@babel/parser")
let ast = parse(`function hi(){console['\x6c\x6f\x67']('\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64\x21');}hi();`)
traverse = require("@babel/traverse").default
traverse(ast, {
StringLiteral: function(path){console.log(path)}
})
path 实际上是traverse的过程中,遍历的时候babel给我们传进去的一个对象,这个对象是构造函数 NodePath的实例化对象,这个对象相对于 node,具有非常非常多的功能和方法,这些方法将极大的方便我们进行后续的任何操作,其中,这个对象的node属性就可以取到遍历的内容的 node。
- path中另有一些有用的方法:
AST基础知识|babel库中path常用方法总结
操作AST语法树babel库中path全部方法和属性
3. evaluate()的作用
- 例1
针对作用域和引用,直接依据引用来计算出执行结果。
var a = 123 + '2sdasda31231';
var b = a;
↓
var a = "1232sdasda31231";
var b = "1232sdasda31231";
visitor = {
"BinaryExpression|Identifier"(path){
const {confident,value} = path.evaluate();
confident && path.replaceInline(types.valueToNode(value))
}
}
如图所示,对BinaryExpression和Identifier处进行了计算,得到结果值。经过试验,若加上Identifier进行traverse操作,则会报Maximum call stack size exceeded的错误。遇到一般的ob混淆时可以遍历BinaryExpression节点,还原如navigator[‘\x75\x73’+‘\x65\x72’+‘\x41\x67’+‘\x65\x6e’+‘\x74’]这样的混淆。
- 例2
let a = `let a = -0x1 * 0x21ea + -0x53b + -0x101 * -0x27`
let ast = parse(a)
traverse(ast, {
BinaryExpression: function (path){
if(path.parentPath.type === "VariableDeclarator"){ //目的是取到-0x1 * 0x21ea + -0x53b + -0x101 * -0x27这个代码对应的节点
let value = path.evaluate().value
path.replaceInline({type:"NumericLiteral", value:2})
}
}
})
console.log(generate(ast).code)
- 例3
let a = `let a = -0x1 * 0x21ea + -0x53b + -0x101 * -0x27`
//使用exit进行最小单元计算,避免一些奇怪的问题
let ast = parse(a)
traverse(ast, {
"BinaryExpression": {
"exit": function (path){
console.log(path.toString())
let {confident,value} = path.evaluate();
path.replaceInline({type:"NumericLiteral", value: value})
console.log(confident, value)
}
}
})
console.log(generate(ast).code)
4. scope
scope基本理解
作用域(scope,或译作有效范围)是名字(name)与实体(entity)的绑定(binding)保持有效的那部分计算机程序。
对于复杂程度不高的代码,可以可简单的理解为:
一个函数就是一个作用域。
一个变量就是一个绑定,依附在作用域上。
一个小demo:
js_code2 = `
function f(a){
a = 105 + a;
a++;
return a
}
const a = f(100)
console.log(a)
`
ast_code2 = parse(js_code2);
traverse(ast_code2,{
UpdateExpression(path){
console.log('当前节点源码:\n',path + '')
console.log('当前作用域节点源码:\n',path.scope.path + '')
console.log('-------------------')
}
})
输出结果:
scope.block
可以用 scope.block 获取标识符作用域,返回node,提取作用域的node。 但是不适用于函数,函数需要path.scope.parent.block父级作用域(可能会出现异常)。
由于返回的是node对象,所以需要用generator生成code,小demo:
ast_code2 = parse(js_code2);
traverse(ast_code2,{
UpdateExpression(path){
console.log('当前节点源码:\n',path + '')
console.log('block:\n',generator(path.scope.block).code + '')
console.log('-------------------')
}
})
输出结果:
scope.getBinding
Binding 对象用于存储绑定的信息,这个对象会作为Scope对象的一个属性存在,同一个作用域可以包含多个 Binding。
scope.getBinding获取标识符的绑定,用法如path.scope.getBinding(标识符),局部变量与全局变量重名时优先使用局部变量,返回一个binding构造函数的实例。path.scope.bindings 返回一个 对象,这个对象的 key,就是变量名,value 是一个 Binding对象,这个对象里面就有比较多的说法了,它有针对这个变量的非常非常多的信息:
- referenced —> 是否被引用
- referencePaths 引用的path
- references 引用次数
- path 这个变量的path
- identifier 这个变量的node
- hasValue 是否有值
- kind 变量类型,也可以理解为标识符类型 (参数 param/let/var/const)
- constantViolations 标识符(变量名)如果被修改,会存到这个里面
- hasValue 该绑定是否有值
- 可以利用作用域的 path.scope.generateUidIdentifier(“_0xabcdef”) 来创建一个不重复的 Identifier
traverse(ast_code2,{
FunctionDeclaration(path){
console.log('当前节点源码:\n',path + '')
console.log(path.scope.getBinding('a'))
console.log('-------------------')
}
})
输出结果,可见a标识符在该作用域内(function)被引用了三次:
traverse(ast_code2,{
FunctionDeclaration(path){
console.log('当前节点源码:\n',path + '')
console.log('当前标识符对应作用域中被引用的次数为:\n',path.scope.getBinding('a').references)
console.log('当前标识符对应作用域的源码为:\n',path.scope.getBinding('a').scope.path + '')
console.log('--------------------------------------')
}
})
输出结果:
binding.scope.traverse()
实现在作用域内的遍历,用法与一般的traverse()一样。
scope.parent
就是返回上一级的作用域,也是一个 Scope对象
5. types
使用types.valueToNode方法可以快速将代码转为node节点:types.binaryExpression("+",types.valueToNode(1),types.valueToNode(1))
进阶部分
使用ast生成代码
- af = 100
const generator = require("@babel/generator").default;
const types = require("@babel/types");
let g = types.assignmentExpression('=', types.identifier('af'), types.valueToNode(100))
console.log(g)
console.log(generator(g).code)
- var a = 1000 + b;
let g2 = types.VariableDeclaration('var',
[
types.VariableDeclarator(types.identifier("a"), types.BinaryExpression("+", types.valueToNode(1000), types.identifier("b"))
)
])
console.log(g2)
console.log(generator(g2).code)
- 函数调用: console.log(g);
computed
代表是否使用[ ]进行取值,为true时候是console[‘log’] ,false是console.log
m = types.ExpressionStatement(
types.CallExpression(
types.MemberExpression(types.identifier("console"),types.identifier("log"),false),
[types.identifier("g")]
)
)
console.log(generator(m).code)
- 生成if-else语句
if (a === "666"){
console.log(6)
}else{
console.log(7)
}
types.ifStatement(
types.binaryExpression("===",types.identifier("a"),types.valueToNode("666")),
types.blockStatement([types.expressionStatement(types.callExpression(types.memberExpression(types.identifier("console"),types.identifier("log")),[types.valueToNode(6)]))]),
types.blockStatement([types.expressionStatement(types.callExpression(types.memberExpression(types.identifier("console"),types.identifier("log")),[types.valueToNode(7)]))])
)
- 生成try-catch语句
try{
console.log("success")
}catch (e){
console.log(e)
}
generator(types.tryStatement(
types.blockStatement([types.expressionStatement(types.callExpression(types.memberExpression(types.identifier("console"),types.identifier("log")),[types.valueToNode("success")]))]),
types.catchClause(types.identifier("e"),types.blockStatement([types.expressionStatement(types.callExpression(types.memberExpression(types.identifier("console"),types.identifier("log")),[types.identifier("e")]))]))
)).code