学习AST语法树及应用

173 阅读5分钟

什么是AST抽象语法树

在传统的编译语言的流程中, 程序的一段源代码在执行之前会经历三个步骤,统称为"编译":

    分词/词法分析

    这个过程会将字符组成的字符串分解成有意义的代码块,这些代码块统称为词法单元,比如 const msg = '提示文字',这段程序通常会被分解成为下面这些词法单元:const 、msg 、= 、'提示文字',空白是否被当成词法单元取决于空格在这门语言中的意义。

    解析/语法分析

    这个过程是将词法单元流转换成一个由元素嵌套所组成的代表了程序语法结构的树,这个树被成为“抽象语法树”。

    代码生成

    将AST转换成可执行代码的过程被称为代码生成

抽象语法树(abstract syntax tree,AST)  是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,这所以说是抽象的,是因为抽象语法树并不会表示出真实语法出现的每一个细节,比如说,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现。

AST结构

示例

例如一行这样定义的代码

import { ref } from 'vue';  
const msg = ref('提示文字')

AST语法树的结构如下:

{
	"type": "Program",
	"start": 0,
	"end": 52,
	"loc": {
		"start": {
			"line": 1,
			"column": 0,
			"index": 0
		},
		"end": {
			"line": 4,
			"column": 0,
			"index": 52
		}
	},
	"sourceType": "module",
	"interpreter": null,
	"body": [{
			"type": "ImportDeclaration",
			"start": 1,
			"end": 27,
			"loc": {
				"start": {
					"line": 2,
					"column": 0,
					"index": 1
				},
				"end": {
					"line": 2,
					"column": 26,
					"index": 27
				}
			},
			"importKind": "value",
			"specifiers": [{
				"type": "ImportSpecifier",
				"start": 10,
				"end": 13,
				"loc": {
					"start": {
						"line": 2,
						"column": 9,
						"index": 10
					},
					"end": {
						"line": 2,
						"column": 12,
						"index": 13
					}
				},
				"imported": {
					"type": "Identifier",
					"start": 10,
					"end": 13,
					"loc": {
						"start": {
							"line": 2,
							"column": 9,
							"index": 10
						},
						"end": {
							"line": 2,
							"column": 12,
							"index": 13
						},
						"identifierName": "ref"
					},
					"name": "ref"
				},
				"importKind": "value",
				"local": {
					"type": "Identifier",
					"start": 10,
					"end": 13,
					"loc": {
						"start": {
							"line": 2,
							"column": 9,
							"index": 10
						},
						"end": {
							"line": 2,
							"column": 12,
							"index": 13
						},
						"identifierName": "ref"
					},
					"name": "ref"
				}
			}],
			"source": {
				"type": "StringLiteral",
				"start": 21,
				"end": 26,
				"loc": {
					"start": {
						"line": 2,
						"column": 20,
						"index": 21
					},
					"end": {
						"line": 2,
						"column": 25,
						"index": 26
					}
				},
				"extra": {
					"rawValue": "vue",
					"raw": "'vue'"
				},
				"value": "vue"
			}
		},
		{
			"type": "VariableDeclaration",
			"start": 28,
			"end": 51,
			"loc": {
				"start": {
					"line": 3,
					"column": 0,
					"index": 28
				},
				"end": {
					"line": 3,
					"column": 23,
					"index": 51
				}
			},
			"declarations": [{
				"type": "VariableDeclarator",
				"start": 34,
				"end": 51,
				"loc": {
					"start": {
						"line": 3,
						"column": 6,
						"index": 34
					},
					"end": {
						"line": 3,
						"column": 23,
						"index": 51
					}
				},
				"id": {
					"type": "Identifier",
					"start": 34,
					"end": 37,
					"loc": {
						"start": {
							"line": 3,
							"column": 6,
							"index": 34
						},
						"end": {
							"line": 3,
							"column": 9,
							"index": 37
						},
						"identifierName": "msg"
					},
					"name": "msg"
				},
				"init": {
					"type": "CallExpression",
					"start": 40,
					"end": 51,
					"loc": {
						"start": {
							"line": 3,
							"column": 12,
							"index": 40
						},
						"end": {
							"line": 3,
							"column": 23,
							"index": 51
						}
					},
					"callee": {
						"type": "Identifier",
						"start": 40,
						"end": 43,
						"loc": {
							"start": {
								"line": 3,
								"column": 12,
								"index": 40
							},
							"end": {
								"line": 3,
								"column": 15,
								"index": 43
							},
							"identifierName": "ref"
						},
						"name": "ref"
					},
					"arguments": [{
						"type": "StringLiteral",
						"start": 44,
						"end": 50,
						"loc": {
							"start": {
								"line": 3,
								"column": 16,
								"index": 44
							},
							"end": {
								"line": 3,
								"column": 22,
								"index": 50
							}
						},
						"extra": {
							"rawValue": "提示文字",
							"raw": "'提示文字'"
						},
						"value": "提示文字"
					}]
				}
			}],
			"kind": "const"
		}
	],
	"directives": []
}

可以看到,一个标准的AST结构可以理解为一个json对象,我们的代码一般在type为program的节点内,可以看到每个节点的结构是差不多的,都有type、start、end、loc固定的公共属性,还有各个type不同的属性

生成语法树

安装

npm install --save-dev @babel/parser

使用

const {parse} = require('@babel/parser');

const ast = parse('const msg = "提示"', {  
sourceType: 'module',  
plugins: [  
// enable jsx and flow syntax  
'typescript',  
],  
});

常用参数介绍

  • sourceType:指示应在其中解析代码的模式。可以是“script”、“module”或“unambiguous”之一。默认为“script”。“unambiguous”将使@babel/parser根据ES6导入或导出语句的存在尝试猜测。带有ES6导入和导出的文件被视为“module”,否则为“script”
  • plugins:包含要启用的插件的数组,示例中写的typescript是因为要修改ts的文件需要加上此插件,否则会报错

遍历语法树

如果我们要对语法树进行操作,大多数情况都需要遍历语法树,遍历过程中获取到当前节点可以对当前节点进行增删改查的操作

安装

npm install --save-dev @babel/traverse

使用

const traverse = require('@babel/traverse');

traverse.default(ast, {  
enter(path) { // 这个path会找到所有的node  
....  
}  
})

创建语法树

如果想要插入自己的节点,需要先创建语法树然后push进抽象语法树再转成代码就可以实现插入代码的功能

创建语法树需要用到@babel/types

安装

npm install --save-dev @babel/types

使用

比如想要实现创建一个defineProps的定义语句,可以用以下方法实现,首先是引入@babel/types ,然后根据节点的type类型在babel/types文档中找到此类型的参数,从外向内添加节点

const t = require('@babel/types');

/**  
*  
* @param {*} originNode 原节点  
* @param {*} propsName setup props形参name 默认是props  
*/

const addDefineProps = (originNode, propsName = 'props') = >{
    const propsArray = originNode.value.properties;
    const defineProps = t.variableDeclaration('const', // 还支持 const 和 var  
    [t.variableDeclarator(t.identifier(propsName), // 变量名  
    t.callExpression(t.identifier('defineProps'), [t.objectExpression(propsArray), // 将props属性加入节点内  
    ]), ), ], );
    return defineProps;
};

删除语法树

想要删除语法,需要在遍历过程中找到每个节点之后通过判断来完成删除操作,以下是经常用到的删除方法

nodePath 常用的属性

  • node:获取当前节点
  • parent:父节点
  • parentPath: 父Path

nodePath 常用的方法

  • get:当前节点

  • findParent:向父节点搜寻节点

  • getSibling:获取兄弟节点

  • replaceWith:用AST节点替换该节点

  • replaceWithMultiple:用多个 AST 节点替换该节点

  • remove:删除节点

修改语法树

修改操作和删除操作很像,都是在遍历的时候针对当前的node节点进行修改操作,实例如下

traverse(ast, {
  enter(path) { 
    if (path.node.type == 'Identifier') {
      path.node.name = 'modify'
    }
  }
})

生成代码

在对AST抽象语法树进行了具体的操作后,我们需要将语法树再转换成代码,转换代码需要用到@babel/generator

安装

npm install --save-dev @babel/generator

使用

const {parse} = require('@babel/parser');


const traverse = require('@babel/traverse');


const generator = require('@babel/generator');

const ast = parse('const msg ="提示"', {
          sourceType: 'module',
          plugins: [
            // enable jsx and flow syntax
            'typescript',
          ],
        });


traverse.default(ast, {
          enter(path) { // 这个path会找到所有的node
              ....
          }
})

const ret = generator.default(ast);