咱们好,这儿是 菜农曰,欢迎来到我的频道。咱们今日的主题是 AST (笼统语法树)
AST 听起来好像是个很新的东西,那么详细有什么用,好不好用就在这篇文章中找到答案吧~
咱们简单将这个词拆分笼统、语法、树
,假如咱们能够顺畅将这个词拆分,那么咱们也就掌握了其核心地点
- 笼统:笼统的反义词是具象,也就说明笼统的事物重视点不在于细节,而在于全体
- 语法:语法一组词法的表达式,具有某种指定的规矩,具有某种特定的意义,比方 1+1
- 树:树是一种一对多的结构,经过根节点往下递生,能够存在多个子树,当然这不是咱们这篇评论的主题,但却是重点
咱们接下来经过几个例子愈加清楚了解一下什么是树
一、什么是树?
1)管用表达式
5 * 4 / 2 + 3 * 6 这是一个简单的算法运算,可是假如咱们要经过树形的方法表达它的话,成果可能是以下这样:
咱们经过剖析这张树形图,咱们能够发现有哪几个结构 ?
- 一部分是数字:
5,4,2,3,6
- 一部分是操作符:
*, /, +, *
咱们从中抽取出了 +
符号,并将其作为该树的根节点,这个时分又能够分为左右两个子树,咱们从中提取出一棵子树来看
观察发现子树又变成了一棵树,那么能够得出一个结论:任何一棵子树都能够独立成为一棵完好的树,多个子树能够组合成一棵完好的树。至此,咱们就完成了一棵树的界说,接下来咱们再看一个其他例子
2)XML 文件
XML文件也是咱们日常中比较常用到的文件结构
<person>
<name>
张三
</name>
<label>
法外狂徒
</label>
</person>
咱们将文件结构转成特点结构后,就能够很直观的看出数据层级与内容
二、树的转化
树的有点是很直观,能够直接看出数据层级与内容,可是咱们平时操作的时分只能是操作客观上的树形结构,而不是以上片面的树形结构。因而当咱们得到上述树形结构后,咱们就需要对该树进行扁平化操作,那问题来了,怎么扁平化呢?
咱们相同拿上述管用运算为例
红色的框框代表一棵树,而绿色和黄色框框则表明该树的两棵子树,当然 5 * 4
当然也能够框起来作为绿色框的子树。
这个时分,聪明的小伙伴们看到这些树有没有什么发现,比方每棵树表明什么?
咱们能够发现每棵树好像都表明着一个管用运算
1)规矩界说
转化需要建立在一定的规矩基础上
咱们需要先界说下规矩,假如遇到一个运算,咱们就以 BinaryExpression
来表明,而 运算 中的结构天然就包含着 字符 和 运算符 ,比方 5 * 4
这是一个运算,咱们将全体标识为一个 BinaryExpression
。
而这个运算中存在三个元素,分别是: 5, 4, *
。那么其中 5
和 4
咱们就能够称之为 字符, *
能够称之为 运算符。由此咱们能够再定一个规矩,字符 的类型咱们能够用 Identifier
来标识,运算符 的类型咱们就以 Operator
来表明。
到这步咱们就现已简单地界说好了一个 规矩,接下来咱们要做的事情就是运用咱们的规矩将上述树形结构扁平化
2)小试牛刀
咱们先拿上述例子来做操作,首要这是一个表达式,咱们运用 BinaryExpression
进行标识
BinaryExpression
type: BinaryExpression
从运算中咱们 以运算符 能够拆分为左右两部分,也就是 5
和 4
,咱们继续进行标识
left: Identifier
type: Identifier
value: 5
right: Identifier
type: Identifier
valuer: 4
界说好两部分后咱们该怎么将两部分链接起来呢? 那就得用到咱们的运算符了 *
,咱们先运用规矩界说好运算符的表明
operator: *
然后将两部分链接起来
BinaryExpression
type: BinaryExpression
left: Identifier
type: Identifier
value: 5
operator: *
right: Identifier
type: Identifier
valuer: 4
3)制品展现
很好,到这儿咱们就完成了第一块里程碑了!
4)趁热打铁
上面咱们才完成了一小部分的规矩转化界说,接下来咱们继续将树形结构进行转化:
到这儿咱们现已从树形结构图转到了咱们界说的层级结构了,但咱们能够发现,以上的层级结构图依然是不够完好的
目前为止咱们才界说了上述表达式中左边的部分,还缺少右边的界说,这个时分就需要咱们来帮个忙, 帮我补充一下右边的部分,结构体现已在下述文本中贴出,咱们能够复制到自己的文本编辑器中进行填空补充,将__
内容替换补充即可
right: __
type: __
left: __
type: __
value: __
operator: __
right: __
type: __
value: __
接下来就到了发布答案的环节了!
right: BinaryExpression
type: BinaryExpression
left: Identifier
type: Identifier
value: 3
operator: *
right: Identifire
type: Identifier
value: 6
咱们能够进行比对下答案是否正确,然后咱们将两部分内容进行组装
到这儿,咱们就现已得到了一个完好的层级结构了,那么这部分内容跟咱们今日将的 AST 有什么关系呢?
咱们先来看下真实的 AST(笼统语法树)长啥样
咱们转化一个简单的函数:
function add(n, m){
return n + m
}
左边是咱们平时编写的代码,而右侧就是经过代码转化得到的 AST 树
咱们经过观察这棵 AST 树有什么发现?没错!这棵 AST 树的结构根本和咱们刚刚共同完成的层级结构图
共同,这意味着咱们刚刚自己手撸了一棵 AST 树出来
三、揭穿 AST 面纱
1)AST 界说
1. 它是什么?
AST(笼统语法树)并没有咱们所想的那么神秘,它是源代码语法结构的一种笼统表明,它以树状的方法表现编程语言的语法结构,树上的每个节点都表明源代码中的一种结构。
2. 它有什么特征?
首要它是笼统的,它无关语法结构,不会记载源语言真实语法中的每个细节,比方分隔符,空白符,注释等,它都会进行移除。
3. 它有什么用?
经过以上的实践,咱们也认识到了转化AST 是一项繁琐的过程,但为什么要去转化呢?现在各种语言语法种类繁复,虽然终究落到计算机的眼中都是 0 和 1,可是编译器需要辨认语言,这个时分就需要运用一种通用的数据结构来描绘,而 AST 就是那个东西,因为 AST 是真实存在且存在一定逻辑规矩的。
4. 它是怎么进行转化的?
它转化的过程中也是运用到了咱们刚刚所说的几种方法:
- 词法剖析器
- 语法剖析器
- 解释器
比方咱们写个简单的代码:
const name = '张三'
-
词法剖析
第一步就是 词法剖析 ,它的使命就是一个一个字母地读取代码,当它遇到 空格、操作符、特殊符号 的时分,就表明自己第一活现已扫描完毕了,咱们上述的代码这经过 词法剖析 后就会被解析为 [const, name, =, '张三']
这几个值
-
语法剖析
经过上层的剖析,咱们现已拿到了各个 token, 也就是 token流 ,也就是接下来咱们就能够对 token流 进行语法剖析,比方咱们第一个遇到的 token 是 const
,语法剖析器经过剖析,判别它是一个 声明参数 ,就会标记为 VariableDeclaration
,以此类推,后边的几个 token 都会进行剖析,直到生成了一棵 AST 笼统语法树
当生成树的时分,解析器 会删去一些没必要的标识tokens(比方不完好的括号),因而AST不是100%与源码匹配的,可是现已能让咱们知道怎么处理了
2)AST 使用
AST 检查辅助东西:点我
解析并转化 AST 的这个步骤比较繁琐,当然咱们不必重复造轮子,现已有人替咱们造好了轮子,比方解析服Java文件,咱们能够使用 Javaparser
进行 AST 转化,解析 Js / Ts 文件,能够使用 Babelparser
进行 AST 转化。当然,虽然轮子现已为咱们准备好了,咱们还需要怎么运用,那就是得了解规矩,下面附上一些常用的节点类型意义对照表,也就是 AST 转化的规矩:
类型名称 | 中文译名 | 描绘 |
---|---|---|
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 |
为了快速了解,咱们这篇以 JavaScript 文件为例,那么解析与操作 JavaScript 文件,现已有了比较好用的轮子 — jscodeshift
,咱们下面就运用 jscodeshift
来操作 AST
1、查找
这儿是一段非常简易的代码:
import React from 'react';
import { Button } from 'antd';
咱们比照上面的 节点类型意义对照表 ,能够看出这是两个 ImportDeclaration
句子
然后咱们将这段代码放到 AST 可视化东西中检查转化成 AST 后的姿态:
这个时分咱们有个小小的需求,那就是我想要获取下面代码块中的导包源,也就是 from
后边的内容
import React from "react";
import { Button } from "antd";
import { moment } from "moment";
咱们来看这段话的意义,代码中咱们经过引进 jscodeshift
来协助咱们解析和操作 AST 文件,然后在 API 中声明了咱们要查找元素的类型
这个时分咱们能够翻开控制台运转 node find.js
来运转该脚本内容,能够看到控制台成功的输出了咱们想要的成果!
react
antd
moment
接下来咱们玩法进阶,咱们在下面代码块中除了看到有 import
语法,还界说了 name
特点,那咱们这个时分需求又来了, 我想获取该 name
的值!这个时分要怎么办呢?
第一步咱们需要检查 AST 结构,咱们能够将文件体复制到咱们的 AST 检查辅助东西上进行 AST 结构概览:
能够看到咱们想要的内容在 ArrayExpression
中的 elements
中,那么接下来咱们在代码中该怎么操作呢?咱们能够先进行测验~
答案如下:
咱们先要找到 ArrayExpression
类型的元素,然后访问该元素下的 elements
特点,就会得到咱们想要的值了!
张三
李四
王五
2、修正
咱们上面现已完成了经过 AST 结构来查找咱们想要的元素,下面咱们就能够开始进行操作节点元素了!
首要先看怎么修正,这时来了个需求,咱们的
Button
组件名称变了,换成了Button01
,那咱们就得做出相应的修正
接下来咱们继续看以下文件,经过检查能够发现有些不同,这个时分多了 find
API,而且这个API能够添加参数 { source: { value: "antd" } }
。
这个 API 的目的是只查找 source = antd
的 ImportDeclaration
元素,然后进行替换,Button
命名的地点方位在 imported.name
,因而咱们相应修正该值即可
咱们经过运转 node modify.js
便能够看到咱们修正后的文件内容,想要使之生效,咱们还需要将修正后的内容写会该文件中,咱们能够在文件最下方补上下面一段代码:
fs.writeFileSync('./code/demo.js', root.toSource(), 'utf-8')
然后运转代码,这个时分咱们就能够发现 demo.js
文件内容现已发生了修正。
import React from "react";
import { Button01 } from "antd";
import { moment } from "moment";
var name = ["张三", "李四", "王五"];
3、新增
有了查,改,接下来就轮到了增
了,增的话会比上面复杂些,因为咱们需要将咱们要新增的内容构建成 AST 结构,然后再往已有的 AST 结构中刺进
老姿态,咱们老朋友需求又来了,之前页面中只用到了
antd
的Button
组件,那咱们页面这个时分还需要用到antd
的Select
组件
咱们第一步就是要将咱们要刺进的内容构建成 AST 元素,咱们先剖析已有的 Button
AST 结构长啥样,然后依葫芦画瓢构建即可。
咱们剖析得到该结构的组成部分由 ImportSpecifier
和 Identifier
组成,ImportSpecifier
中包着 Identifier
那么咱们就能够得出咱们要刺进的内容结构为:
接下来就交给 jscodeshift
帮咱们生成
$.importSpecifier($.identifier("Select"))
得到 AST 结构后咱们还需要检查咱们要刺进的方位,回到之前的 AST 结构中
咱们发现导入的资源组件内容都放在了 specifiers
特点中,那咱们就能够着手操作了,咱们在项目中找到 create.js
文件
经过运转代码,能够发现成果现已变成了咱们修正后的内容。
import React from "react";
import { Button, Select } from "antd";
import { moment } from "moment";
var name = ["张三", "李四", "王五"];
4、删去
讲完查,改,增,最终就剩余咱们擅长的删
了
需求它又来了,页面这个时分不需要
antd
组件了,也就是将import { Button } from "antd";
这句话移除
那就老规矩,先找到 antd
这个元素地点的 AST,然后将它置为空即可
这个时分经过运转,就能够发现打印出来的内容现已没有了关于antd
的引进信息了
import React from "react";
import { moment } from "moment";
var name = ["张三", "李四", "王五"];
到这儿咱们就讲完了关于 AST 的增删改查操作
好了,以上就是本篇的一切内容,AST 是个很有用的东西,假如觉得对你有协助的小伙伴无妨点个重视做个伴,就是对小菜最大的支撑。不要空谈,不要贪懒,和小菜一同做个吹着牛X做架构
的程序猿吧~ 咱们下文再会!
今日的你多努力一点,明日的你就能少说一句求人的话!
我是小菜,一个和你一同变强的男人。
微信公众号已开启,菜农曰,没重视的同学们记住重视哦!