乐趣区

关于代码质量:AST-初探深浅代码还能这样玩

大家好,这里是 菜农曰,欢送来到我的频道。咱们明天的主题是 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, *。那么其中 54 咱们就能够称之为 字符 * 能够称之为 运算符 。由此咱们能够再定一个规定, 字符 的类型咱们能够用 Identifier 来标识, 运算符 的类型咱们就以 Operator 来示意。

到这步咱们就曾经简略地定义好了一个 规定 ,接下来咱们要做的事件就是利用咱们的规定将上述树形构造 扁平化

2)小试牛刀

咱们先拿上述例子来做操作,首先这是一个表达式,咱们利用 BinaryExpression 进行标识

BinaryExpression
        type: BinaryExpression

从运算中咱们 以运算符 能够拆分为左右两局部,也就是 54,咱们持续进行标识

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 = antdImportDeclaration 元素,而后进行替换,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 构造中插入

老样子,咱们老朋友需要又来了,之前页面中只用到了 antdButton 组件,那咱们页面这个时候还须要用到 antdSelect 组件

咱们第一步就是要将咱们要插入的内容构建成 AST 元素,咱们先剖析已有的 Button AST 构造长啥样,而后依葫芦画瓢构建即可。

咱们剖析失去该构造的组成部分由 ImportSpecifierIdentifier 组成,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 做架构 的程序猿吧~ 咱们下文再见!

明天的你多致力一点,今天的你就能少说一句求人的话!

我是小菜,一个和你一起变强的男人。 💋

微信公众号已开启,菜农曰,没关注的同学们记得关注哦!

退出移动版