AST应用
# 初识AST
掌握了AST,相当于掌握了控制代码的代码能力
# 什么是ast
It is a hierarchical program representation that presents source code structure according to the grammar of a programming language, each AST node corresponds to an item of a source code. 它是一种分层的程序表示,根据编程语言的语法表示源代码结构,每个AST节点对应于源代码的一项。
# ast的抽象结构标准?
JavaScript AST 的结构 遵循的是ESTree 标准, 无论babel、或者Esprima、Acorn https://github.com/estree/estree, estree是由多个 JavaScript 解析器和编译器开发者、工具和库的贡献者们共同制定和维护
HTML 的解析通常会生成 DOM(文档对象模型),它是由浏览器中的 HTML 解析器创建的一种树形结构,表示 HTML 文档的层次结构,暂时没有类似estree统一的标准规范制定, 例如htmlparser解析工具
CSS 的解析通常会生成样式表对象模型(CSSOM),它类似于 DOM,是表示 CSS 样式表的一种树形结构。CSSOM 由浏览器中的 CSS 解析器创建 。 例如postcss解析工具
# 为什么要了解ast
掌握了AST,相当于掌握了控制代码的代码能力,可以帮助我们拓宽思路和视野,不管是写框架,还是写工具和逻辑,AST都会成为我们的得力助手
# AST 在编译中的位置
在编译原理中,编译器转换代码通常要经过三个步骤:词法分析(Lexical Analysis)、语法分析(Syntax Analysis)、代码生成(Code Generation),如下图:
# 词法分析
词法分析工具 (opens new window) 词法分析工具 (opens new window)
词法分析阶段是编译过程的第一个阶段,这个阶段的任务是从左到右一个字符一个字符地读入源程序,然后根据构词规则识别单词,生成 token 符号流,比如 isPanda('🐼'),会被拆分成 isPanda,(,'🐼',) 四部分,每部分都有不同的含义,可以将词法分析过程想象为不同类型标记的列表或数组。
# 语法分析
语法分析是编译过程的一个逻辑阶段,语法分析的任务是在词法分析的基础上将单词序列组合成各类语法短语,比如“程序”,“语句”,“表达式”等,前面的例子中,isPanda('🐼') 就会被分析为一条表达语句 ExpressionStatement,isPanda() 就会被分析成一个函数表达式 CallExpression,就会被分析成一个变量 Literal 等,众多语法之间的依赖、嵌套关系,就构成了一个树状结构,即 AST 语法树。
# 代码生成
代码生成是最后一步,将 AST 语法树转换成可执行代码即可,在转换之前,我们可以直接操作语法树,进行增删改查等操作,例如,我们可以确定变量的声明位置、更改变量的值、删除某些节点等,我们将语句 isPanda('🐼') 修改为一个布尔类型的 Literal:true,语法树就有如下变化:
# Babel 相关
Babel 包含的各种功能包、API、各方法可选参数等,在实际使用过程中,可以安装如下 npm install @babel/core @babel/parser @babel/traverse @babel/generator
在做逆向解混淆中,主要用到了 Babel 的以下几个功能包:
@babel/core:Babel 编译器本身,提供了 babel 的编译 API; @babel/parser:将 JavaScript 代码解析成 AST 语法树; @babel/traverse:遍历、修改 AST 语法树的各个节点; @babel/generator:将 AST 还原成 JavaScript 代码; @babel/types:判断、验证节点的类型、构建新 AST 节点等。
@babel/core Babel 编译器本身,被拆分成了三个模块:@babel/parser、@babel/traverse、@babel/generator,比如以下方法的导入效果都是一样的:
const parse = require("@babel/parser").parse;
const parse = require("@babel/core").parse;
const traverse = require("@babel/traverse").default
const traverse = require("@babel/core").traverse
2
3
4
5
@babel/parser
@babel/parser 可以将 JavaScript 代码解析成 AST 语法树,其中主要提供了两个方法:
parser.parse(code, [{options}]):解析一段 JavaScript 代码;<br>
parser.parseExpression(code, [{options}]):考虑到了性能问题,解析单个 JavaScript 表达式。
2
以下看个具体的例子、源码:
/**
* 专题&章节封面图,占位图placeholder,根据屏幕宽度自适应拼接前端别名后缀
*/
import { util_feSuffix } from '../../util.js';
const computedBehavior = require('miniprogram-computed');
Component({
behaviors: [computedBehavior],
properties: {
src: {
type: String,
value: '',
},
width: {
type: Number,
value: 0,
},
height: {
type: Number,
value: 0,
},
isBg: {
type: Boolean,
value: false,
},
mode: {
type: String,
value: 'aspectFill',
},
quality: {
type: Boolean,
value: false,
},
isSuffix: {
type: Boolean,
value: true,
},
index: {
type: [String, Number],
value: 0,
},
},
data: {
loaded: false,
test11: [1, 2, 3],
},
computed: {
style(data) {
const { width, height } = data;
if (width > height) {
// 宽图
return `width: ${height}rpx; height: ${height / 2}rpx;`;
} else {
// 长图
return `width: ${width / 2}rpx; height: ${width / 4}rpx;`;
}
},
},
watch: {
src: function () {
setTimeout(() => {
this.init();
}, 200);
},
},
methods: {
init() {
const { src, width, quality, isSuffix } = this.data;
if (!src) {
console.log('检查是否传递了src');
return;
}
this.setData({
computedSrc: isSuffix ? util_feSuffix({ src, width, quality }) : src,
});
},
loaded() {
this.setData({ loaded: true });
this.setEvent();
},
setEvent() {
let event = 'onImageLoad';
let index = this.data.index;
this.triggerEvent(event, { index });
},
},
});
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
转换后的ast结构
例如我们在将微信的代码转化为百度小程序是,因为百度不支持miniprogram-computed依赖引入以及behaviors注入,第一行为VariableDeclaration节点类型
traverse(result, {
// 删除js中computedBehavior引用
VariableDeclaration(path) {
path.node.declarations.forEach((item) => {
if (item.id.name == 'computedBehavior') {
path.remove();
}
})
},
ObjectProperty(path) {
if (path.node.key.name === 'behaviors' && path.node.value.type === 'ArrayExpression') {
const elements = path.node.value.elements;
const index = elements.findIndex((item) => item.name == 'computedBehavior');
if (index > -1) {
elements.splice(index, 1);
}
}
}
})
或者
traverse(result, {
// 删除js中computedBehavior引用
VariableDeclaration: {
enter(path) {
path.node.declarations.forEach((item) => {
if (item.id.name == 'computedBehavior') {
path.remove();
}
})
},
},
ObjectProperty: {
enter(path) {
if (
path.node.key.name === 'behaviors' &&
path.node.value.type === 'ArrayExpression'
) {
const elements = path.node.value.elements;
const index = elements.findIndex((item) => item.name == 'computedBehavior');
if (index > -1) {
elements.splice(index, 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
以上几种写法中有用到了 enter 方法,在节点的遍历过程中,进入节点(enter)与退出(exit)节点都会访问一次节点,traverse 默认在进入节点时进行节点的处理,如果要在退出节点时处理,那么在 visitor 中就必须声明 exit 方法
以上修改ast之后,当我们执行generate之后即得到我们修改之后的代码,使用astexplorer (opens new window)查看
# babel/traverse节点类型
Identifier:标识符
Literal:字面量(数字、字符串、布尔值等)
BinaryExpression:二元表达式(如加法、减法)
UnaryExpression:一元表达式(如取反、递增、递减)
LogicalExpression:逻辑表达式(如逻辑与、逻辑或)
AssignmentExpression:赋值表达式
ConditionalExpression:条件表达式
CallExpression:函数调用表达式
FunctionDeclaration:函数声明
ArrowFunctionExpression:箭头函数表达式
ObjectExpression:对象表达式
ArrayExpression:数组表达式
MemberExpression:成员表达式
IfStatement:if 语句
SwitchStatement:switch 语句
ForStatement:for 循环语句
WhileStatement:while 循环语句
DoWhileStatement:do-while 循环语句
BreakStatement:break 语句
ContinueStatement:continue 语句
ReturnStatement:return 语句
ThrowStatement:throw 语句
TryStatement:try-catch 语句
BlockStatement:代码块
ExpressionStatement:表达式语句
# traverse节点遍历方法
# 基础节点遍历方法
Identifier 用于访问标识符节点
VariableDeclaration 用于访问变量声明节点
FunctionDeclaration 用于访问函数声明节点
# 表达式节点遍历方法
CallExpression 用于访问调用表达式节点
MemberExpression 用于访问成员表达式节点
BinaryExpression 用于访问二元表达式节点
UnaryExpression 用于访问一元表达式节点
ObjectExpression 用于访问对象表达式节点
ArrayExpression 用于访问数组表达式节点
ConditionalExpression 用于访问条件表达式节点
# path节点操作方法
# 添加节点
path.pushContainer() 用于在容器节点中添加子节点
path.insertBefore() 用于在当前节点之前插入新节点
path.insertAfter() 用于在当前节点之后插入新节点
# 移除节点
path.remove() 用于移除当前节点
path.replaceWith() 用于替换当前节点
# 获取节点信息
path.node 用于访问当前节点的 AST 信息
path.parent 用于访问当前节点的父节点
path.get() 用于获取指定路径下的节点
# 其他
path.skip 方法用于跳过当前节点的后代节点
path.skipKey 方法用于跳过特定的节点键的遍历
path.traverse 是 NodePath 对象的方法,可以在特定节点上进行遍历
# AST工具和V8编译
对比 AST 标准工具与 V8 引擎的编译过程
AST 标准工具与 V8 引擎处理代码的过程有一定的重合,不同的是,V8 将 AST 转换为字节码后进入执行代码的阶段,而 AST 标准工具只是将旧代码转换为新代码