AST(Abstract Syntax Tree)抽象语法树是源代码语法结构的一种抽象表示,它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。在代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码错误提示、代码自动补全等等场景均有广泛的应用,是前端工程化的基石。

可以使用AST Explorer来在线查看一段代码的AST。

核心概念

本文所说所有AST结点类型都是在Bebel中的,因为这是目前最流行的AST工具。

不同的代码类型分别有着不同的AST类型,每种 AST 都有自己的属性,但是它们也有一些公共的属性:

  • type: AST 节点的类型
  • start、end、loc:start 和 end 代表该节点在源码中的开始和结束下标。而 loc 属性是一个对象,有 line 和 column 属性分别记录开始和结束的行列号。
  • leadingComments、innerComments、trailingComments: 表示开始的注释、中间的注释、结尾的注释,每个 AST 节点中都可能存在注释,而且可能在开始、中间、结束这三种位置,想拿到某个 AST 的注释就通过这三个属性。

下面就来展开各种代码对应的AST类型。

字面量 Literal

字面量应该很熟悉,let a = 10这里的10就是一个number类型的字面量,字面量类型的AST类型如下:

  • StringLiteral:字符串字面量,如"string";
  • NumbericLiteral:数字字面量,如123;
  • TemplateLiteral:字母串模板字面量,如`string-${a}`;
  • RegExpLiteral:正则表达式字面量,如/\b[a-z]{1,5}/;
  • BooleanLiteral:布尔值字面量,如true;
  • BigIntLiteral:BigInt字面领,如1.23456n;
  • NullLiteral:空值字面量,null;
  • UndefinedLiteral:为定义值字面量,undefined;
  • ArrayLiteral:数组字面量,如[1,2];
  • ObjectLiteral:对象字面量,如{ a: 1 }

标识符 Identifer

变量名、属性名、参数名等各种声明和引用的名字,都是标识符,Identifier 通常表示为一个字符串,它可以出现在表达式、语句和声明等多种语法结构中。

const a = 1;

function b(x) {
  return x + 1;
}

例如上面这段代码中,属于标识符的有abx

其解析后的AST如下

Program
├── VariableDeclaration (const a = 1)
│   ├── VariableDeclarator
│   │   ├── Identifier (a)
│   │   └── NumericLiteral (1)
│   └── const
└── FunctionDeclaration (function b(x) {...})
    ├── Identifier (b)
    ├── FunctionExpression
    │   ├── Identifier (x)
    │   ├── BlockStatement
    │   │   └── ReturnStatement
    │   │       └── BinaryExpression (x + 1)
    │   └── []
    └── []

语句 Statement

是可以独立执行的单位,比如 break、continue、debugger、return 或者 if 语句、while 语句、for 语句,还有声明语句,表达式语句等。

常见的语句类型如下

语句 AST类型
break; BreakStatement
continue; ContinueStatement
return; ReturnStatement
debugger; DebuggerStatement
throw Error(); ThrowStatement
{} BlockStatement
try {} catch(e) {} finally{} TryStatement
for (let key in obj) {} ForInStatement
for (let i = 0;i < 10;i ++) {} ForStatement
while (true) {} WhileStatement
do {} while (true) DoWhileStatement
switch (v){case 1: break;default:;} SwitchStatement
label: console.log(); LabeledStatement
with (a){} WithStatement

声明 Declaration

声明语句是一种特殊的语句,在作用域内声明一个变量、函数、class、import、export 等。

声明语句主要用于定义变量,常见的声明语句类型如下:

  • VariableDeclaration:定义变量,const a = 1
  • FunctionDeclaration:函数声明,function f() {};
  • ClassDeclaration:类定义,class Animal {}
  • ImportDeclaration:导入模块,import React from 'react'
  • ExportDefaultDeclaration:默认导出,export default {}
  • ExportNamedDeclaration:具名导出,export {a}
  • ExportAllDeclaration:全部导出,export * from './utils'

表达式 Expression

表达式和语句的区别是:表达式执行完成之后有返回值

例如以下常见的表达式

代码 AST类型
function(){}; FunctionExpression
() => {}; ArrowFunctionExpression
class{}; ClassExpression
a = 1 AssignmentExpression
1 + 2; BinaryExpression
-1; UnaryExpression
a; Identifier
this; ThisExpression
super; Super
a::b; BindExpression

注意这里的是匿名函数和匿名class。

解析AST

除了使用开头提到的在线AST解析工具,我们也可以通过代码来手动解析AST,这就要用到了前端工程化的基础工具——Babel。

首先安装必要的依赖包

$ pnpm install @babel/parser @babel/traverse @babel/generator @babel/types @babel/core -D
  • @babel/parser:Babel 的 JavaScript 解析器,用于将 JavaScript 代码解析为 AST。它支持最新的 ECMAScript 标准,包括 ECMAScript 2022 中的特性。
  • @babel/traverse:Babel 的 AST 遍历工具,可以遍历 AST 并执行操作。它可以用于修改 AST,例如添加、修改或删除节点等。
  • @babel/generator:Babel 的 AST 代码生成工具,用于将 AST 转换回 JavaScript 代码。它可以将修改后的 AST 转换回修改过的 JavaScript 代码。
  • @babel/types:Babel 的 AST 节点类型定义库,用于定义 AST 节点类型和节点操作的 API。它包含了 AST 中所有可能的节点类型,以及对这些节点类型的操作和方法。

使用Babel转换代码的过程如下图所示

image-20230507202134922

我们分别来看一下不同的模块都有怎么样的效果,我们将利用下面这段代码来进行练习

function func(p: string) {
  console.log(p);
  return p + 'a'
}

let a = func('1');

console.log(a);

我们要把这段代码中的console.log打印的内容添加所在位置的行列号

@babel/parser

使用@babel/parser来解析这一段代码

import { parse } from '@babel/parser';
import {readFileSync} from 'node:fs';
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'

// ESM 环境下构造__dirname
const __dirname = dirname(fileURLToPath(import.meta.url))
// 导入源码
const sourceCode = readFileSync(resolve(__dirname, './sourceCode.ts'), 'utf-8');
// 解析
const ast = parse(sourceCode, {
  plugins: ['typescript']
})

@babel/traverse

使用@babel/traverse来遍历转化所有的console.log调用,注意每次替换节点之后需要使用skip方法跳过当前节点,因为替换节点之后还会再次遍历新的节点,避免触发死循环。

注意,真正的traverse需要通过.default调用。

import traverse from '@babel/traverse';

traverse.default(ast, {
  CallExpression(path) {
    if (path.get('callee').matchesPattern('console.log')) {
      // 获取行列号
      const loc = path.node.loc;
      const lineNumberNode = loc
        ? types.stringLiteral(`[${loc.start.line}, ${loc.start.column}] `)
        : types.stringLiteral('(unknown location): ');
      const newArguments = [lineNumberNode, ...path.node.arguments];
      const newCallExpression = types.callExpression(types.memberExpression(types.identifier('console'), types.identifier('log')), newArguments);
      // 替换原有的节点
      path.replaceWith(newCallExpression);
      // 跳过当前节点,避免死循环
      path.skip()
    }
  },
})

@babel/generator

最后需要把转化后的AST进行编码,这个过程需要使用@babel/generator这个包,同样的也需要通过 .default调用。

import generator from '@babel/generator';

// 将修改后的 AST 转换回代码
const { code } = generator.default(ast, {
  sourceMaps: true,
}, sourceCode);

最终实现的效果如下

image-20230508222205232

以上这三个包分别提供了Babel在不同阶段的能力,可以通过@babel/core来统一调用这些功能。

如果在TS中使用Babel,通过安装@types/babel__xx来安装各个包的类型支持,例如@babel/traverse的类型支持为@types/babel__traverse

除了这三个提供基本能力的包之外,Babel还有其他的工具,可以去babel官网的工具目录进行查看。


前端小白