AST 入门

253 阅读6分钟

问题背景

如何获取js的依赖关系 image.png

image.png

AST - 抽象语法树

image.png

是源代码语法结构的一种抽象表示。它以 树状 的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。比如,会省略掉一些无具体意义的分隔符如 ;{} 等等。

列如以下代码:

function square(n) { 
    return n * n; 
}

生成:

image.png

通过fs读取文件

  1. 通过正则匹配内容“./foo.js”
  2. 通过ast获取内容“./foo.js”

用途

AST在前端无处不在,我们熟悉的开发工具几乎全依赖于AST进行开发:WebpackBabelEslintPrettier。如果你想了解 JavaScript 编译执行的原理并掌握其精髓,那么你就必须得了解 AST。

image.png

ast的生成过程

1. 词法分析

主要是将字符流(char stream)转换为令牌流(token stream),简单来说就是在源代码的基础上进行分词

let a = 10;
===>
let` 、`a`、`=`、`10`、 `;

词法分析器 从左往右逐个字符扫描分析 整个程序的字符串,当遇到不同的字符时,会驱使它迁移到不同的状态。在扫描字符的时候,遇到 c 字母,如果后面还有字符,将继续扫描,直到遇到空格,识别出 const,发现是一个关键字,将其生成词法单元 { type: 'Keyword', value: 'const' },然后接着扫描,以此类推,生成 Token List。 列如:

[ 
    { type: 'Keyword', value: 'const' }, 
    { type: 'Identifier', value: 'a' }, 
    { type: 'Punctuator', value: '=' }, 
    { type: 'Numeric', value: '10' }, 
    { type: 'Punctuator', value: ';' }, 
];

通过上面分解出来的词法分析结果,我们可以观察到,缺少一些比较关键的信息:

  • 没有任何语法信息;
  • 体现不了代码的执行顺序;
2. 语法分析

image.png

符号表填项:扫描源程序,遇到一个名字就检索表,若它是一个新的名字,就填表,在调用词法语法分析程序时,还要把有关的信息填入

语法分析会将词法分析出来的 词法单元 转化成有语法含义的 抽象语法树结构(AST) 。同时,验证语法,语法如果有错的话,将抛出语法错误。

词法分析和语法分析不是完全独立的,而是交错进行的,也就是说,词法分析器不会在读取所有的词法记号后再使用语法分析器来处理。通常情况下,每取得一个词法记号,就将其送入语法分析器进行分析。

节点
{ type: "FunctionDeclaration", id: {...}, params: [...], body: {...} }

ast的每一层结构,被称为 节点(Node ,每一个节点都包含 type 属性,用于表示节点类型,比如:FunctionDeclarationIdentifierBinaryExpression 等等。除此之外,Babel 还为每个节点额外生成了一些属性,用于描述该节点在原始代码中的位置,比如: startendloc

节点类型

主要分为以下几个大类:字面量标志符语句声明表达式注释 等等

1. 字面量 Literal

类型名称中文译名描述
StringLiteral字符型字面量通常指字符串类型的字面量:'Hello, AST!'
NumericLiteral数值型字面量通常指数字类型的字面量:123
BooleanLiteral布尔型字面量通常指布尔类型值:true / false
RegExpLiteral正则型字面量通常指正则表达式:/[0-9]/
TemplatLiteral模板型字面量通常指模板字符串(`)

2. 标志符Identifier

程序中所有的 变量名、函数名、对象键(key) 以及函数中的参数名,都属于标志符(Identifier)。

3. 语句Statement

语句是能够独立执行的基本单位,常见的语句类型有:

类型名称中文译名描述
IfStatementIf 控制流语句通常指 if (true) {} else {}
ForInStatementFor-in 循环语句通常指 for(let key in obj) {}
SwitchStatementSwitch 语句通常指 switch
WhileStatementWhile 循环语句通常指 while(true) {}
ForStatementFor 循环语句通常指 for(let i = 0; i < 10; i++) {}
BreakStatement中断语句通常指 break
ContinueStatement持续语句通常指 continue
ReturnStatement返回语句通常指 return
BlockStatement块语句包裹在 {} 内的语句
ExpressionStatement表达式语句通常为调用一个函数,比如 console.log(1)

4. 声明 Declaration

声明语句是一种特殊的语句,它执行的逻辑是在作用域内声明一个 变量、函数、classimportexport 等。

类型名称中文译名描述
VariableDeclaration变量声明const a = 10;
FunctionDeclaration函数声明function sum() {}
ImportDeclaration模块引入声明import { reactive } from 'vue;'
ExportDefaultDeclaration模块默认导出声明export default a = 10;

5. 表达式 Expression

表达式的特点是执行完以后有返回值,这是和语句 (statement) 的区别

类型名称中文译名描述
ArrayExpression数组表达式通常指一个数组:[1, 2, 3]
ArrowFunctionExpression箭头函数表达式() => {}
AssignmentExpression赋值表达式通常指为一个变量赋值,比如 a = 1
BinaryExpression二元表达式1 + 2
UnaryExpression一元表达式-1
FunctionExpression函数表达式function(){}

6. Comment & Program

类型名称中文译名描述
Program程序主体整段代码的主体
CommentBlock块级注释/* 块级注释 */
CommentLine单行注释// 单行注释

在线生成地址

babel 中的使用

@babel/parser
// → 导入模块 const parser = require('@babel/parser'); 

// → 定义一段代码字符串 
const codeString = `function square(n) { return n * n; }`; 

// → 解析代码字符串 
const ast = parser.parse(codeString, { 
    sourceType: 'script', // module unambigious 
    plugins: ['jsx', 'typescript'], 
});
@babel/traverse
const traverse = require('@babel/traverse');

// → 遍历节点 
traverse.default(ast, { 
    Identifier(path) { // 判断是否是 name 为 n 的标志符 
        if (path.node.name === 'codeString') { 
            path.node.name = 'codeObj'; 
        } 
    }
});
Path(路径)

AST 通常会有许多节点,我们可以通过 path 对象表示节点之间的关联关系,通过这个对象提供的属性方法,我们可以 操作 AST 语法树。

属性:

  • path.node:当前遍历到的 node 节点,可通过它访问节点上的属性,对于 Ast 节点;

  • path.parent:父级 node,无法进行替换;

  • path.parentPath:父级 path,可进行替换;

  • path.scope:作用域相关,可用于变量重命名,变量绑定关系检测等;

  • path.key:获取路径所在容器的索引

  • path.listKey:获取容器的 key

  • path.container:获取路径的容器(包含所有同级节点的数组)

  • path.inList:判断路径是否有同级节点

方法:

  • path.toString():当前路径所对应的源代码;

  • path.isXXXXXX 为节点类型,可以判断是否符合节点类型。比如我们需要判断路径是否为 StringLiteral 类型 → path.isStringLiteral

  • path.get(key):获取子节点 path,例如:path.get('body.0') 可以理解为 path.node.body[0]这样的形式,让我们更加方便的拿到子路径,但是注意仅路径可以这样操作,访问属性是不允许的!

  • path.set(key):设置子节点 path;

  • path.remove():删除 path;

  • path.replaceWith():用AST节点替换该节点,如:path.replaceWith({ type: 'NumericLiteral', value: 3 }),创建节点可以使用 @babel/types,如果是多路径则使用 relaceWithMultiple([AST...]);

  • path.replaceWidthSourceString(): 用字符串替换源码

  • path.find((path) => path.isObjectExpression()) :向下搜寻节点

  • path.findParent():向父节点搜寻节点

  • path.getSibling()path.getNextSibling()path.getPrevSibling(): 获取兄弟路径;

  • path.getFunctionParent(): 向上获取最近的 Function 类型节点;

  • path.getStatementParent(): 向上获取最近的 Statement 类型节点;

  • path.insertBefore():在之前插入兄弟节点

  • path.insertAfter(): 在之后插入兄弟节点

  • path.pushContainer(): 将AST push到节点属性里面

  • path.traverse(): 递归的形式消除全局状态

  • path.stop(): 停止遍历

  • path.skip(): 不往下遍历,跳过该节点

@babel/generate
const generator = require('@babel/generator');

generator.default(ast).code;
@babel/core
// → 同步方法 
transformSync(code, options); // => { code, map, ast } 
transformFileSync(filename, options); // => { code, map, ast } 
transformFromAstSync(parsedAst, sourceCode, options); // => { code, map, ast }

// → 异步方法 
transformAsync('code();', options).then((result) => {}); 
transformFileAsync('filename.js', options).then((result) => {}); transformFromAstAsync(parsedAst, sourceCode, options).then((result) => {});

demo1

插件扩展

语法结构

image.png

demo2