一文搞懂AST语法树

647 阅读5分钟

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

//使用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 字面量 NumericLiteral StringLiteral BooleanLiteral
  • Statement 语句
  • Declaration 声明语句
  • Expression 表达式
  • Class 类

AST 遍历规则

1705999763285.png

最先进入的最后出去 最后进入的 最先出去 就像现实中走房间 我总结为

依次遍历 深度优先(DFS:depth first search)

demo

image.png

  • 利用ast的4个核心步骤
    1. 将源代码转换成ast语法树
    2. 解析ast语法树转换成想要生成的代码+
    3. 代替
    4. 重新生成代码