AST概念
AST(Abstract Syntax Tree) ,抽象语法树,或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式。
为什么要好好的源代码搞成树状的形式?因为统一的结构化的形式,有助于对源代码进行后续分析和处理。
比如下面这句代码:
import xx from 'jq'
经过AST转换后,我们得到这样的形式:
{"type": "Program","start": 0,"end": 19,"body": [{"type": "ImportDeclaration","start": 0,"end": 19,"specifiers": [{"type": "ImportDefaultSpecifier","start": 7,"end": 9,"local": {"type": "Identifier","start": 7,"end": 9,"name": "xx"}}],"source": {"type": "Literal","start": 15,"end": 19,"value": "jq","raw": "'jq'"}}],"sourceType": "module"}
简单的看下这个结构不难发现,似乎将我们的每一个词语都拆开了,用了很多描述的属性来描述。比如,body中现在只有一个对象(因为我们只有一句话暂时)这个对象的type:ImportDeclaration顾名思义就是Import的声明语句,然后我们看他的specifiers,标识符也有type: ImportDefaultSpecifier,import的default标识符(显然,如果我们引入不是default引入而是{xx}, 其type会变成ImportSpecifier),然后local字段表示在当前文件的变量名,比如我们这个xx,类型就是Identifier, 名字就是xx。后面继续看source,就是这个Import的声明语句的被引入源的信息。
还有一点值得注意的是,每一处要点信息都有start、end表示在这句字符串中的位置信息。这为处理字符串提供了很大的方便。
词法分析和语法分析
上面就一句代码,生成AST如此庞大,虽然啰嗦,但是全面。那么,AST 如何生成?
生成大致分为两个核心步骤:
- 词法分析 (Lexical Analysis):扫描输入的源代码字符串,生成一系列的词法单元 (tokens)。这些词法单元包括数字,标点符号,运算符等。词法单元之间都是独立的,暂时不考虑如何组合以及组合的含义。
 - 语法分析 (Syntax Analysis):建立分析语法单元之间的关系,这个阶段就能得到完整的AST了。
 
上面的说法比较官方,不直观,借用一个通俗的例子(参考自这里)
比如分析:它是猪
在词法分析阶段,得到孤立的token:
[{type: '主语', value: '它'},{type: '谓语', value: '是'}{type: '宾语', value: '猪'}]
然后经过语法分析之后,生成有意义的结构化的树:
{type: '语句',body:[{type: '陈述句',declaration: [{type: '声明',id: {type: 'Identifier',name: '它'},init: {type: 'Literal',name: '猪',raw: '猪'}}]}]}
这里只是一个DEMO,真的的AST规范和说明可以参见这篇文章:https://juejin.cn/post/6844903450287800327#heading-3
AST有什么用
AST的作用一般不会体现在业务代码之中,但却它隐式存在在前端程序员开发的各个方面:
- IDE 的错误提示、代码美化、代码高亮、代码自动补全等
 - eslint对代码错误或风格的检查等
 - webpack、rollup 进行代码打包等
 - TypeScript、JSX 等转化为原生 Javascript
 - bable转化高版本JS为低版本
 - vue 模板编译
 
相关的工具和库
astexplorer
https://astexplorer.net/
这是一个在线的AST转换工具,将源代码输入到左边,右边就会显示转换之后的AST。而且源语言不限于JS,转换目标格式默认是acorn,也可以选择。
babel-types
这是一篇文档,列出了AST中的类型
https://babeljs.io/docs/en/babel-types
常用类型(参考自:这里 ):
| 类型名称 | 中文译名 | 描述 | 
|---|---|---|
| Program | 程序主体 | 整段代码的主体 | 
| VariableDeclaration | 变量声明 | 声明变量,比如 let const var | 
| FunctionDeclaration | 函数声明 | 声明函数,比如 function | 
| ExpressionStatement | 表达式语句 | 通常为调用一个函数,比如 console.log(1) | 
| BlockStatement | 块语句 | 包裹在 {} 内的语句,比如 if (true) { console.log(1) } | 
| BreakStatement | 中断语句 | 通常指 break | 
| ContinueStatement | 持续语句 | 通常指 continue | 
| ReturnStatement | 返回语句 | 通常指 return | 
| SwitchStatement | Switch 语句 | 通常指 switch | 
| IfStatement | If 控制流语句 | 通常指 if (true) {} else {} | 
| Identifier | 标识符 | 标识,比如声明变量语句中 const a = 1 中的 a | 
| ArrayExpression | 数组表达式 | 通常指一个数组,比如 [1, 2, 3] | 
| StringLiteral | 字符型字面量 | 通常指字符串类型的字面量,比如 const a = ‘1’ 中的 ‘1’ | 
| NumericLiteral | 数字型字面量 | 通常指数字类型的字面量,比如 const a = 1 中的 1 | 
| ImportDeclaration | 引入声明 | 声明引入,比如 import | 
babel
babel作为转换代码转换工具,原理也是利用了AST。babel自身也有AST相关的工具:
- @babel/parser:转换工具, https://babeljs.io/docs/en/babel-parser
 - @babel/traverse: 遍历工具,https://babeljs.io/docs/en/babel-traverse
 - @babel/generator:反向生成代码,AST -> js code, https://babeljs.io/docs/en/babel-generator
 
下面的代码演示了如何去除源码中的debugger, 思路是利用parser.parse生成AST,利用traverse在遍历的过程中,删除DebuggerStatement类型的tree节点,最终再通过generator.default反向生成JS的代码:
const parser = require('@babel/parser');const traverse = require("@babel/traverse");const generator = require("@babel/generator");// 源代码const code = `function fn() {console.log('debugger')debugger;}`;// 1. 源代码解析成 astconst ast = parser.parse(code);// 2. 转换const visitor = {// traverse 会遍历树节点,只要节点的 type 在 visitor 对象中出现,变化调用该方法DebuggerStatement(path) {// 删除该抽象语法树节点path.remove();}}traverse.default(ast, visitor);// 3. 生成const result = generator.default(ast, {}, code);console.log(result.code)// 4. 日志输出// function fn() {// console.log('debugger');// }
esprima
这是一个解析code为AST的库
https://esprima.org/doc/index.html#
下面代码演示了利用esprima解析AST,利用estraverse遍历AST的,利用escodegen反向生成js code的代码,代码把const变成var:
const esprima = require('esprima');const estraverse = require('estraverse');const escodegen = require('escodegen');let code = 'const a = 1';const ast = esprima.parseScript(code);estraverse.traverse(ast, {enter: function (node) {node.kind = "var";}});const transformCode = escodegen.generate(ast);console.log(transformCode);
recast
https://github.com/benjamn/recast
// parse.jsconst recast = require('recast')const code = `function add (a, b) {return a + b}`const ast = recast.parse(code)// 获取代码块 ast 的第一个 body,即我们的 add 函数const add = ast.program.body[0]console.log(add)// print:// FunctionDeclaration {// type: 'FunctionDeclaration',// id: Identifier...,// params: [Identifier...],// body: BlockStatement...// }
这个库的方法很多,除了能建立AST之外,利用recast.types.builders可以操作修改AST,或者利用recast.visit遍历AST树。
recast的用法参考官网或者这些文章:
https://juejin.cn/post/6844903910960791566#heading-3
https://segmentfault.com/a/1190000016231512
jscodeshift
https://github.com/facebook/jscodeshift
jscodeshift封装了recast,比之API设计的更为友好一点,可以比较方便的对AST中的节点进行增删查改,可以参照这篇文章:https://juejin.cn/post/6942016231214055454#heading-12
acorn
acorn是一个解析AST的库,解析的结果符合 The Estree Spec 规范(这是 Mozilla 的工程师给出的 SpiderMonkey 引擎输出的 JavaScript AST 的规范文档,也可以参考:SpiderMonkey in MDN)。
webpack、rollup解析AST都用的是acorn。
https://github.com/acornjs/acorn
这里有一文介绍acorn可以参考:https://juejin.cn/post/6844903450287800327#heading-1
我们使用acorn,一句acorn.parse就搞定了
const ast = acorn.parse( code, {locations: true,ranges: true,sourceType: 'module',ecmaVersion: 8});
不过需要做点什么的时候,需要便利AST,不过acorn不提供遍历的方法(有acorn-walk),不过我们可以尝试自己写一下,假设我们期望这样遍历ast :
ast.body.forEach(statement =>walk(statement, {enter(node) {// enter},leave(node) {// leave}});});
那么实现下walk就是这样(这是rollup_v0.31 /src/ast/walk.js的方法):
function walk(ast, { enter, leave }) {visit(ast, null, enter, leave)}function visit(node, parent, enter, leave) {if (enter) {enter.call(null, node, parent)}let keys = Object.keys(node).filter(key => typeof node[key] === 'object');keys.forEach(key => {let value = node[key];if (Array.isArray(value)) {value.forEach(val => {visit(val, node, enter, leave);});} else if (value && value.type) {visit(value, node, enter, leave);}});if (leave) {leave(node, parent)}}
其他和参考
很遗憾,有些参考文章的demo只能简单记录下要点而不能亲自实践了。
案例:jscodeshift + commander 实现命令行处理css后缀名
参考文章:https://juejin.cn/post/6942016231214055454#heading-17
在对 jscodeshift 有了初步了解之后,我们接下来做一个命令行工具来解决我在上面提出的“引入样式文件后缀名问题”,接下来会简单使用到 commander ,它使 nodejs 命令行接口变得更简单~ import ‘./style.scss’ ,全部转换成以 .css
案例:开发babel-loader
参考文章:https://juejin.cn/post/6844903910960791566#heading-15
AST遍历
这篇文章专门介绍AST的遍历和遍历工具:https://juejin.cn/post/7016711681158053925
介绍了一种通用的遍历AST的思路:visitorKeys,另外,分析了 babel、eslint、tsc、estraverse、postcss 这些工具都是怎么遍历 AST 的。
