浅析AST抽象语法树及如何利⽤AST转换JS代码
在学习AST之前,可以结合此篇博客()⼀起看。
抽象语法树(Abstract Syntax Tree)也称为AST语法树,指的是源代码语法所对应的树状结构。也就是说,对于⼀种具体编程语⾔下的源代码,通过构建语法树的形式将源代码中的语句映射到树中的每⼀个节点上。
如果你查看⽬前任何主流的项⽬中的devDependencies,会发现前些年的不计其数的插件诞⽣。我们归纳⼀下有:javascript转译、代码压缩、css预处理器、elint、pretiier,等。有很多js模块我们不会在⽣产环境⽤到,但是它们在我们的开发过程中充当着重要的⾓⾊。所有的上述⼯具,不管怎样,都建⽴在了AST这个巨⼈的肩膀上。
所有的上述⼯具,不管怎样,都建⽴在了AST这个巨⼈的肩膀上。
⼀、JavaScript语法解析
1、什么是AST抽象语法树
It is a hierarchical program representation that presents source code structure according to the grammar of a programming
language, each AST node corresponds to an item of a source code.
估计很多同学看完这段官⽅的定义⼀脸懵逼,可以通过⼀个简单的例⼦来看语法树具体长什么样⼦。有如下代码:
我们可以发现,程序代码本⾝可以被映射成为⼀棵语法树(实际上,真正AST每个节点会有更多的信息。但是,这是⼤体思想。从纯⽂本中,我们将得到树形结构的数据,每个条⽬和树中的节点⼀⼀对应。),⽽通过操纵语法树,我们能够精准的获得程序代码中的某个节点。例如声明语句,赋值语句,⽽这是⽤正则表达式所不能准确体现的地⽅。
JavaScript的语法解析器提供了⼀个,你可以借助于这个⼯具,将JavaScript代码解析为⼀个JSON⽂件表⽰的树状结构。
2、有什么⽤
聊到AST的⽤途,其应⽤⾮常⼴泛,下⾯我简单罗列了⼀些:
IDE的错误提⽰、代码格式化、代码⾼亮、代码⾃动补全等
JSLint、JSHint对代码错误或风格的检查等
webpack、rollup进⾏代码打包等
CoffeeScript、TypeScript、JSX等转化为原⽣Javascript
其实它的⽤途,还不⽌这些,如果说你已经不满⾜于实现枯燥的业务功能,想写出类似react、vue这样的⽜逼框架,或者想⾃⼰搞⼀套类似webpack、rollup这样的前端⾃动化打包⼯具,那你就必须弄懂AST。
抽象语法树的作⽤⾮常的多,⽐如编译器、IDE、压缩优化代码等。在JavaScript中,虽然我们并不会常常与AST直接打交道,但却也会经常的涉及到它。例如使⽤UglifyJS来压缩代码,实际这背后就是在对JavaScript的抽象语法树进⾏操作。在⼀些实际开发过程中,我们也会⽤到抽象语法树,下⾯通过⼀个⼩例⼦来看看怎么进⾏JavaScript的语法解析以及对节点的遍历与操纵。
⼆、如何⽣成AST?
在了解如何⽣成AST之前,有必要了解⼀下Parser(常见的Parser有esprima、traceur、acorn、shift等)。JS Parser其实是⼀个解析器,它是将js源码转化为抽象语法树(AST)的解析器。整个解析过程主要分为以下两个步骤:
分词(也就是词法分析):将整个代码字符串分割成最⼩语法单元数组
语法分析:在分词基础上建⽴分析语法单元之间的关系
1、什么是语法单元?
语法单元是被解析语法当中具备实际意义的最⼩单元,简单的来理解就是⾃然语⾔中的词语。举个例⼦来说,下⾯这段话:“2019年是祖国70周年”,我们可以把这句话拆分成最⼩单元,即:2019年、是、祖国、70、周年。
这就是我们所说的分词,也是最⼩单元,因为如果我们把它再拆分出去的话,那就没有什么实际意义了。js arguments
Javascript 代码中的语法单元主要包括以下这么⼏种:
关键字:例如 var、let、const等
标识符:没有被引号括起来的连续字符,可能是⼀个变量,也可能是 if、else 这些关键字,⼜或者是 true、false 这些内置常量
运算符: +、-、 *、/ 等
数字:像⼗六进制,⼗进制,⼋进制以及科学表达式等语法
字符串:因为对计算机⽽⾔,字符串的内容会参与计算或显⽰
空格:连续的空格,换⾏,缩进等
注释:⾏注释或块注释都是⼀个不可拆分的最⼩语法单元
其他:⼤括号、⼩括号、分号、冒号等
如果我们以最简单的复制语句为例的话,如下:
var a = 1
通过分词,我们可以得到如下结果:
[
{
"type": "Keyword",
"value": "var"
},
{
"type": "Identifier",
"value": "a"
},
{
"type": "Punctuator",
"value": "="
},
{
"type": "Numeric",
"value": "1"
},
{
"type": "Punctuator",
"value": ";"
}
]
2、什么是语法分析?
上⾯我们已经得到了我们分词的结果,需要将词汇进⾏⼀个⽴体的组合,确定词语之间的关系,确定词语最终的表达含义。
简单来说语法分析是对语句和表达式识别,确定之前的关系,这是个递归过程。
上⾯我们通过语法分析,可以得到如下结果:
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "a"
},
"init": {
"type": "Literal",
"value": 1,
"raw": "1"
}
}
],
"kind": "var"
}
],
"sourceType": "script"
}
这就是 var a = 1 所转换的 AST;(这⾥推荐⼀下astexplorer AST的可视化⼯具,,可以直接进⾏对代码进⾏AST转换~)
三、⽰例代码解析AST如何⽤
⼩需求:我们将构建⼀个简单的静态分析器,它可以从命令⾏进⾏运⾏。它能够识别下⾯⼏部分内容:
已声明但没有被调⽤的函数
调⽤了未声明的函数
被调⽤多次的函数
现在我们已经知道了可以将代码映射为AST进⾏语法解析,从⽽到这些节点。但是,我们仍然需要⼀个语法解析器才能顺利的进⾏⼯作,在JavaScript的语法解析领域,⼀个流⾏的开源项⽬是Esprima,我们可以利⽤这个⼯具来完成任务。此外,我们需要借助Node来构建能够在命令⾏运⾏的JS代码。
1、准备⼯作
为了能够完成后⾯的⼯作,你需要确保安装了Node环境。⾸先创建项⽬的基本⽬录结构,以及初始化NPM。
mkdir esprima-tutorial
cd esprima-tutorial
npm install esprima --save
在根⽬录新建index.js⽂件,初试代码如下
var fs = require('fs'),
esprima = require('esprima');
function analyzeCode(code) {
// 1
}
// 2
if (process.argv.length < 3) {
console.log('Usage: index.js file.js');
}
// 3
var filename = process.argv[2];
console.log('Reading ' + filename);
var code = fs.readFileSync(filename);
analyzeCode(code);
console.log('Done');
在上⾯的代码中:
(1)函数analyzeCode⽤于执⾏主要的代码分析⼯作,这⾥我们暂时预留下来这部分⼯作待后⾯去解决。
(2)我们需要确保⽤户在命令⾏中指定了分析⽂件的具体位置,这可以通过查看process.argv的长度来得到。为什么?你可以参考Node的官⽅⽂档:
The first element will be ‘node’, the second element will be the name of the JavaScript file. The next elements will be any
additional command line arguments.
(3)获取⽂件,并将⽂件传⼊到analyzeCode函数中进⾏处理
2、解析代码和遍历AST
借助Esprima解析代码⾮常简单,只要使⽤⼀个⽅法即可:
var ast = esprima.parse(code);
esprima.parse()⽅法接收两种类型的参数:字符串或Node的Buffer对象,它也可以收附加的选项作为参数。解析后返回结果即为抽象语法树(AST),AST遵守Mozilla SpiderMonkey的。例如代码:
var answer = 6 * 7;
解析后的结果为:
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "answer"
},
"init": {
"type": "BinaryExpression",
"operator": "*",
"left": {
"type": "Literal",
"value": 6,
"raw": "6"
},
"right": {
"type": "Literal",
"value": 7,
"raw": "7"
}
}
}
],
"kind": "var"
}
],
"sourceType": "script"
}
代码:6*7,解析结果为:
{
"type": "Program",
"body": [
{
"type": "ExpressionStatement",
"expression": {
"type": "BinaryExpression",
"operator": "*",
"left": {
"type": "Literal",
"value": 6,
"raw": "6"
},
"right": {
"type": "Literal",
"value": 7,
"raw": "7"
}
}
}
],
"sourceType": "script"
}
可以⾃⾏在此解析⼯具⾥试试:/demo/parse.html#
我们可以发现每个节点都有⼀个type,根节点的type为Program。type也是所有节点都共有的,其他的属性依赖于节点的type。例如上⾯实例的程序中,我们可以发现根节点下⾯的⼦节点的类型为EspressionStatement,依此类推。
为了能够分析代码,我们需要对得到的AST进⾏遍历,我们可以借助Estraverse进⾏节点的遍历。执⾏如下命令进⾏安装该NPM包:
npm install estraverse --save
基本⽤法如下:
function analyzeCode(code) {
var ast = esprima.parse(code);
enter: function (node) {
console.pe);
}
});
}
上⾯的代码会输出遇到的语法树上每个节点的类型。
3、获取分析数据
为了完成需求,我们需要遍历语法树,并统计每个函数调⽤和声明的次数。因此,我们需要知道两种节点类型。⾸先是函数声明:
{
"type": "FunctionDeclaration",
"id": {
"type": "Identifier",
"name": "myAwesomeFunction"
},
"params": [
...
],
"body": {
"type": "BlockStatement",
"body": [
...
]
}
}
对函数声明⽽⾔,其节点类型为FunctionDeclaration,函数的标识符(即函数名)存放在id节点中,其中name⼦属性即为函数名。params和body分别为函数的参数列表和函数体。
我们再来看函数调⽤:
"expression": {
"type": "CallExpression",
"callee": {
"type": "Identifier",
"name": "myAwesomeFunction"
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论