AST的简单实践

本文首发于 hzzly的博客javascript

原文连接:AST的简单实践前端

什么是AST(抽象语法树)?

It is a hierarchical program representation that presents source code structure according to the grammar of a programming language, each AST node corresponds to an item of a source code.vue

AST是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每一个节点都表示源代码中的一种结构。java

AST是一个很是基础可是同时很是重要的知识点,咱们熟知的 TypeScript、babel、webpack、vue-cli 都是依赖 AST 进行开发的。node

这里咱们就以 babel 为例来实践一下 AST。webpack

Babel运行原理

Babel 做为当今最为经常使用的 JavaScript 编译器,在前端开发中扮演着极为重要的角色。大多数状况下,Babel 被用来转译 ECMAScript 2015+ 至可兼容浏览器的版本。web

Babel 的三个主要处理步骤分别是:vue-cli

  • 解析(parse)
  • 转换(transform)
  • 生成(generate)

Babel处理步骤

整个过程当中,parsing和generation是固定不变的,最关键的是transforming步骤,经过babel插件来支持,这是其扩展性的关键。npm

这三个阶段分别由 @babel/parser、@babel/core、@babel/generator 执行。Babel 本质上只是一个代码的搬运工,若是不给 Babel 装上插件,它将会把输入的代码原封不动地输出。正是由于有插件的存在, Babel 才能将输入的代码进行转变,从而生成新的代码。编程

解析

输入JS源码,输出AST

parsing(解析),对应于编译器的词法分析,及语法分析阶段。输入的源码字符序列通过词法分析,生成具备词法意义的token序列(可以区分出关键字、数值、标点符号等),接着通过语法分析,生成具备语法意义的AST(可以区分出语句块、注释、变量声明、函数参数等)。

利用 @babel/parser 对源代码进行解析 获得 AST。

栗如:

console.log(info)
复制代码

通过parsing后,生成的AST以下:

{
  "type": "CallExpression",
  "callee": {
    "type": "MemberExpression",
    "object": {
      "type": "Identifier",
      "loc": {
        "identifierName": "console",
      },
      "name": "console",
    },
    "property": {
      "type": "Identifier",
      "loc": {
        "identifierName": "log",
      },
      "name": "log",
    }
  },
  "arguments": [
    "Identifier": {
      "type": "Identifier",
      "loc": {
        "identifierName": "log",
      },
      "name": "info",
    }
  ]
}
复制代码

🔥Tip: JS代码对应的AST结构能够经过AST Explorer工具查看

仔细的小伙伴可能就会发现从咱们的源代码到AST的过程其实就是一个分词的过程,将咱们的 console.log(info) 分红 console、log、info。

有了这个 AST 树结构,咱们就能进行语义层面转换了。

转换

输入AST,输出修改过的AST

利用 @babel/traverse 对 AST 进行遍历,并解析出整个树的 path,经过挂载的 metadataVisitor 读取对应的元信息,这一步叫 set AST 过程。

@babel/traverse 是一款用来自动遍历抽象语法树的工具,它会访问树中的全部节点,在进入每一个节点时触发 enter 钩子函数,退出每一个节点时触发 exit 钩子函数。开发者可在钩子函数中对 AST 进行修改。

import traverse from "@babel/traverse";

traverse(ast, {
  enter(path) {
    // 进入 path 后触发
  },
  exit(path) {
    // 退出 path 前触发
  },
});
复制代码

transforming(转换),对应于编译器的机器无关代码优化阶段(稍微有点牵强,但两者工做内容都是修改AST),对 AST 作一些修改,好比针对上面的 log 增长一些信息方便咱们调试:

console.log(info) => console.log('[info]', info)
复制代码

修改事后的 AST 结构:

{
  "type": "CallExpression",
  "callee": {
    // ....
  },
  "arguments": [
    "StringLiteral": {
      "type": "StringLiteral",
      "value": "'[info]'",
    },
    "Identifier": {
      "type": "Identifier",
      "loc": {
        "identifierName": "log",
      },
      "name": "info",
    }
  ]
}
复制代码

语义层面的转换具体而言就是对AST进行增、删、改操做,修改后的AST可能具备不一样的语义,映射回代码字符串也不一样

生成

输入AST,输出JS源码

generation(生成),对应于编译器的代码生成阶段,把AST映射回代码字符串。

利用 @babel/generator 将 AST 树输出为转码后的代码字符串。

实践

说了这么多接下来咱们就用代码实践一下上面的例子

相关npm包

  • @babel/parser 解析输入源码,建立AST
  • @babel/traverse 遍历操做AST
  • @babel/generator 把AST转回JS代码
  • @babel/types AST操做工具库

代码

const parser = require('@babel/parser');
const traverse = require('@babel/traverse');
const generate = require('@babel/generator');
const t = require('@babel/types');

function compile(code) {
  // 1. parse
  const ast = parser.parse(code);

  // 2. traverse
  const visitor = {
    CallExpression(path) {
      const { callee, arguments } = path.node;
      if (
        t.isMemberExpression(callee)
        && callee.object.name === 'console'
        && callee.property.name === 'log'
        && arguments.length > 0
      ) {
        const variableName = arguments[0].name;
        path.node.arguments.unshift(
          t.StringLiteral(`[${variableName}]`)
        )
      }
    },
  };
  traverse.default(ast, visitor);

  // 3. generate
  return generate.default(ast, {}, code);
}

const code = `console.log(info)`;

const result = compile(code);
console.log(result.code);
复制代码

总结

看到这,咱们的 AST 实践也告一段落了。固然,文章所讲的只是一个简单的例子,但基本的原理思路八九不离十,更多的类型还得本身去探究。总之,掌握好 AST,你真的能够作不少事情。

相关文章
相关标签/搜索