走进AST

微信扫一扫,分享到朋友圈

走进AST

前言:AST已经深入的存在我们项目脚手架中,但是我们缺不了解他,本文带领大家一起体验AST,感受一下解决问题另一种方法

什么是AST

在讲之前先简单介绍一下什么AST,抽象语法树( Abstract Syntax Tree )简称 AST ,是源代码的抽象语法结构的树状表现形式。

平时很多库都有他的影子:

例如 babel , es-lint , node-sass , webpack 等等。

OK 让我们看下代码转换成 AST 是什么样子。

const ast = 'tree'

这是一行简单的声明代码,我们看下他转换成AST的样子

我们发现整个树的根节点是 Program ,他有一个子节点 bodybody 是一个数组,数组中还有一个子节点 VariableDeclarationVariableDeclaration 中表示 const ast = 'tree' 这行代码的声明,具体的解析如下:

type: 描述语句的类型,此处是一个变量声明类型
kind: 描述声明类型,类似的值有'var' 'let'
declarations: 声明内容的数组,其中每一项都是一个对象
------------type: 描述语句的类型,此处是一个变量声明类型
------------id: 被声明字段的描述
----------------type: 描述语句的类型,这里是一个标识符
----------------name: 变量的名字
------------init: 变量初始化值的描述
----------------type: 描述语句的类型,这里是一个标识符
----------------name: 变量的值

大体上的结构是这样,body下的每个节点还有一些字段没有给大家说明,例如:位置信息,以及一些没有值的key都做了隐藏,推荐大家可以去 asteplorer 这个网站去试试看。

总结一下, AST就是把代码通过编译器变成树形的表达形式。

如何生成AST

如何生成把纯文本的代码变成AST呢?编辑器生成语法树一般分为三个步骤

  • 词法分析
  • 语法分析
  • 生成语法树
  1. 词法分析:也叫做 扫描 。它读取我们的代码,然后把它们按照预定的规则合并成一个个的标识tokens。同时,它会移除空白符,注释,等。最后,整个代码将被分割进一个tokens列表(或者说一维数组)。

比方说上面的例子 const ast = 'tree' ,会被分析为 const、ast、=、'tree'

const ast = 'tree';
[
{ type: 'keyword', value: 'const' },
{ type: 'identifier', value: 'a' },
{ type: 'punctuator', value: '=' },
{ type: 'numeric', value: '2' },
]

当词法分析源代码的时候,它会一个一个字母地读取代码,所以很形象地称之为扫描-scans;当它遇到空格,操作符,或者特殊符号的时候,它会认为一个话已经完成了。

2.语法分析:也称为解析器。它会将词法分析出来的数组转化成树形的表达形式。同时,验证语法,语法如果有错的话,抛出语法错误。

3.生成树:当生成树的时候,解析器会删除一些没必要的标识tokens(比如不完整的括号),因此AST不是100%与源码匹配的,但是已经能让我们知道如何处理了。说个题外话,解析器100%覆盖所有代码结构生成树叫做CST(具体语法树)

能否通过第三方库来生成?

有很多的第三方库可以用来实战操作,可以去 asteplorer 这个网站去找你喜欢的第三方库,这里不限于 javascript ,其他的语言也可以在这个网站上找到。

如图:

关于 javascript 的第三方库,这里给大家推荐 babel 的核心库 babylon

// yarn add babylon
import * as babylon from 'babylon';
const code = `
const ast = 'tree'
`
const ast = babylon.parse(code); // ast

如何实践

ok,现在我们已经知道如何把我们的代码变成 AST 了,但是现实中,我们经常会使用到代码的转换,比方说 jsx -> js, es6 -> es5, 是的就是 babel ,我们来看看 babel 是如何转换代码的。

大体上 babel 转换代码分为三步

1. 通过`babylon`生成`AST`
2. 遍历`AST`同时通过指定的访问器访问需要修改的节点
3. 生成代码

看一个简单的例子一起理解一下

生成 AST

import * as babylon from 'babylon';
// 这里也可以使用 import parser from '@babel/parser'; 这个来生成语法树
const code = `
const ast = 'tree'
console.log(ast);
`
const ast = babylon.parse(code); // ast

遍历 AST 同时通过访问器 CallExpression 来访问 console.log(ast) 并删除它

import traverse from '@babel/traverse'
import t from '@babel/types';
// 2 遍历
const visitor = {
CallExpression(path) {
const { callee } = path.node;
if (
t.isMemberExpression(callee) &&
callee.object.name === 'console' &&
callee.property.name === 'log'
) {
path.remove();
}
},
}
traverse.default(ast, visitor);

生成新代码

import generator from '@babel/generator';
generator.default(ast);

简单的答疑:CallExpression表示这是一个调用,为什么还要做更深入的判断呢,因为直接的函数调用 foo() 这也是一个CallExpression,A.foo()这也是一个CallExpression, 所以要更深入的判断

好的,代码转换完成!值得庆祝。我们可以看到 第一步生成 AST 第三步生成新代码 都由 babel 替我们做了,我们真正操作的地方在于第二步:通过访问器操作需要操作的节点。

由此可见我们开发babel-plugin的时候,也只需要关注 visitor 这部分就好。

上述代码改为babel-plugin示例:

module.export = function plugin({ types: t}) {
return {
visitor: {
CallExpression(path) {
const { node } = path;
if (t.isMemberExpression(node.callee) &&
node.callee.object.name === 'console' &&
node.callee.property.name === 'log'
) {
path.remove();
}
},
},
};
}

将这个插件加入到你的 babel 插件列表中,可以看到它真的生效了,一切都是这么简单。so amazing!

结语

开头提到的常用库 prettire , eslint , css-loader 等等其实都是先生成 AST ,然后再操作 AST ,最后在生成代码。只不过操作 AST 的过程很复杂,举一反三在项目里,组件库升级,组件批量替换都可以使用这个思路。甚至可以根据业务做一些自己业务方的 babel-plugin 都行。

感谢您的阅读,有问题可以在评论区交流~

微信扫一扫,分享到朋友圈

走进AST

撞脸领克?奇瑞新车正式定名为“蚂蚁”

上一篇

这就是Windows 10即将推出的磁盘分区管理工具

下一篇

你也可能喜欢

走进AST

长按储存图像,分享给朋友