Javascript抽象語法樹下篇(實踐篇)

  • 2019 年 12 月 16 日
  • 筆記

上篇已經對AST基礎做了介紹,本篇介紹AST的運用

AST應用的三個要點

  1. 需要一個解析器,將代碼轉換為AST
  2. 需要一個遍歷器,能夠遍歷AST,並能夠方便的對AST節點進行增刪改查等操作
  3. 需要一個代碼生成器,能夠將AST轉換為代碼

esprima與babel

常用的滿足上述3個要點的工具包有兩個,一個是 esprima,一個是 babel

esprima相關包及使用如下

const esprima = require('esprima');   // code => ast  const estraverse = require('estraverse'); //ast遍歷  const escodegen = require('escodegen'); // ast => code  let code = 'const a = 1';  const ast = esprima.parseScript(code);  estraverse.traverse(ast, {      enter: function (node) {          //節點操作      }  });  const transformCode = escodegen.generate(ast);

babel相關包及使用如下

const parser = require('@babel/parser');  //code => ast  const traverse = require('@babel/traverse').default; // ast遍歷,節點增刪改查,作用域處理等  const generate = require('@babel/generator').default; // ast => code  const t = require('@babel/types'); // 用於AST節點的Lodash式工具庫,各節點構造、驗證等  let code = 'const a = 1';  let ast = parser.parse(sourceCode);  traverse(ast, {    enter (path) {      //節點操作    }  })  const transformCode = escodegen.generate(ast);

目前babel不管是從生態上還是文檔上比esprima要好很多,因此推薦大家使用babel工具,本文示例也使用babel來做演示。

使用babel工具操作AST

如上一章節所示

  • @babel/parser用於將代碼轉換為AST
  • @babel/traverse用於對AST的遍歷,包括節點增刪改查、作用域等處理
  • @babel/generator 用於將AST轉換成代碼
  • @babel/types 用於AST節點操作的Lodash式工具庫,各節點構造、驗證等

更多api詳見babel手冊[1]

下面通過簡單案例來介紹如何操作AST,注意案例只是示例,由於篇幅對部分邊界問題只會注釋說明,實際開發過程中需要考慮周全。

案例1:去掉代碼中的console.log()

實現代碼

const parser = require('@babel/parser');  const traverse = require('@babel/traverse').default;  const generate = require('@babel/generator').default;  const t = require('@babel/types');  let sourceCode = `  function square(n) {    console.log(n);    console.warn(n);    return n * n;  }  `  let ast = parser.parse(sourceCode);  traverse(ast, {   CallExpression(path) {    let { callee } = path.node;    if (callee.type === 『MemberExpression』 && callee.object.name === 『console』 && callee.property.name === 『log』 ) {     path.remove(); // 注意考慮對象掛載的識別,如global.console.log(),此時remove後剩下global.,會導致語法錯誤,此時可以判斷父節點類型來排除    }   }  })  console.log(generate(ast).code);

處理結果

function square(n) {  -  console.log(n);    console.warn(n);    return n * n;  }

此案例涉及知識點

  1. 如何通過traverse遍歷特定節點
  2. 識別出console.log()在規範中屬於函數調用表達式,節點類型為 CallExpression
  3. console.log本身即 callee是在對象console上的一個方法,因此 console.log是一個成員表達式,類型為 MemberExpression
  4. MemberExpression根據規範有一個 object屬性代表被訪問的對象,有一個 property代表訪問的成員。
  5. 通過 path.remove()api可以對節點進行刪除。
  6. 可以通過https://astexplorer.net/ 來輔助對代碼節點的識別。注意選擇 babylon7,即babe7,對應 @babel/parser

案例2:變量混淆

實現代碼

const parser = require('@babel/parser');  const traverse = require('@babel/traverse').default;  const generate = require('@babel/generator').default;  const t = require('@babel/types');  let sourceCode = `  function square(number) {    console.warn(number);    return number * number;  }  `  let ast = parser.parse(sourceCode);  traverse(ast, {    FunctionDeclaration(path) {      let unia = path.scope.generateUidIdentifier("a");      path.scope.rename("number",unia.name);   }  })    console.log(generate(ast).code);

處理結果

-function square(number) {  +  function square(_a) {  -  console.warn(number);  +  console.warn(_a);  -  return number * number;  +  return _a * _a;  }

此案例涉及知識點

  1. path.scope保存了當前作用域的相關信息
  2. 可以通過api對作用域內的變量名進行批量修改操作
  3. 通過 path.scope可以獲得當前作用域唯一標識符,避免變量名衝突

案例3:轉換箭頭函數並去掉未使用參數

實現代碼

const parser = require('@babel/parser');  const traverse = require('@babel/traverse').default;  const generate = require('@babel/generator').default;  const t = require('@babel/types');  let sourceCode = `  new Promise((resolve,reject)=>{    setTimeout(()=>{      resolve(1);    },200)  });  `  let ast = parser.parse(sourceCode);  traverse(ast, {    ArrowFunctionExpression (path) {      let { id, params, body } = path.node;      for(let key in path.scope.bindings){   //注意考慮箭頭函數的this特性,若發現函數體中有this調用,則需要在當前作用域綁定其父作用域的this        if(!path.scope.bindings[key].referenced){          params = params.filter(param=>{            return param.name!==key;          })        }      }    path.replaceWith(t.functionExpression(id, params, body));    }  })    console.log(generate(ast).code);

處理結果

-new Promise((resolve,reject)=>{  +new Promise(function(resolve){  -  setTimeout(()=>{  +  setTimeout(function(){      resolve(1);    },200)  });

此案例涉及知識點

  1. 箭頭函數節點: ArrowFunctionExpression
  2. 通過path.scope可以識別變量引用情況,是否有被引用,被哪些路徑引用
  3. 通過@babel/types可以很方便的構建任意類型節點
  4. 通過 path.replaceWith()可以進行節點替換

案例4:京東購物小程序的Tree-shaking

刪掉小程序中的冗餘代碼, 部分實現代碼示例如下

const parser = require('@babel/parser');  const traverse = require('@babel/traverse').default;  const generate = require('@babel/generator').default;  const t = require('@babel/types');  let sourceCode = `  export function square (x) {      return x * x;  }  export function cube (x) {      return x * x * x;  }  `  let ast = parser.parse(sourceCode);  traverse(ast, {    ExportNamedDeclaration (path) {      let unused = ['cube']   // 藉助webpack,我們能獲得導出的方法中,哪些是沒有被使用過的      let { declaration = {} } = path.node;      if (declaration.type === 'FunctionDeclaration') {        unused.forEach(exportItem => {          // references=1表示僅有一次引用,即export的引用,沒有在別處調用          if (declaration.id.name === exportItem && path.scope.bindings[exportItem].references === 1) {            path.remove();          }        });      }    }  })    console.log(generate(ast).code);

處理結果

export function square (x) {      return x * x;  }  -export function cube (x) {  -    return x * x * x;  -}

此案例涉及知識點

  1. export節點: ExportNamedDeclaration

案例5:將代碼轉換成svg流程圖

此案例是git上一個比較有意思的開源項目,通過AST將代碼轉換為svg流程圖,詳見js-code-to-svg-flowchart[2]

可以體驗一下:demo[3]

通過以上示例,可以看到通過AST我們可以對代碼任意蹂躪,做出很多有意思的事情

AST在其他語言的應用

除了Javascript,其他語言如HTML、CSS、SQL等也有廣泛的AST應用。如下圖,可以在這裡找到對應語言的解析器,開啟AST之門。

結語

在上述AST網站中,可以看到HTML的解析器有個vue選項,讀過vue源碼的同學應該知道vue模板在轉換成HTML之前會先將模板轉換成AST然後生成render function進而生成VirtualDOM。我們平時開發對AST使用比較少,但其實到處都能見到AST的影子:babel、webpack、eslint、taro等等。希望能拋磚引玉,使同學們在各自團隊產出更多基於AST的優秀工具、項目。

References [1] babel手冊:https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md [2] js-code-to-svg-flowchart:https://github.com/Bogdan-Lyashenko/js-code-to-svg-flowchart [3] demo:https://bogdan-lyashenko.github.io/js-code-to-svg-flowchart/docs/live-editor/index.html