一文让你搞懂ast

1,385 阅读7分钟

本文主要内容如下:

  • ast是什么
  • ast如何编译的
  • ts
  • ts-morph
  • babel

AST是什么

AST(Abstract Syntax Tree,抽象语法树)是一种数据结构,用于表示源代码的抽象语法结构。AST通过将源代码转换为树状结构,使得代码的结构化表示更加清晰,便于进一步的分析和操作。这种树形结构的数据模型包含了代码的各个部分,如函数调用、变量声明、算术运算等,每个部分都对应树中的一个节点。AST的生成过程通常包括两个阶段:分词(Tokenization)和语法分析(Parsing)

  • 分词:将整个代码字符串分割成最小的语法单元数组,即tokens。
  • 语法分析:在分词的基础上,建立分析语法单元之间的关系,生成AST。

AST在JavaScript开发中的应用非常广泛,包括但不限于:

  • 代码转译:如Babel通过AST转译ES6+代码到ES5代码。
  • 代码压缩:通过AST重构代码,移除未使用的变量和函数,减少代码大小。
  • 代码格式化:利用AST对代码进行格式化调整,使其符合规范。
  • 代码检查:如ESLint通过AST检查代码是否遵循规定的编码规则。
  • 代码高亮和自动补全:编辑器通过AST提供智能提示和代码高亮功能。

ast如何编译的

ts编译:

要将TypeScript编译成AST(抽象语法树),你需要使用TypeScript的编译器API。下面是一个简单的步骤和示例代码,展示了如何使用TypeScript编译器API来加载TypeScript源文件并获取其AST。 示例:

import * as ts from "typescript";

// 定义一个函数来打印AST节点
function printTree(node: ts.Node) {
  console.log(ts.SyntaxKind[node.kind]); // 打印节点类型
  ts.forEachChild(node, printTree); // 递归遍历子节点
}

// 创建一个源文件对象
const code = "console.log('Hello, World');";
const sourceFile = ts.createSourceFile("example.ts", code, ts.ScriptTarget.ES2015, true);

// 调用printTree函数来打印AST
printTree(sourceFile);

这个示例首先导入了TypeScript的编译器API,然后定义了一个printTree函数,该函数递归地遍历AST并打印每个节点的类型。接着,使用ts.createSourceFile方法创建了一个源文件对象,该对象代表了输入的TypeScript代码。最后,调用printTree函数来打印出AST的结构。

注意事项:

  • ts.createSourceFile方法的参数包括源文件名称、源代码、目标脚本目标版本以及是否包含类型注释。
  • 你可以根据需要修改printTree函数,来实现更复杂的AST操作,比如搜索特定类型的节点或修改节点的属性。

特点:

  • 直接访问:直接使用 TypeScript 编译器 API 时,您可以直接访问 TypeScript 编译器的全部功能,包括对编译过程的详细控制以及处理 TypeScript 语言功能各个方面的能力。
  • 详细程度:与 TypeScript 编译器 API 交互需要更深入地了解 TypeScript 语言及其 AST 结构。
  • 灵活性:虽然这种直接方法提供了更大的灵活性和控制力,但它也意味着要处理较低级别的细节,并且在实现某些功能时可能会更加复杂。

ts-morph

ts虽然更加灵活,但考虑到ast转换的易用性和可行性,推荐使用ts-morph解析ts

示例

import { Project, SyntaxKind } from 'ts-morph'
// 实例化一个 Project
const project = new Project({ jsx: true, tsConfigFilePath: 'tsconfig.json' })

// 添加特定的源文件到 project
//当你调用这个方法时,ts-morph 会扫描提供的文件路径或 glob 模式,并将匹配的文件作为源文件(SourceFile 类的实例)添加到 Project 中。这些源文件随后可以进行分析和修改。
const paths = [
  { path: 'packages/router.tsx', prefix: 'y_app' },
]

const loop = (element, prefix) => {
  // 查找名为path的属性赋值
  const pathPropertyAssignment = element.getProperties().find((prop) => prop.getName() === 'path')
  const children = element.getProperties().find((prop) => prop.getName() === 'children')
  const isNotCode = !element.getProperties().find((prop) => prop.getName() === 'code')
  // const propertyAssignment = element?.getProperty('code')

  //propertyAssignment?.remove()
  // 如果找到了path属性赋值,就新增一个code属性赋值,值为path的值
  if (pathPropertyAssignment && isNotCode) {
    const initStr = pathPropertyAssignment.getInitializer().getText()
    element.addPropertyAssignment({
      name: 'code',
      initializer: `'${prefix}_${initStr.replace(/^'\//, '').replace(/\/|-/g, '_')}`,
    })
  }
  if (children) {
    children
      .getInitializer()
      .getElements()
      .forEach((el) => loop(el, prefix))
  }
}
paths.forEach((item) => {
  const { path, prefix } = item
  const sourceFile = project.getSourceFile(path)
  const defaultRouterListDeclaration = sourceFile?.getVariableDeclarations().find((declaration) => declaration.getName() === 'DefaultRouterList')
  // 获取初始化器(也就是数组)
  const arrayInitializer = defaultRouterListDeclaration?.getInitializer()

  // 确保初始化器是数组
  if (arrayInitializer && arrayInitializer.getKind() === SyntaxKind.ArrayLiteralExpression) {
    // 遍历数组的每一项
    arrayInitializer.getElements().forEach((element) => {
      // 查找名为path的属性赋值
      loop(element, prefix)
    })
  }
})
project.saveSync()
示例解析:
  • 实现了自动给router.tsx每一个节点路由配置文件按照路由地址生成对应的路由code
  • 每一个路由code都有一个统一的code前缀 y_app

ts解析网站

TypeScript AST Viewer image.png

特点

  • 抽象层:ts-morph 充当 TypeScript 编译器 API 上的高级抽象层。它通过提供更直观、更简洁的界面简化了导航和操作 AST 的过程。
  • 易于使用:使用 ts-morph,与直接使用 TypeScript 编译器 API 相比,您可以以更直接的方式执行常见任务,例如添加或修改文件、类、函数和属性。
  • 同步和异步操作:ts-morph 支持同步和异步操作,使其适用于各种用例,包括涉及性能考虑很重要的大型代码库的用例

babel

Babel 是一个编译器,它允许开发者使用最新版本的 JavaScript 语法,同时确保代码可以在旧版浏览器中运行。通过获取新特性转换为 ES5 语法,Babel 使得现代 JavaScript(如 ES6、ES7、ES8 等)与旧版浏览器兼容。它不仅支持最新的 JavaScript 语法,还提供了语法转换、填充(polyfill)、代码转换等功能。此外,Babel 还支持实验性的语言建议,并且可以通过插件系统扩展其功能。

Babel的流程主要包括三个步骤:解析、转换和生成。

解析阶段

  • 解析器:首先,Babel会使用解析器将源代码解析成一个抽象语法树(AST)。 解析器称为解析器或词法分析。 在这个阶段,源代码被分割成一系列的令牌(标记),每个令牌代表代码一个元素,如关键字、标识符、操作符等。
  • AST:解析后的结果是AST,类似于树状结构表示源代码语义信息数据结构。AST展现代码中不同部分的概念,而不考虑具体的语法细节。

转换阶段

  • 在AST上应用一系列的转换规则,需要从一种JavaScript语法到另一种语法的转换。这一步骤通常涉及多个转换器(transformer),每个转换器负责处理特定的语法转换任务。
  • 转换器可以修改AST,使其符合目标环境的要求,例如将箭头函数转化为普通函数,或将模块中的语句转化为整数。

生成

  • 代码生成器:最后,Babel将修改后的AST转换回源代码,Chone调用代码生成。生成的代码应该与原始输入相似,但已经应用了所有必要的转换,以适应目标环境。

示例

下面是一个简单的示例,演示如何使用Babel将快捷函数转换为普通函数:

import { parse } from "@babel/parser";
import generate from "@babel/generator";

const code = "const double = a => a * 2;";
const ast = parse(code);

const output = generate(ast, {}, code);

console.log(output.code); // 输出转换后的代码

 

为了解决这个问题,我们需要一个parse函数来解析 AST,generate函数将返回 AST 的源代码。通过上述示例,Babel 能够使用最新 JavaScript 特性代码来修改浏览器的页面显示代码,从而保证浏览器的正常运行。

开发一个babel插件

babel插件主要通过visiter(访问器)实现,提供一个可以访问的函数,和@babel/traverse解析的第二个参数相同 示例:

export default function({ types: t }) {
  return {
    visitor: {
      AssignmentExpression(path) {
        // 修改节点
        const node = path.node; 
        if (node.operator === '=') { 
        // 做一些修改
        node.value = t.stringLiteral('newValue'); 
        }
      }
    }
  };
};

解析对比网站

需要知道visiter(访问器)应该访问哪个元素,可以通过下面网站进行对比解析

AST Explorer

image.png