AST抽象语法树

一、什么是抽象语法树

在计算机科学中,抽象语法树(abstract syntax tree 或者缩写为 AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结构。

之所以说语法是「抽象」的,是因为这里的语法并不会表示出真实语法中出现的每个细节。

二、使用场景

  • JS 反编译,语法解析
  • Babel 编译 ES6 语法
  • 代码高亮
  • 关键字匹配
  • 作用域判断
  • 代码压缩

    如果你是一名前端开发,一定用过或者听过babeleslintprettier等工具,它们对静态代码进行翻译、格式化、代码检查,在我们的日常开发中扮演了重要的角色,这些工具无一例外的应用了AST。
  • 前端开发中依赖的AST工具集合

    这里不得不拉出来介绍一下的是Babel,从ECMAScript的诞生后,它便充当了代码和运行环境的翻译官,让我们随心所欲的使用js的新语法进行代码编写。

  • 那么Babel是怎么进行代码翻译的呢?

    如下图所示,Babylon首先解析(parse)阶段会生成AST,然后babel-transform对AST进行变换(transform),最后使用babel-generate生成目标代码(generate)。

    babel

    我们用一个小例子来看一下,例如我们想把const nokk = 5;中的变量标识符nokk逆序, 变成const kkon = 5

    Step Parse

    const babylon = require('babylon')
    
    const code = `
      const nokk = 5;
    `
    const ast = babylon.parse(code)
    console.log('%o', ast)
    

      

    Step Transform

    const traverse = require('@babel/traverse').default
    
    traverse(ast, {
      enter(path) {
        if (path.node.type === 'Identifier') {
          path.node.name = path.node.name
            .split('')
            .reverse()
            .join('')
        }
      }
    })
    

      

    Step Generate

    const generator = require('@babel/generator').default
    
    const targetCode = generator(ast)
    console.log(targetCode)
    
    // { code: 'const kkon = "water";', map: null, rawMappings: null }
    

      

    复制代码
     1 const babel = require('babel-core'); //babel核心解析库
     2 const t = require('babel-types'); //babel类型转化库
     3 let code = `let sum = (a, b)=> a+b`;
     4 let ArrowPlugins = {
     5 //访问者模式
     6   visitor: {
     7   //捕获匹配的API
     8     ArrowFunctionExpression(path) {
     9       let { node } = path;
    10       let params = node.params;
    11       let body = node.body;
    12       if(!t.isBlockStatement(body)){
    13         let returnStatement = t.returnStatement(body);
    14         body = t.blockStatement([returnStatement]);
    15       }
    16       let r = t.functionExpression(null, params, body, false, false);
    17       path.replaceWith(r);
    18     }
    19   }
    20 }
    21 let d = babel.transform(code, {
    22   plugins: [
    23     ArrowPlugins
    24   ]
    25 })
    26 console.log(d.code);
    复制代码

    看看输出结果:

    1 let sum = function (a, b) {
    2   return a + b;
    3 };

    三、AST Explorer

    https://astexplorer.net/

    四、深入原理

    可视化的工具可以让我们迅速有感官认识,那么具体内部是如何实现的呢?

    继续使用上文的例子:

    1 Function getAST(){}

    JSON 也很简单:

    复制代码
     1 {
     2   "type": "Program",
     3   "start": 0,
     4   "end": 19,
     5   "body": [
     6     {
     7       "type": "FunctionDeclaration",
     8       "start": 0,
     9       "end": 19,
    10       "id": {
    11         "type": "Identifier",
    12         "start": 9,
    13         "end": 15,
    14         "name": "getAST"
    15       },
    16       "expression": false,
    17       "generator": false,
    18       "params": [],
    19       "body": {
    20         "type": "BlockStatement",
    21         "start": 17,
    22         "end": 19,
    23         "body": []
    24       }
    25     }
    26   ],
    27   "sourceType": "module"
    28 }
    复制代码

    ast4

    怀着好奇的心态,我们来模拟一下用代码实现:

    复制代码
     1 const esprima = require('esprima'); //解析js的语法的包
     2 const estraverse = require('estraverse'); //遍历树的包
     3 const escodegen = require('escodegen'); //生成新的树的包
     4 let code = `function getAST(){}`;
     5 //解析js的语法
     6 let tree = esprima.parseScript(code);
     7 //遍历树
     8 estraverse.traverse(tree, {
     9   enter(node) {
    10     console.log('enter: ' + node.type);
    11   },
    12   leave(node) {
    13     console.log('leave: ' + node.type);
    14   }
    15 });
    16 //生成新的树
    17 let r = escodegen.generate(tree);
    18 console.log(r);
    复制代码

    运行后,输出:

    复制代码
     1 enter: Program
     2 enter: FunctionDeclaration
     3 enter: Identifier
     4 leave: Identifier
     5 enter: BlockStatement
     6 leave: BlockStatement
     7 leave: FunctionDeclaration
     8 leave: Program
     9 function getAST() {
    10 }
    复制代码

    我们看到了遍历语法树的过程,这里应该是深度优先遍历。

    稍作修改,我们来改变函数的名字 getAST => Jartto

    复制代码
     1 const esprima = require('esprima'); //解析js的语法的包
     2 const estraverse = require('estraverse'); //遍历树的包
     3 const escodegen = require('escodegen'); //生成新的树的包
     4 let code = `function getAST(){}`;
     5 //解析js的语法
     6 let tree = esprima.parseScript(code);
     7 //遍历树
     8 estraverse.traverse(tree, {
     9   enter(node) {
    10     console.log('enter: ' + node.type);
    11     if (node.type === 'Identifier') {
    12       node.name = 'Jartto';
    13     }
    14   }
    15 });
    16 //生成新的树
    17 let r = escodegen.generate(tree);
    18 console.log(r);
    复制代码

    运行后,输出:

    1 enter: Program
    2 enter: FunctionDeclaration
    3 enter: Identifier
    4 enter: BlockStatement
    5 function Jartto() {
    6 }

    可以看到,在我们的干预下,输出的结果发生了变化,方法名编译后方法名变成了 Jartto

    这就是抽象语法树的强大之处,本质上通过编译,我们可以去改变任何输出结果。

    补充一点:关于 node 类型,全集大致如下:

    (parameter) node: Identifier | SimpleLiteral | RegExpLiteral | Program | FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | SwitchCase | CatchClause | VariableDeclarator | ExpressionStatement | BlockStatement | EmptyStatement | DebuggerStatement | WithStatement | ReturnStatement | LabeledStatement | BreakStatement | ContinueStatement | IfStatement | SwitchStatement | ThrowStatement | TryStatement | WhileStatement | DoWhileStatement | ForStatement | ForInStatement | ForOfStatement | VariableDeclaration | ClassDeclaration | ThisExpression | ArrayExpression | ObjectExpression | YieldExpression | UnaryExpression | UpdateExpression | BinaryExpression | AssignmentExpression | LogicalExpression | MemberExpression | ConditionalExpression | SimpleCallExpression | NewExpression | SequenceExpression | TemplateLiteral | TaggedTemplateExpression | ClassExpression | MetaProperty | AwaitExpression | Property | AssignmentProperty | Super | TemplateElement | SpreadElement | ObjectPattern | ArrayPattern | RestElement | AssignmentPattern | ClassBody | MethodDefinition | ImportDeclaration | ExportNamedDeclaration | ExportDefaultDeclaration | ExportAllDeclaration | ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier | ExportSpecifier

    说到这里,聪明的你,可能想到了 Babel,想到了 js 混淆,想到了更多背后的东西。接下来,我们要介绍介绍 Babel 是如何将 ES6 转成 ES5 的。

    esprima、estraverse 和 escodegen

    esprimaestraverse 和 escodegen 模块是操作 AST 的三个重要模块,也是实现 babel 的核心依赖,下面是分别介绍三个模块的作用。

    1、esprima 将 JS 转换成 AST

    esprima 模块的用法如下:

    /
    / 文件:esprima-test.js
    const esprima = require("esprima");
    
    let code = "function fn() {}";
    
    // 生成语法树
    let tree = esprima.parseScript(code);
    
    console.log(tree);
    
    // Script {
    //   type: 'Program',
    //   body:
    //    [ FunctionDeclaration {
    //        type: 'FunctionDeclaration',
    //        id: [Identifier],
    //        params: [],
    //        body: [BlockStatement],
    //        generator: false,
    //        expression: false,
    //        async: false } ],
    //   sourceType: 'script' }
    

      

    通过上面的案例可以看出,通过 esprima 模块的 parseScript 方法将 JS 代码块转换成语法树,代码块需要转换成字符串,也可以通过 parseModule 方法转换一个模块。

    2、estraverse 遍历和修改 AST

    查看遍历过程:

    // 文件:estraverse-test.js
    const esprima = require("esprima");
    const estraverse = require("estraverse");
    
    let code = "function fn() {}";
    
    // 遍历语法树
    estraverse.traverse(esprima.parseScript(code), {
        enter(node) {
            console.log("enter", node.type);
        },
        leave() {
            console.log("leave", node.type);
        }
    });
    
    // enter Program
    // enter FunctionDeclaration
    // enter Identifier
    // leave Identifier
    // enter BlockStatement
    // leave BlockStatement
    // leave FunctionDeclaration
    // leave Program
    

      

    上面代码通过 estraverse 模块的 traverse 方法将 esprima 模块转换的 AST 进行了遍历,并打印了所有的 type 属性并打印,每含有一个 type 属性的对象被叫做一个节点,修改是获取对应的类型并修改该节点中的属性即可。

    其实深度遍历 AST 就是在遍历每一层的 type 属性,所以遍历会分为两个阶段,进入阶段和离开阶段,在 estraverse 的 traverse 方法中分别用参数指定的 entry 和 leave 两个函数监听,但是我们一般只使用 entry

    3、escodegen 将 AST 转换成 JS

    下面的案例是一个段 JS 代码块被转换成 AST,并将遍历、修改后的 AST 重新转换成 JS 的全过程。

    // 文件:escodegen-test.js
    const esprima = require("esprima");
    const estraverse = require("estraverse");
    const escodegen = require("escodegen");
    
    let code = "function fn() {}";
    
    // 生成语法树
    let tree = esprima.parseScript(code);
    
    // 遍历语法树
    estraverse.traverse(tree, {
        enter(node) {
            // 修改函数名
            if (node.type === "FunctionDeclaration") {
                node.id.name = "ast";
            }
        }
    });
    
    // 编译语法树
    let result = escodegen.generate(tree);
    
    console.log(result);
    
    // function ast() {
    // }
    

      

    在遍历 AST 的过程中 params 值为数组,没有 type 属性。

    实现 Babel 语法转换插件

    实现语法转换插件需要借助 babel-core 和 babel-types 两个模块,其实这两个模块就是依赖 esprimaestraverse 和 escodegen 的。

    使用这两个模块需要安装,命令如下:

    npm install babel-core babel-types

    1、plugin-transform-arrow-functions

    plugin-transform-arrow-functions 是 Babel 家族成员之一,用于将箭头函数转换 ES5 语法的函数表达式。

    // 文件:plugin-transform-arrow-functions.js
    const babel = require("babel-core");
    const types = require("babel-types");
    
    // 箭头函数代码块
    let sumCode = `
    const sum = (a, b) => {
        return a + b;
    }`;
    let minusCode = `const minus = (a, b) => a - b;`;
    
    // 转化 ES5 插件
    let ArrowPlugin = {
        // 访问者(访问者模式)
        visitor: {
            // path 是树的路径
            ArrowFunctionExpression(path) {
                // 获取树节点
                let node = path.node;
    
                // 获取参数和函数体
                let params = node.params;
                let body = node.body;
    
                // 判断函数体是否是代码块,不是代码块则添加 return 和 {}
                if (!types.isBlockStatement(body)) {
                    let returnStatement = types.returnStatement(body);
                    body = types.blockStatement([returnStatement]);
                }
    
                // 生成一个函数表达式树结构
                let func = types.functionExpression(null, params, body, false, false);
    
                // 用新的树结构替换掉旧的树结构
                types.replaceWith(func);
            }
        }
    };
    
    // 生成转换后的代码块
    let sumResult = babel.transform(sumCode, {
        plugins: [ArrowPlugin]
    });
    
    let minusResult = babel.transform(minusCode, {
        plugins: [ArrowPlugin]
    });
    
    console.log(sumResult.code);
    console.log(minusResult.code);
    
    // let sum = function (a, b) {
    //   return a + b;
    // };
    // let minus = function (a, b) {
    //   return a - b;
    // };
    

      

    我们主要使用 babel-core 的 transform 方法将 AST 转化成代码块,第一个参数为转换前的代码块(字符串),第二个参数为配置项,其中 plugins 值为数组,存储修改 babal-core 转换的 AST 的插件(对象),使用 transform 方法将旧的 AST 处理成新的代码块后,返回值为一个对象,对象的 code 属性为转换后的代码块(字符串)。

    内部修改通过 babel-types 模块提供的方法实现,API 可以到 https://github.com/babel/babe... 中查看。

    ArrowPlugin 就是传入 transform 方法的插件,必须含有 visitor 属性(固定),值同为对象,用于存储修改语法树的方法,方法名要严格按照 API,对应的方法会修改 AST 对应的节点。

    在 types.functionExpression 方法中参数分别代表,函数名(匿名函数为 null)、函数参数(必填)、函数体(必填)、是否为 generator 函数(默认 false)、是否为 async 函数(默认 false),返回值为修改后的 AST,types.replaceWith 方法用于替换 AST,参数为新的 AST。

    2、plugin-transform-classes

    plugin-transform-classes 也是 Babel 家族中的成员之一,用于将 ES6 的 class 类转换成 ES5 的构造函数。

    // 文件:plugin-transform-classes.js
    const babel = require("babel-core");
    const types = require("babel-types");
    
    // 类
    let code = `
    class Person {
        constructor(name) {
            this.name = name;
        }
        getName () {
            return this.name;
        }
    }`;
    
    // 将类转化 ES5 构造函数插件
    let ClassPlugin = {
        visitor: {
            ClassDeclaration(path) {
                let node = path.node;
                let classList = node.body.body;
    
                // 将取到的类名转换成标识符 { type: 'Identifier', name: 'Person' }
                let className = types.identifier(node.id.name);
                let body = types.blockStatement([]);
                let func = types.functionDeclaration(className, [], body, false, false);
                path.replaceWith(func);
    
                // 用于存储多个原型方法
                let es5Func = [];
    
                // 获取 class 中的代码体
                classList.forEach((item, index) => {
                    // 函数的代码体
                    let body = classList[index].body;
    
                    // 获取参数
                    let params = item.params.length ? item.params.map(val => val.name) : [];
    
                    // 转化参数为标识符
                    params = types.identifier(params);
    
                    // 判断是否是 constructor,如果构造函数那就生成新的函数替换
                    if (item.kind === "constructor") {
                        // 生成一个构造函数树结构
                        func = types.functionDeclaration(className, [params], body, false, false);
                    } else {
                        // 其他情况是原型方法
                        let proto = types.memberExpression(className, types.identifier("prototype"));
    
                        // 左侧层层定义标识符 Person.prototype.getName
                        let left = types.memberExpression(proto, types.identifier(item.key.name));
    
                        // 右侧定义匿名函数
                        let right = types.functionExpression(null, [params], body, false, false);
    
                        // 将左侧和右侧进行合并并存入数组
                        es5Func.push(types.assignmentExpression("=", left, right));
                    }
                });
    
                // 如果没有原型方法,直接替换
                if (es5Func.length === 0) {
                    path.replaceWith(func);
                } else {
                    es5Func.push(func);
                    // 替换 n 个节点
                    path.replaceWithMultiple(es5Func);
                }
            }
        }
    };
    
    // 生成转换后的代码块
    result = babel.transform(code, {
        plugins: [ClassPlugin]
    });
    
    console.log(result.code);
    
    // Person.prototype.getName = function () {
    //     return this.name;
    // }
    // function Person(name) {
    //     this.name = name;
    // }
    

      

    上面这个插件的实现要比 plugin-transform-arrow-functions 复杂一些,归根结底还是将要互相转换的 ES6 和 ES5 语法树做对比,找到他们的不同,并使用 babel-types 提供的 API 对语法树对应的节点属性进行修改并替换语法树,值得注意的是 path.replaceWithMultiple 与 path.replaceWith 不同,参数为一个数组,数组支持多个语法树结构,可根据具体修改语法树的场景选择使用,也可根据不同情况使用不同的替换方法。

原文地址:https://www.cnblogs.com/ygunoil/p/14830587.html