AST语法树
ast抽象语法树,全名:Abstract Syniax Tree ,是源代码语法结构的一种抽象表示,它以树的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构我们熟知的很多的工具和库的核心都是通过 Abstract Syniax Tree 抽象语法树这个概念来实现对代码的检查、分析等操作的
使用ast可以干嘛?
- 将源代码进行解析解析成 ast语法树,遍历这个树,修改节点的值,重新生成新的树
- 转换ast的第三方库
esprima(将源代码生成ast) estraverse(遍历ast语法树) escodegen(重新生成源代码) - 使用
babel转换源代码 使用到的库@babel/core(生成ast,遍历语法树,转换) babel-types(用于创建新的节点类型或是判断节点类型)。babel的转换过程就是使用ast解析语法,修改源代码,在输出新的代码的过程 webpack打包EsLint代码的检查IDE的错误提示,格式化,高亮,自动补全UglifyJS代码压缩混淆- TypeScript、jsx和原生js得转化
如果了解抽象语法树这个概念,就可以随手编写类似的工具
以上工具得原理都是通过 javascript parser 将代码转换成抽象语法树(ast),这棵树定义了代码得结构,因此,我们可以精准的定位到声明语句,赋值语句,运算语句...,实现对代码的分析,优化,变更等操作。
其大概流程是:
input -> tokenizer -> token 词法分析 逐词的分析和判断 此时不涉及到具体的结构
token -> parser -> ast 语法分析
ast -> transform -> newAst 代码转换
newAst -> generate -> output 生成需要的代码
词法分析
// 词法分析 逐词的分析和判断 此时不涉及到具体的结构
// const code = '(add 2(subtract 4 2))'
// DFS depth first search 深度优先
function tokenizer(code) {
let index = 0; // 索引
let tokens = [];
let charSpaceReg = /\s/ // 空格
let numberReg = /[0-9]/ // 数字
let letterReg = /[a-zA-Z]/ // 字母
let operateReg = /[+\-*/]/ // 运算符
while (index < code.length) {
let char = code[index] // 当前索引位置的值
if (char === ',') {
tokens.push({
type: 'comma',
value: ","
})
index++
continue
}
if (char === '(') {
tokens.push({
type: 'paren',
value: "("
})
index++
continue
}
if (char === ')') {
tokens.push({
type: 'paren',
value: ")"
})
index++
continue
}
if (char === '{') {
tokens.push({
type: 'brace',
value: "{"
})
index++
continue
}
if (char === '}') {
tokens.push({
type: 'brace',
value: "}"
})
index++
continue
}
// 空格
if (charSpaceReg.test(char)) {
index++
continue
}
// 数字
if (numberReg.test(char)) {
let value = '';
while (numberReg.test(char)) {
value += char;
char = code[++index]
}
tokens.push({
type: 'number',
value
})
continue
}
// 引号之间的内容
if (char === '"') {
let value = '';
// 第一个引号
char = code[++index]
while (char !== '"') {
value += char;
// 引号之间的内容
char = code[++index]
}
// 第二个引号
char = code[++index]
tokens.push({
type: 'string',
value
})
continue
}
// 字母
if (letterReg.test(char)) {
let value = '';
while (letterReg.test(char)) {
value += char;
char = code[++index]
}
tokens.push({
type: 'name',
value
})
continue
}
// 运算符
if (operateReg.test(char)) {
tokens.push({
type: 'operater',
value: char
})
index++
continue
}
throw TypeError('其他类型', char)
}
return tokens
}
tokenizer('(add 6(subtract 8 9))')
/*
[
{ type: 'paren', value: '(' },
{ type: 'name', value: 'add' },
{ type: 'number', value: '6' },
{ type: 'paren', value: '(' },
{ type: 'name', value: 'subtract' },
{ type: 'number', value: '8' },
{ type: 'number', value: '9' },
{ type: 'paren', value: ')' },
{ type: 'paren', value: ')' }
]
*/
tokenizer('function fn(a,b){ return a + b }')
/*
[
{ type: 'name', value: 'function' },
{ type: 'name', value: 'fn' },
{ type: 'paren', value: '(' },
{ type: 'name', value: 'a' },
{ type: 'comma', value: ',' },
{ type: 'name', value: 'b' },
{ type: 'paren', value: ')' },
{ type: 'brace', value: '{' },
{ type: 'name', value: 'return' },
{ type: 'name', value: 'a' },
{ type: 'operater', value: '+' },
{ type: 'name', value: 'b' },
{ type: 'brace', value: '}' }
]
*/
语法分析
// 语法分析
function parser(tokens) {
let index = 0;
function combin() {
let token = tokens[index];
if (token.type === 'number') {
index++;
return {
type: "NumberLiteral",
value: token.value
}
}
if (token.type === 'string') {
index++;
return {
type: "StringLiteral",
value: token.value
}
}
// 注意层级关系
if (token.type === 'paren' && token.value === '(') {
token = tokens[++index];
let node = {
type: "CallLiteral",
name: token.value,
params: []
}
token = tokens[++index];
// 处理 ((xxx)) || (x(xxx)) 情况
while (token.type !== 'paren' || (token.type === 'paren' && token.value !== ')')) {
node.params.push(combin());
token = tokens[index];
}
index++
return node
}
}
let ast = {
type: "Program",
body: []
}
while (index < tokens.length) {
ast.body.push(combin())
}
return ast
}
let tokens = [
{ type: 'paren', value: '(' },
{ type: 'name', value: 'add' },
{ type: 'number', value: '6' },
{ type: 'paren', value: '(' },
{ type: 'name', value: 'subtract' },
{ type: 'number', value: '8' },
{ type: 'number', value: '9' },
{ type: 'paren', value: ')' },
{ type: 'paren', value: ')' }
]
let res = parser(tokens)
console.log('res', JSON.stringify(res));
/*
{
"type": "Program",
"body": [
{
"type": "CallLiteral",
"name": "add",
"params": [
{
"type": "NumberLiteral",
"value": "6"
},
{
"type": "CallLiteral",
"name": "subtract",
"params": [
{
"type": "NumberLiteral",
"value": "8"
},
{
"type": "NumberLiteral",
"value": "9"
}
]
}
]
}
]
}
*/
javascript parser
javascript parser 就是把js源码转为抽象语法树的解析器
常用的javascript parser有
- esprima www.npmjs.com/package/esp…
- esprima 把代码转为ast
- estraverse 遍历语法树 www.npmjs.com/package/est…
- escodegen 根据语法树重新生成源代码 www.npmjs.com/package/esc…
- traceur
- acorn
- shift
- babel parser
- 在线生成
ast地址
//使用https://esprima.org/demo/parse.html# 在线解析
function fn(){}
//的结果:
{
"type": "Program", // 程序
"start": 0, // 整体范围 0~15
"end": 15,
"body": [ // 代码体 body
{
"type": "FunctionDeclaration", // 函数声明
"start": 0, // 开始位置
"end": 15, // 结束位置
"id": {
"type": "Identifier", // 标识符 function 到小括号 之间的标识符
"start": 9, // 标识符位置在9~11
"end": 11,
"name": "fn" // 名字
},
"expression": false, // 是否为表达式
"generator": false, // 是否是generator 这儿没加* 也就是false
"async": false, // 是否异步函数
"params": [], // 参数 这儿没有参数 所以为空
"body": { // 函数体 body
"type": "BlockStatement", // 块语句 这儿也就是 这对大括号
"start": 13,
"end": 15,
"body": [] // 大括号里面的 代码体 这儿没有 就是空的
}
}
],
"sourceType": "module" // 源码类型 模块
}
const esprima = require('esprima');
let code = `function fn() { }`;
let ast = esprima.parseScript(code);
console.log('ast', ast);
/*
{
"type": "Program",
"body": [
{
"type": "FunctionDeclaration",
"id": {
"type": "Identifier",
"name": "fn"
},
"params": [],
"body": {
"type": "BlockStatement",
"body": []
},
"generator": false,
"expression": false,
"async": false
}
],
"sourceType": "script"
}
*/
上面就是ast的结构和节点,除此之外还有一些节点,如:
- File 文件
- Program 程序
- Literal 字面量
NumericLiteralStringLiteralBooleanLiteral - Statement 语句
- Declaration 声明语句
- Expression 表达式
- Class 类
AST 遍历规则
最先进入的最后出去 最后进入的 最先出去 就像现实中走房间 我总结为
依次遍历 深度优先(DFS:depth first search)
demo
- 利用ast的4个核心步骤
- 将源代码转换成ast语法树
- 解析ast语法树转换成想要生成的代码+
- 代替
- 重新生成代码