学习抽象语法树 AST
前言
作为一个前端切图仔,工作中少不了用上 Babel,ESLint,Prettier 这些工具,但是这些工具背后的工作原理却不求甚解😅 ,最近才尝试去了解一番,没想到发现一个全新的世界,那就是 AST
中文名抽象语法树。
初识
在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法架构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真是语法中出现的每个细节。比如嵌套括号被隐含在树的结构中,并没有以节点的形式呈现;二类似于
if-condition-then
这样的条件跳转语句,可以使用带有三个分支的节点来表示。
和抽象语法树相对的是具体语法树(通常称作分析树)。一般的,在源代码的翻译和编译过程中,语法分析器创建出分析树,然后从分析树生成AST。一旦AST被创建出来,在后续的处理过程中,比如语义分析阶段,会添加一些信息。
让我们从一个全能的函数开始,它返回生命、宇宙以及任何事情的终极答案
1 | function ask() { |
首先它是一个函数声明,函数名称为 ask
,函数体内定义了一个常量 answer 值为 42,最后返回 answer。把它输入到一个神奇的网站 astexplorer,神秘的 AST
终于被揭开了面纱。
可以看出 AST
就是一个自上而下的树形结构,每一层有一个或多个节点组成,每个节点有一个 type
属性表示节点的类型,如 “FunctionDeclaration”, “BlockStatement”, “VariableDeclaration”,以及节点的其他属性。(节点的类型定义在 ESTree 这个仓库,它包括了 es5 到最新的 js 语法定义)。下图可以更清晰的看出这个函数 AST
的结构,至此我们对 AST
有了初步的认识。
解析
要得到代码的 AST
,首先需要对代码进行解析。解析阶段接受源码并输出 AST
,它使用一个解析器对源码进行词法分析和语法分析。 词法分析将字符串形式的代码转换为一个语法片段数组 Tokens ,语法分析阶段把 *Tokens *转换成 AST
形式。
词法分析
Tokens 是一个数组,由代码语句的碎片组成,它们可以是数字、标签、标点符号、运算符,或者其它任何东西。
1 | // 源码 |
语法分析
语法分析阶段把 *Tokens * 数组转换成 AST
的形式便于后续操作,详细操作可以查看这里的代码。
遍历
有了 AST
就可以对这棵树进行从上到下的递归遍历,过程中访问树的节点,这里使用了一种设计模式 访问者模式,通过创建一个访问者 visitor
对象,这个对象中包括一些方法,在遍历 AST
过程中进行匹配,匹配成功就调用访问者的方法。
通过访问节点可以对源码进行语法检查,ESLint 就是基于此,以下是检测语法的规则示例(关于编写 ESLint 规则的详细内容可以查看此链接)
限制函数参数数量
思路是匹配类型为 FunctionDeclaration
的节点,FunctionDeclaration
表示这个节点为函数声明,如果函数的参数数量大于 3 个就进行提示。
1 | export default function (context) { |
效果如下
限制嵌套的条件语句
匹配类型为 IfStatement
的节点,如果它的第一个子节点还是 IfStatement
就进行提示。
1 | export default function (context) { |
修改
在遍历 AST
过程中可以对树的节点进行修改,如添加,移动,替换这些节点,也可以生成全新的 AST
。
Babel 的作用就是修改 AST
上的节点,从而到达修改代码的效果。一个 Babel 插件是一个接收 babel
对象作为参数的函数,返回一个带有 visitor
属性的对象。visitor
对象中的每个函数接受 path
和 state
参数,以下是编写 Babel 插件示例,关于编写 Babel 插件可以查看此链接。
1 | // Babel 插件 |
将 **
语法转换为 Math.pow
1 | // Before |
- 找到
**
语法所在位置 - 获取左右操作数
- 创建
Math.pow
语句,替换原节点
1 | export default function (babel) { |
修改工具函数引入方式
1 | // Before |
- 找到
lodash
的import
节点 - 遍历所有的引入值,获取引用的
name
属性 - 插入新生成的
import
节点 - 删除原节点
1 | export default function (babel) { |
生成
就是根据 AST
输出代码,下面通过两个工具说明。
Jscodeshift
jscodeshift 是一个 Facebook 开源的用来对 JavaScript 或者 TypeScript 文件运行转换的工具,它的目的是更方便的批量修改代码。它通过接受 *transformer *对源码进行转换,一个 *transformer *就是一个接受 fileInfo
, api
, options
参数并返回源码的函数。
1 | module.exports = function(fileInfo, api, options) { |
示例,将 <React.Fragment>
转换成 <>``</>
语法,思路是找到 name
属性为 Fragment
的节点,然后将它的父节点清除。
1 | export default function transformer(file, api) { |
更多示例可查看这个链接
Gogocode
gogocode 是最近发现的一个 AST
处理工具,号称全网最简单易上手,可读性最强,提供的示例提供类似于 jQuery 的 API。
一个替换变量名例子
1 |
|
总结
本文解释了什么是 AST
抽象语法树,如何获得代码的 AST
,以及对 AST
进行遍历,修改和生成,利用 AST
我们可以开发的代码工具。
相关链接
Awesome AST
The ESTree Spec
Babel Handbook
The Super Tiny Compiler
Working with Rules
jscodeshift
GOGOCODE