java sqlparse ast树_AST 抽象语法树 - Go语言中文社区

java sqlparse ast树_AST 抽象语法树


提起 AST 抽象语法树,大家可能并不感冒。但是提到它的使用场景,也许会让你大吃一惊。原来它一直在你左右与你相伴,而你却不知。

一、什么是抽象语法树

在计算机科学中,抽象语法树(abstract syntax tree 或者缩写为 AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结构。

之所以说语法是「抽象」的,是因为这里的语法并不会表示出真实语法中出现的每个细节。

二、使用场景

JS 反编译,语法解析

Babel 编译 ES6 语法

代码高亮

关键字匹配

作用域判断

代码压缩

三、AST Explorer

c629b8f1ffd040ee0e2de17a9752a14c.png

我们来看一个 ES6 的解释器,声明如下的代码:

1 let tips =[2 "Jartto's AST Demo"

3 ];

看看是如何解析的, JSON 格式如下:

1 {2 "type": "Program",3 "start": 0,4 "end": 38,5 "body": [6 {7 "type": "VariableDeclaration",8 "start": 0,9 "end": 37,10 "declarations": [11 {12 "type": "VariableDeclarator",13 "start": 4,14 "end": 36,15 "id": {16 "type": "Identifier",17 "start": 4,18 "end": 8,19 "name": "tips"

20 },21 "init": {22 "type": "ArrayExpression",23 "start": 11,24 "end": 36,25 "elements": [26 {27 "type": "Literal",28 "start": 15,29 "end": 34,30 "value": "Jartto's AST Demo",31 "raw": ""Jartto's AST Demo""

32 }33 ]34 }35 }36 ],37 "kind": "let"

38 }39 ],40 "sourceType": "module"

41 }

而它的语法树大概如此:

db85f176da0a6068b4bf3145d67a5520.png

每个结构都看的清清楚楚,这时候我们会发现,这和 Dom 树真的差不了多少。再来看一个例子:

1 (1+2)*3

AST Tree:

fe6e61b2589a836995b275f298000f7b.png

我们删掉括号,看看规则是如何变化的?JSON 格式会一目了然:

1 {2 "type": "Program",3 "start": 0,4 "end": 6,5 "body": [6 {7 "type": "ExpressionStatement",8 "start": 0,9 "end": 5,10 "expression": {11 "type": "BinaryExpression",12 "start": 0,13 "end": 5,14 "left": {15 "type": "Literal",16 "start": 0,17 "end": 1,18 "value": 1,19 "raw": "1"

20 },21 "operator": "+",22 "right": {23 "type": "BinaryExpression",24 "start": 2,25 "end": 5,26 "left": {27 "type": "Literal",28 "start": 2,29 "end": 3,30 "value": 2,31 "raw": "2"

32 },33 "operator": "*",34 "right": {35 "type": "Literal",36 "start": 4,37 "end": 5,38 "value": 3,39 "raw": "3"

40 }41 }42 }43 }44 ],45 "sourceType": "module"

46 }

可以看出来,(1+2)*3 和 1+2*3,语法树是有差别的:

1.在确定类型为 ExpressionStatement 后,它会按照代码执行的先后顺序,将表达式 BinaryExpression 分为 Left,operator 和 right 三块;

2.每块标明了类型,起止位置,值等信息;

3.操作符类型;

再来看看我们最常用的箭头函数:

1 const mytest = (a,b) =>{2 return a+b;3 }

JSON 格式如下:

1 {2 "type": "Program",3 "start": 0,4 "end": 42,5 "body": [6 {7 "type": "VariableDeclaration",8 "start": 0,9 "end": 41,10 "declarations": [11 {12 "type": "VariableDeclarator",13 "start": 6,14 "end": 41,15 "id": {16 "type": "Identifier",17 "start": 6,18 "end": 12,19 "name": "mytest"

20 },21 "init": {22 "type": "ArrowFunctionExpression",23 "start": 15,24 "end": 41,25 "id": null,26 "expression": false,27 "generator": false,28 "params": [29 {30 "type": "Identifier",31 "start": 16,32 "end": 17,33 "name": "a"

34 },35 {36 "type": "Identifier",37 "start": 18,38 "end": 19,39 "name": "b"

40 }41 ],42 "body": {43 "type": "BlockStatement",44 "start": 24,45 "end": 41,46 "body": [47 {48 "type": "ReturnStatement",49 "start": 28,50 "end": 39,51 "argument": {52 "type": "BinaryExpression",53 "start": 35,54 "end": 38,55 "left": {56 "type": "Identifier",57 "start": 35,58 "end": 36,59 "name": "a"

60 },61 "operator": "+",62 "right": {63 "type": "Identifier",64 "start": 37,65 "end": 38,66 "name": "b"

67 }68 }69 }70 ]71 }72 }73 }74 ],75 "kind": "const"

76 }77 ],78 "sourceType": "module"

79 }

AST Tree 结构如下图:

4bef623caa884df7062bacbdbd213096.png

我们注意到了,增加了几个新的字眼:

ArrowFunctionExpression

BlockStatement

ReturnStatement

到这里,其实我们已经慢慢明白了:

抽象语法树其实就是将一类标签转化成通用标识符,从而结构出的一个类似于树形结构的语法树。

四、深入原理

可视化的工具可以让我们迅速有感官认识,那么具体内部是如何实现的呢?

继续使用上文的例子:

1 Function getAST(){}

JSON 也很简单:

1 {2 "type": "Program",3 "start": 0,4 "end": 19,5 "body": [6 {7 "type": "FunctionDeclaration",8 "start": 0,9 "end": 19,10 "id": {11 "type": "Identifier",12 "start": 9,13 "end": 15,14 "name": "getAST"

15 },16 "expression": false,17 "generator": false,18 "params": [],19 "body": {20 "type": "BlockStatement",21 "start": 17,22 "end": 19,23 "body": []24 }25 }26 ],27 "sourceType": "module"

28 }

4bb640bbce511d13a0153ba7856ab04e.png

怀着好奇的心态,我们来模拟一下用代码实现:

1 const esprima = require('esprima'); //解析js的语法的包

2 const estraverse = require('estraverse'); //遍历树的包

3 const escodegen = require('escodegen'); //生成新的树的包

4 let code = `functiongetAST(){}`;5 //解析js的语法

6 let tree =esprima.parseScript(code);7 //遍历树

8 estraverse.traverse(tree, {9 enter(node) {10 console.log('enter: ' +node.type);11 },12 leave(node) {13 console.log('leave: ' +node.type);14 }15 });16 //生成新的树

17 let r =escodegen.generate(tree);18 console.log(r);

运行后,输出:

1 enter: Program2 enter: FunctionDeclaration3 enter: Identifier4 leave: Identifier5 enter: BlockStatement6 leave: BlockStatement7 leave: FunctionDeclaration8 leave: Program9 functiongetAST() {10 }

我们看到了遍历语法树的过程,这里应该是深度优先遍历。

稍作修改,我们来改变函数的名字 getAST => Jartto:

1 const esprima = require('esprima'); //解析js的语法的包

2 const estraverse = require('estraverse'); //遍历树的包

3 const escodegen = require('escodegen'); //生成新的树的包

4 let code = `functiongetAST(){}`;5 //解析js的语法

6 let tree =esprima.parseScript(code);7 //遍历树

8 estraverse.traverse(tree, {9 enter(node) {10 console.log('enter: ' +node.type);11 if (node.type === 'Identifier') {12 node.name = 'Jartto';13 }14 }15 });16 //生成新的树

17 let r =escodegen.generate(tree);18 console.log(r);

运行后,输出:

1 enter: Program2 enter: FunctionDeclaration3 enter: Identifier4 enter: BlockStatement5 functionJartto() {6 }

可以看到,在我们的干预下,输出的结果发生了变化,方法名编译后方法名变成了 Jartto。

这就是抽象语法树的强大之处,本质上通过编译,我们可以去改变任何输出结果。

补充一点:关于 node 类型,全集大致如下:

(parameter) node: Identifier | SimpleLiteral | RegExpLiteral | Program | FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | SwitchCase | CatchClause | VariableDeclarator | ExpressionStatement | BlockStatement | EmptyStatement | DebuggerStatement | WithStatement | ReturnStatement | LabeledStatement | BreakStatement | ContinueStatement | IfStatement | SwitchStatement | ThrowStatement | TryStatement | WhileStatement | DoWhileStatement | ForStatement | ForInStatement | ForOfStatement | VariableDeclaration | ClassDeclaration | ThisExpression | ArrayExpression | ObjectExpression | YieldExpression | UnaryExpression | UpdateExpression | BinaryExpression | AssignmentExpression | LogicalExpression | MemberExpression | ConditionalExpression | SimpleCallExpression | NewExpression | SequenceExpression | TemplateLiteral | TaggedTemplateExpression | ClassExpression | MetaProperty | AwaitExpression | Property | AssignmentProperty | Super | TemplateElement | SpreadElement | ObjectPattern | ArrayPattern | RestElement | AssignmentPattern | ClassBody | MethodDefinition | ImportDeclaration | ExportNamedDeclaration | ExportDefaultDeclaration | ExportAllDeclaration | ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier | ExportSpecifier

说到这里,聪明的你,可能想到了 Babel,想到了 js 混淆,想到了更多背后的东西。接下来,我们要介绍介绍 Babel 是如何将 ES6 转成 ES5 的。

五、关于 Babel

由于 ES6 的兼容问题,很多情况下,我们都在使用 Babel 插件来进行编译,那么有没有想过 Babel 是如何工作的呢?先来看看:

1 let sum = (a, b)=>{return a+b};

AST 大概如此:

a807c9b7be3ce0e031bce0f510fc806c.png

JSON 格式可能会看的清楚些:

1 {2 "type": "Program",3 "start": 0,4 "end": 31,5 "body": [6 {7 "type": "VariableDeclaration",8 "start": 0,9 "end": 31,10 "declarations": [11 {12 "type": "VariableDeclarator",13 "start": 4,14 "end": 30,15 "id": {16 "type": "Identifier",17 "start": 4,18 "end": 7,19 "name": "sum"

20 },21 "init": {22 "type": "ArrowFunctionExpression",23 "start": 10,24 "end": 30,25 "id": null,26 "expression": false,27 "generator": false,28 "params": [29 {30 "type": "Identifier",31 "start": 11,32 "end": 12,33 "name": "a"

34 },35 {36 "type": "Identifier",37 "start": 14,38 "end": 15,39 "name": "b"

40 }41 ],42 "body": {43 "type": "BlockStatement",44 "start": 18,45 "end": 30,46 "body": [47 {48 "type": "ReturnStatement",49 "start": 19,50 "end": 29,51 "argument": {52 "type": "BinaryExpression",53 "start": 26,54 "end": 29,55 "left": {56 "type": "Identifier",57 "start": 26,58 "end": 27,59 "name": "a"

60 },61 "operator": "+",62 "right": {63 "type": "Identifier",64 "start": 28,65 "end": 29,66 "name": "b"

67 }68 }69 }70 ]71 }72 }73 }74 ],75 "kind": "let"

76 }77 ],78 "sourceType": "module"

79 }

结构大概如此,那我们再用代码模拟一下:

1 const babel = require('babel-core'); //babel核心解析库

2 const t = require('babel-types'); //babel类型转化库

3 let code = `let sum = (a, b)=>{return a+b}`;4 let ArrowPlugins ={5 //访问者模式

6 visitor: {7 //捕获匹配的API

8 ArrowFunctionExpression(path) {9 let { node } =path;10 let body =node.body;11 let params =node.params;12 let r = t.functionExpression(null, params, body, false, false);13 path.replaceWith(r);14 }15 }16 }17 let d =babel.transform(code, {18 plugins: [19 ArrowPlugins20 ]21 })22 console.log(d.code);

记得安装 babel-core,babel-types 这俩插件,之后运行 babel.js,我们看到了这样的输出:

1 let sum = function(a, b) {2 return a +b;3 };

这里,我们完美的将箭头函数转换成了标准函数。

那么问题又来了,如果是简写呢,像这样,还能正常编译吗?

1 let sum = (a, b)=>a+b

eee10e891af6e9091eceefee6c9fe886.png

Body 部分的结构发生了变化,所以,我们的 babel.js 运行就会报错了。

TypeError: unknown: Property body of FunctionExpression expected node to be of a type ["BlockStatement"] but instead got "BinaryExpression"

意思很明了,我们的 body 类型变成 BinaryExpression 不再是 BlockStatement,所以需要做一些修改:

1 const babel = require('babel-core'); //babel核心解析库

2 const t = require('babel-types'); //babel类型转化库

3 let code = `let sum = (a, b)=> a+b`;4 let ArrowPlugins ={5 //访问者模式

6 visitor: {7 //捕获匹配的API

8 ArrowFunctionExpression(path) {9 let { node } =path;10 let params =node.params;11 let body =node.body;12 if(!t.isBlockStatement(body)){13 let returnStatement =t.returnStatement(body);14 body =t.blockStatement([returnStatement]);15 }16 let r = t.functionExpression(null, params, body, false, false);17 path.replaceWith(r);18 }19 }20 }21 let d =babel.transform(code, {22 plugins: [23 ArrowPlugins24 ]25 })26 console.log(d.code);

看看输出结果:

1 let sum = function(a, b) {2 return a +b;3 };

看起来不错,堪称完美~

六、深入 Babel

当然,上文我们简单演示了 Babel 是如何来编译代码的,但是并非简单如此。

Babel 使用一个基于 ESTree 并修改过的 AST,它的内核说明文档可以在这里找到。

正如我们上面示例代码一样,Babel 的三个主要处理步骤分别是: 解析(parse),转换(transform),生成(generate)。

1.解析(parse):解析步骤接收代码并输出 AST。 这个步骤分为两个阶段:词法分析 Lexical Analysis 和语法分析Syntactic Analysis。

词法分析:词法分析阶段把字符串形式的代码转换为令牌(tokens) 流。你可以把令牌看作是一个扁平的语法片段数组:

n * n;

例如上面的代码片段,解析结果如下:

[

{ type: { ... }, value:"n", start: 0, end: 1, loc: { ... } },

{ type: { ... }, value:"*", start: 2, end: 3, loc: { ... } },

{ type: { ... }, value:"n", start: 4, end: 5, loc: { ... } },

...

]

每一个 type 有一组属性来描述该令牌,和 AST 节点一样它们也有 start,end,loc 属性:

{

type: {

label:'name',

keyword: undefined,

beforeExpr:false,

startsExpr:true,

rightAssociative:false,

isLoop:false,

isAssign:false,

prefix:false,

postfix:false,

binop:null,

updateContext:null},

...

}

语法分析:语法分析阶段会把一个令牌流转换成 AST 的形式。 这个阶段会使用令牌中的信息把它们转换成一个 AST 的表述结构,这样更易于后续的操作。

2.转换(transform):接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。 这是 Babel 或是其他编译器中最复杂的过程,同时也是插件将要介入工作的部分。

3.生成(generate):代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)。

代码生成其实很简单:深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。

了解这这些过程,我们回头再来参悟一下之前的示例代码:

1 const babel = require('babel-core'); //babel核心解析库

2 const t = require('babel-types'); //babel类型转化库

3 let code = `let sum = (a, b)=>{return a+b}`;4 let ArrowPlugins ={5 //访问者模式

6 visitor: {7 //捕获匹配的API

8 ArrowFunctionExpression(path) {9 let { node } =path;10 let body =node.body;11 let params =node.params;12 let r = t.functionExpression(null, params, body, false, false);13 path.replaceWith(r);14 }15 }16 }17 let d =babel.transform(code, {18 plugins: [19 ArrowPlugins20 ]21 })22 console.log(d.code);

是不是发现突然简单易懂了。

七、关于遍历

想要转换 AST 你需要进行递归的树形遍历。

比方说我们有一个 FunctionDeclaration 类型。它有几个属性:id,params,和 body,每一个都有一些内嵌节点。

1 {2 type: "FunctionDeclaration",3 id: {4 type: "Identifier",5 name: "square"

6 },7 params: [{8 type: "Identifier",9 name: "n"

10 }],11 body: {12 type: "BlockStatement",13 body: [{14 type: "ReturnStatement",15 argument: {16 type: "BinaryExpression",17 operator: "*",18 left: {19 type: "Identifier",20 name: "n"

21 },22 right: {23 type: "Identifier",24 name: "n"

25 }26 }27 }]28 }29 }

按照上面的代码结构,我们来说一下具体流程:

1.首先我们从 FunctionDeclaration 开始并且我们知道它的内部属性(即:id,params,body),所以我们依次访问每一个属性及它们的子节点;

2.然后我们来到 id,它是一个 Identifier。Identifier 没有任何子节点属性,所以我们继续;

3.紧接着是 params,由于它是一个数组节点所以我们访问其中的每一个,它们都是 Identifier 类型的单一节点,然后我们继续;

4.此时我们来到了 body,这是一个 BlockStatement 并且也有一个 body 节点,而且也是一个数组节点,我们深入访问其中的每一个;

5.这里唯一的一个属性是 ReturnStatement 节点,它有一个 argument,我们访问 argument 就找到了 BinaryExpression;

6.BinaryExpression 有一个 operator,一个 left,和一个 right。 Operator 不是一个节点,它只是一个值。因此我们不用继续向内遍历,我们只需要访问 left 和 right。

Babel 的转换步骤基本都是是这样的遍历过程。

八、具体语法树

看到抽象语法树,我们脑海中会出现这样一个疑问:有没有具体语法树呢?

和抽象语法树相对的是具体语法树(通常称作分析树)。一般的,在源代码的翻译和编译过程中,语法分析器创建出分析树。一旦AST 被创建出来,在后续的处理过程中,比如语义分析阶段,会添加一些信息。

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/weixin_32175667/article/details/114880578
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2021-05-16 05:48:31
  • 阅读 ( 497 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢