玩轉 ast- 手寫 babel 插件篇

AST

抽象語法樹是什麼?

以下是普通函數 function ast(){}, 轉換爲 ast 後的格式:

抽象語法樹用途有哪些?

webpack、Lint 等這些工具的原理都是通過 JavaScript Parser 把源代碼轉化爲一顆抽象語法樹(AST),通過操縱這顆樹,我們可以精準的定位到聲明語句、賦值語句、運算語句等等,實現對代碼的分析、優化、變更等操作。

JavaScript 解析器

那什麼是 JavaScript 解析器呢?
JavaScript 解析器的作用,把 JavaScript 源碼轉化爲抽象語法樹。
瀏覽器會把 js 源碼通過解析器轉換爲 ast,再進一步轉換爲字節碼或者直接生成機器碼,進行渲染和執行。
一般來說,每個 js 引擎都有自己的抽象語法樹格式,Chrome 的 v8 引擎, firfox 的 Spider Monkey 引擎等。

JavaScript 解析器通常可以包含四個組成部分。

詞法分析器(Lexical Analyser)

首先詞法分析器會掃描(scanning)代碼,將一行行源代碼,通過 switch case 把源碼?“/ 爲一個個小單元,jS 代碼有哪些語法單元呢?大致有以下這些:

就是一個字符一個字符地遍歷,然後通過 switch case 分情況討論,整個實現方法就是順序遍歷和大量的條件判斷。以 @babel/parser 源碼爲例:

getTokenFromCode(code) {
  switch (code) {
    case 46:
      this.readToken_dot();
      return;

    case 40:
      ++this.state.pos;
      this.finishToken(10);
      return;

    case 41:
      ++this.state.pos;
      this.finishToken(11);
      return;

    case 59:
      ++this.state.pos;
      this.finishToken(13);
      return;

      // 此處省略...

    case 92:
      this.readWord();
      return;

    default:
      if (isIdentifierStart(code)) {
        this.readWord(code);
        return;
      }

  }

}

readToken_dot() {
  // charCodeAt一個一個向後移
  const next = this.input.charCodeAt(this.state.pos + 1);

  if (next >= 48 && next <= 57) {
    this.readNumber(true);
    return;
  }

  if (next === 46 && this.input.charCodeAt(this.state.pos + 2) === 46) {
    this.state.pos += 3;
    this.finishToken(21);
  } else {
    ++this.state.pos;
    this.finishToken(16);
  }
}

語法解析器

將上一步生成的數組,根據語法規則,轉爲抽象語法樹(Abstract Syntax Tree,簡稱 AST)。
以  const a = 1  爲例,詞法分析中獲得了 const 這樣一個 token,並判斷這是一個關鍵字,根據這個 token 的類型,判斷這是一個變量聲明語句。以 @babel/parser 源碼爲例,執行 parseVarStatement 方法。

parseVarStatement(node, kind, allowMissingInitializer = false) {
    const {
      isAmbientContext
    } = this.state;
    const declaration = super.parseVarStatement(node, kind, allowMissingInitializer || isAmbientContext);
    if (!isAmbientContext) return declaration;


    for (const {
      id,
      init
    } of declaration.declarations) {
      if (!init) continue;


      if (kind !== "const" || !!id.typeAnnotation) {
        this.raise(TSErrors.InitializerNotAllowedInAmbientContext, {
          at: init
        });
      } else if (init.type !== "StringLiteral" && init.type !== "BooleanLiteral" && init.type !== "NumericLiteral" && init.type !== "BigIntLiteral" && (init.type !== "TemplateLiteral" || init.expressions.length > 0) && !isPossiblyLiteralEnum(init)) {
        this.raise(TSErrors.ConstInitiailizerMustBeStringOrNumericLiteralOrLiteralEnumReference, {
          at: init
        });
      }
    }


    return declaration;
  }

經過這一步的處理,最終  const a = 1  會變成如下 ast 結構:

字節碼生成器

字節碼生成器的作用,是將抽象語法樹轉爲 JavaScript 引擎可以執行的二進制代碼。目前,還沒有統一的 JavaScript 字節碼的格式標準,每種 JavaScript 引擎都有自己的字節碼格式。最簡單的做法,就是將語義單位翻成對應的二進制命令。

字節碼解釋器

字節碼解釋器的作用是讀取並執行字節碼。

幾種常見的解析器

Esprima

這是第一個用 JavaScript 編寫的符合 EsTree 規範的 JavaScript 的解析器,後續多個編譯器都是受它的影響

acorn

acorn 和 Esprima 很類似,輸出的 ast 都是符合 EsTree 規範的,目前 webpack 的 AST 解析器用的就是 acorn

@babel/parser(babylon)

babel 官方的解析器,最初 fork 於 acorn,後來完全走向了自己的道路,從 babylon 改名之後,其構建的插件體系非常強大

uglify-js

**用於混淆和壓縮代碼,**因爲一些原因,uglify-js 自己 [內部實現了一套 AST 規範,也正是因爲它的 AST 是自創的,不是標準的 ESTree,es6 以後新語法的 AST,都不支持,所以沒有辦法壓縮最新的 es6 的代碼,如果需要壓縮,可以用類似 babel 這樣的工具先轉換成 ES5。

esbuild

esbuild 是用 go 編寫的下一代 web 打包工具,它擁有目前最快的打包記錄和壓縮記錄,snowpack 和 vite 的也是使用它來做打包工具,爲了追求卓越的性能,目前沒有將 AST 進行暴露,也無法修改 AST,無法用作解析對應的 JavaScript。

babel

下面先介紹下 babel 相關工具庫,以及一些 API,瞭解完這些基礎概念後,我們會利用這些工具操作 AST 來編寫一個 babel 插件。

Visitor

path

scope

scope.bindings 當前作用域內聲明所有變量
scope.path 生成作用域的節點對應的路徑
scope.references 所有的變量引用的路徑
getAllBindings() 獲取從當前作用域一直到根作用域的集合
getBinding(name) 從當前作用域到根使用域查找變量
getOwnBinding(name) 在當前作用域查找變量
parentHasBinding(name, noGlobals) 從當前父作用域到根使用域查找變量
removeBinding(name) 刪除變量
hasBinding(name, noGlobals) 判斷是否包含變量
moveBindingTo(name, scope) 把當前作用域的變量移動到其它作用域中
generateUid(name) 生成作用域中的唯一變量名, 如果變量名被佔用就在前面加下劃線
scope.dump() 打印自底向上的 作用域與變量信息到控制檯

實現 eslint 插件

上面這些概念在我們編寫 babel 插件時會用到,接下來我們來實現一個 eslint 移除 console.log() 插件吧!
先看下 console.log('a') 的 AST 長什麼樣子?

可以看到 console.log('a') 是一個 type 爲 “ExpressionStatement” 的節點,所以我們只需要遍歷 AST,當遇到 type=ExpressionStatement 的節點刪除即可!來一起實現吧~

  1. 引入基礎包
var fs = require("fs");
//babel核心模塊,裏面包含transform方法用來轉換源代碼。
const core = require('@babel/core');
//用來生成或者判斷節點的AST語法樹的節點
let types = require("@babel/types");
  1. 遍歷 AST 節點,babel 插件的語法都是固定的,裏面包含 visitor,我們只需在 visitor 裏面處理 ExpressionStatement 即可,
//no-console 禁用 console
const eslintPlugin = ({ fix }) ={
  // babel插件的語法都是固定的,裏面包含visitor
    return {
      pre(file) {
        file.set('errors'[]);
      },
      // 訪問器
      visitor: {
        CallExpression(path, state) {
          const errors = state.file.get('errors');
          const { node } = path
          if (node.callee.object && node.callee.object.name === 'console') {
            errors.push(path.buildCodeFrameError(`代碼中不能出現console語句`, Error));
            if (fix) {
              path.parentPath.remove();
            }
          }
        }
      },
      post(file) {
        // console.log(...file.get('errors'));
      }
    }
  };
  1. 完整實現:
var fs = require("fs");
//babel核心模塊,裏面包含transform方法用來轉換源代碼。
const core = require('@babel/core');
//用來生成或者判斷節點的AST語法樹的節點
let types = require("@babel/types");

//no-console 禁用 console
const eslintPlugin = ({ fix }) ={
  // babel插件的語法都是固定的,裏面包含visitor
    return {
      pre(file) {
        file.set('errors'[]);
      },
      // 訪問器
      visitor: {
        CallExpression(path, state) {
          const errors = state.file.get('errors');
          const { node } = path
          if (node.callee.object && node.callee.object.name === 'console') {
            errors.push(path.buildCodeFrameError(`代碼中不能出現console語句`, Error));
            if (fix) {
              path.parentPath.remove();
            }
          }
        }
      },
      post(file) {
        // console.log(...file.get('errors'));
      }
    }
  };

core.transformFile("eslint/source.js",{
    plugins: [eslintPlugin({ fix: false })]
}function(err, result) {
    result; // ={ code, map, ast }
    console.log(result.code);
  })
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/_j2lcoNK7XIsAJbmddFMtA