AST - 抽象语法树

2,061 阅读6分钟

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

AST 被广泛应用于翻译、格式化、代码检查、编译、构建等方面。前端开发中 babeleslintprettierwebpack 等工具,无一例外的应用了AST。这些工具底层可能依赖了不同的解析器生成 AST,比如 eslint 使用了espree、babel 使用了 Babylon 等。AST 树主要使用场景如下:

  • JS 反编译,语法解析
  • Babel 编译 ES6 语法
  • 代码高亮
  • 关键字匹配
  • 作用域判断
  • 代码压缩

在本文中主要以 Babel 为例来介绍 AST。Babel,从ECMAScript的诞生后,它便充当了代码和运行环境的翻译官,让我们随心所欲的使用js的新语法进行代码编写。Babel 单词的本意是通天塔的意思。Babel 广义上可以理解成一个生态圈,基于一个叫做 Babylon 的解析器。

v2-41788753ec9f58a1f079dce0b336fe08_720w.png

一. 常用节点和常用方法概括

具体推荐 AST在线解析网站 astexplorer.net/

企业微信截图_16369425859103.png

常用节点:

AST常用节点.webp

常用方法:

AST 常用方法.webp

二. AST 分析步骤的拆解

v2-6706d626afb8b28577ff60a10410c55c_720w.png

1.解析(parse):解析步骤接收代码并输出 AST。 这个步骤分为两个阶段:

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

    [
      { type: { ... }, value: ..., start: ..., end: ..., loc: { ... } },
      { type: { ... }, value: ..., start: ..., end: ..., loc: { ... } },
      { type: { ... }, value: ..., start: ..., end: ..., loc: { ... } },
      ...
    ]
    

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

{
  type: {
    label: 'name',
    keyword: undefined,
    beforeExpr: false,
    startsExpr: true,
    rightAssociative: false,
    isLoop: false,
    isAssign: false,
    prefix: false,
    postfix: false,
    binop: null,
    updateContext: null
  },
  ...
}
  • 语法分析 Syntactic Analysis :语法分析阶段会把一个令牌流转换成 AST 的形式。 这个阶段会使用令牌中的信息把它们转换成一个 AST 的表述结构,这样更易于后续的操作。

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

3.生成(generate):代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)。深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。

const fs = require('fs');
//babel库相关,解析,转换,构建,生产
const parser    = require("@babel/parser");
const traverse  = require("@babel/traverse").default;
const types     = require("@babel/types");
const generator = require("@babel/generator").default;

const jscode = 'var a = 123, b = 321'

// 第一步
// 将 code 转换为 AST 树
const ast    = parser.parse(jscode);

// 第二步 遍历 AST 树,转换到相应格式的源码 AST
traverse(ast, { /** 转换逻辑 */ });

// 第三步
// 生成新的js code,并保存到文件中输出
const {code} = generator(ast);
fs.writeFile('./build.js', code, (err)=>{});

三. 实例解析

function add(a, b) {
   return a + b
}

先将这个函数拆成了基本的三块:

  • 一个 id,就是它的名字,即add
  • 两个 params,就是它的参数,即[a, b]
  • 一块 body,也就是大括号内的一堆东西

add 没办法继续拆下去了,它是一个最基础 Identifier(标志)对象,用来作为函数的唯一标志,就像人的姓名一样。

params 继续拆下去,其实是两个 Identifier 组成的数组 [a, b]。之后也没办法拆下去了。

body 其实是一个 BlockStatement(块状域)对象,用来表示是{return a + b}

{
  "type": "File",
  "start": 0,
  "end": 38,
  "loc": {
    "start": {
      "line": 1,
      "column": 0
    },
    "end": {
      "line": 3,
      "column": 1
    }
  },
  "errors": [],
  "program": {
    "type": "Program",
    "start": 0,
    "end": 38,
    "loc": {
      "start": {
        "line": 1,
        "column": 0
      },
      "end": {
        "line": 3,
        "column": 1
      }
    },
    "sourceType": "module",
    "interpreter": null,
    "body": [
      {
        "type": "FunctionDeclaration",
        "start": 0,
        "end": 38,
        "loc": {
          "start": {
            "line": 1,
            "column": 0
          },
          "end": {
            "line": 3,
            "column": 1
          }
        },
        "id": {
          "type": "Identifier",
          "start": 9,
          "end": 12,
          "loc": {
            "start": {
              "line": 1,
              "column": 9
            },
            "end": {
              "line": 1,
              "column": 12
            },
            "identifierName": "add"
          },
          "name": "add"
        },
        "generator": false,
        "async": false,
        "params": [
          {
            "type": "Identifier",
            "start": 13,
            "end": 14,
            "loc": {
              "start": {
                "line": 1,
                "column": 13
              },
              "end": {
                "line": 1,
                "column": 14
              },
              "identifierName": "a"
            },
            "name": "a"
          },
          {
            "type": "Identifier",
            "start": 16,
            "end": 17,
            "loc": {
              "start": {
                "line": 1,
                "column": 16
              },
              "end": {
                "line": 1,
                "column": 17
              },
              "identifierName": "b"
            },
            "name": "b"
          }
        ],
        "body": {
          "type": "BlockStatement",
          "start": 19,
          "end": 38,
          "loc": {
            "start": {
              "line": 1,
              "column": 19
            },
            "end": {
              "line": 3,
              "column": 1
            }
          },
          "body": [
            {
              "type": "ReturnStatement",
              "start": 24,
              "end": 36,
              "loc": {
                "start": {
                  "line": 2,
                  "column": 3
                },
                "end": {
                  "line": 2,
                  "column": 15
                }
              },
              "argument": {
                "type": "BinaryExpression",
                "start": 31,
                "end": 36,
                "loc": {
                  "start": {
                    "line": 2,
                    "column": 10
                  },
                  "end": {
                    "line": 2,
                    "column": 15
                  }
                },
                "left": {
                  "type": "Identifier",
                  "start": 31,
                  "end": 32,
                  "loc": {
                    "start": {
                      "line": 2,
                      "column": 10
                    },
                    "end": {
                      "line": 2,
                      "column": 11
                    },
                    "identifierName": "a"
                  },
                  "name": "a"
                },
                "operator": "+",
                "right": {
                  "type": "Identifier",
                  "start": 35,
                  "end": 36,
                  "loc": {
                    "start": {
                      "line": 2,
                      "column": 14
                    },
                    "end": {
                      "line": 2,
                      "column": 15
                    },
                    "identifierName": "b"
                  },
                  "name": "b"
                }
              }
            }
          ],
          "directives": []
        }
      }
    ],
    "directives": []
  },
  "comments": []
}

13103695-d34754b834126263.webp

四. AST 实战

  • 打印当前路径所对应的源代码

var a = 123,b = 321 的 AST 树

{
  "type": "File",
  "start": 0,
  "end": 20,
  "loc": {
    "start": {
      "line": 1,
      "column": 0
    },
    "end": {
      "line": 1,
      "column": 20
    }
  },
  "errors": [],
  "program": {
    "type": "Program",
    "start": 0,
    "end": 20,
    "loc": {
      "start": {
        "line": 1,
        "column": 0
      },
      "end": {
        "line": 1,
        "column": 20
      }
    },
    "sourceType": "module",
    "interpreter": null,
    "body": [
      {
        "type": "VariableDeclaration",
        "start": 0,
        "end": 20,
        "loc": {
          "start": {
            "line": 1,
            "column": 0
          },
          "end": {
            "line": 1,
            "column": 20
          }
        },
        "declarations": [
          {
            "type": "VariableDeclarator",
            "start": 4,
            "end": 11,
            "loc": {
              "start": {
                "line": 1,
                "column": 4
              },
              "end": {
                "line": 1,
                "column": 11
              }
            },
            "id": {
              "type": "Identifier",
              "start": 4,
              "end": 5,
              "loc": {
                "start": {
                  "line": 1,
                  "column": 4
                },
                "end": {
                  "line": 1,
                  "column": 5
                },
                "identifierName": "a"
              },
              "name": "a"
            },
            "init": {
              "type": "NumericLiteral",
              "start": 8,
              "end": 11,
              "loc": {
                "start": {
                  "line": 1,
                  "column": 8
                },
                "end": {
                  "line": 1,
                  "column": 11
                }
              },
              "extra": {
                "rawValue": 123,
                "raw": "123"
              },
              "value": 123
            }
          },
          {
            "type": "VariableDeclarator",
            "start": 13,
            "end": 20,
            "loc": {
              "start": {
                "line": 1,
                "column": 13
              },
              "end": {
                "line": 1,
                "column": 20
              }
            },
            "id": {
              "type": "Identifier",
              "start": 13,
              "end": 14,
              "loc": {
                "start": {
                  "line": 1,
                  "column": 13
                },
                "end": {
                  "line": 1,
                  "column": 14
                },
                "identifierName": "b"
              },
              "name": "b"
            },
            "init": {
              "type": "NumericLiteral",
              "start": 17,
              "end": 20,
              "loc": {
                "start": {
                  "line": 1,
                  "column": 17
                },
                "end": {
                  "line": 1,
                  "column": 20
                }
              },
              "extra": {
                "rawValue": 321,
                "raw": "321"
              },
              "value": 321
            }
          }
        ],
        "kind": "var"
      }
    ],
    "directives": []
  },
  "comments": []
}
traverse(ast,{
    // 此处访问的是VariableDeclarator的节点路径(VariableDeclarator未在常用节点列出),下同
    VariableDeclarator(path)
    {
        console.log(path.toString());  // a = 123, b = 321;
        console.log(generator(path.node).code)  // a = 123, b = 321;
    }
});
  • 判断当前path的类型type(使用isxxx,如:isVariableDeclarator)
traverse(ast,{
    VariableDeclarator(path)
    {
        console.log(path.type === 'VariableDeclarator'); // true 
        console.log(path.isVariableDeclarator()) // true
        console.log(types.isVariableDeclarator(path.node))  //true
    }
});
  • 删除指定节点 remove
traverse(ast,{
    VariableDeclarator(path)
    {
       console.log(path.toString())  // b = 321
       if (path.node.id.name === 'b'){
          path.remove()};
		   }
    }
});
  • 删除空语句
traverse(ast,{
    EmptyStatement(path){
       path.remove();
    }
});
  • 处理Unicode字符串 -> 将源码中Unicode字符串转换成我们易读的字符串
/** 处理Unicode字符串 -> 将源码中Unicode字符串转换成我们易读的字符串
 *  假如: jscode = 'var _0x350e = ['\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64\x21', '\x38\x36\x54\x66\x76\x53\x44\x73', '\x32\x37\x31\x39\x38\x36\x32\x76\x65\x77\x71\x65\x56', '\x31\x7a\x62\x78\x4c\x6a\x46', '\x31\x36\x34\x37\x31\x30\x32\x45\x64\x79\x68\x66\x6a', '\x35\x33\x36\x37\x31\x39\x59\x69\x47\x42\x65\x6c', '\x36\x38\x32\x35\x38\x33\x5a\x71\x61\x43\x76\x76', '\x32\x30\x35\x30\x34\x32\x44\x5a\x42\x66\x6e\x75', '\x31\x35\x32\x34\x34\x35\x43\x65\x54\x51\x51\x6d', '\x34\x39\x31\x35\x6f\x52\x43\x4e\x4f\x6f', '\x6c\x6f\x67', '\x31\x54\x62\x71\x48\x45\x4e', '\x31\x31\x6f\x61\x57\x75\x79\x4e'];' 
 *  对照在线网站进行解析时,其value节点是可阅读的字符串,
 *  难以识别的字符串放在了extra节点里,所以直接进行删除即可。
 */

企业微信截图_16366878562630.png

traverse(ast,{
  NumericLiteral({node}) {
    if (node.extra && /^0[obx]/i.test(node.extra.raw)) {
      node.extra = undefined;
    }
  },
  StringLiteral({node}) 
  {
    if (node.extra && /\\[ux]/gi.test(node.extra.raw)) {
      node.extra = undefined;
    }
  }
});