zoukankan      html  css  js  c++  java
  • 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 不同,参数为一个数组,数组支持多个语法树结构,可根据具体修改语法树的场景选择使用,也可根据不同情况使用不同的替换方法。

  • 相关阅读:
    Flask路由+视图补充
    Flask登录认证
    Flask
    初识Flask
    redis 注意事项
    Linux安装python和更新pip
    Django 导入配置文件
    redis 5种类型
    redis 支持事务
    数组乱序与数组拆解
  • 原文地址:https://www.cnblogs.com/ygunoil/p/14830587.html
Copyright © 2011-2022 走看看