RubyConf China 2023 - 玩转 AST,构建自己的代码分析和代码重写工具 by 黄志敏

50 阅读6分钟

以下是整理后的演讲内容,已去除发言人姓名、时间及口头语,保留核心逻辑与内容结构,使文本结构更为清晰连贯。


玩转 AST:构建自己的代码分析与重写工具

一、AST 的基本概念

抽象语法树(AST, Abstract Syntax Tree)是在编程语言处理中最常见的数据结构,用于抽象表示源代码的语法结构。
通过树形结构来展示代码的组成,每个节点对应代码中的一个语法元素。AST 在编译器、解释器、静态分析工具、代码格式化工具中都有广泛应用,使得代码分析、优化、重构更容易实现。

二、Ruby 中常见的 AST 工具

在 Ruby 中有几种常用方式将源代码转换成 AST:

  • Ripper:Ruby 自带的库。
  • Parser Gem:如 RuboCop 使用。
  • Ruby Parser:Breakman 等安全扫描工具使用。

这些工具能将 Ruby 源码转换成 AST,为代码格式化、静态分析等提供基础。


三、格式化工具的工作原理

代码格式化工具会遍历整个 AST,根据节点类型生成新的代码字符串。
例如遇到 class 节点就输出类定义代码,遇到 def 节点就生成方法定义。
格式规则(如缩进、单双引号样式)由工具预置,而生成的代码并不依赖原始源文件的布局。

Prettier 为例,它通过定义“中间表示”,使用 grouplinehardline 等标记控制换行与缩进。当代码超出行宽限制时,可以自动换行或保持一行显示。


四、Linter 工具

Linter 是静态分析工具。其实现模式一般有两种:

  1. 纯分析型:发现问题仅提示。
  2. 可修复型:除了提示,还能自动修复。

作者开发过两个工具:

  • rails_best_practices:检查是否违反最佳实践,仅提示问题。
  • Synvert:能自动重写和修复代码。

五、rails_best_practices 的实现模式

通过监听特定 AST 节点的类型与属性,分析模型与数据库索引、关联定义是否匹配。如检测到缺少索引,则输出警告信息,指出文件与行号。此方法不修改源码,仅报告问题。


六、Synvert 的实现模式

Synvert 以“代码片段(snippet)”为基本单元,提供查找、修改 AST 的规则组合。
它以 AST 为核心,对 Ruby 代码执行“查找 + 重写”操作。
工作流程为:

  1. 使用 RipperParserRuby Parser 将代码生成 AST。
  2. 利用四类信息:node typechildrenlocationsource 来定位节点。
  3. 增强节点属性,便于后续使用(如 node.receivernode.message 等)。

七、自定义 AST 查询语言(Node Query Language)

为方便查询 AST,作者设计了类 CSS 的查询语法(NQL):

.send[receiver=factory_bot][message=create]

该语法可根据节点类型与属性条件匹配 AST 节点。
其底层实现借鉴编译器中的 词法分析(LEX)语法分析(YACC) 技术,使用 Ruby 中的 RexRacc 实现。

通过定义 token(如节点类型、属性 key/value、操作符等),构建解析器,将字符串形式的查询语言解析成可执行的节点选择器。

该语言支持:

  • 多个属性条件;
  • 各种数值、布尔、字符串类型;
  • 多种比较操作符(=、!=、>、<、in、not in 等);
  • 数组、正则匹配;
  • 多重选择器与子节点关系选择。

可匹配复杂模式,例如查找方法调用、查找特定 key 的哈希、检测测试方法中缺少 super 调用等。


八、代码替换与重写策略

代码替换通常有两种方式:

  1. 基于 AST 修改后重新生成代码:简单但可能破坏代码风格。
  2. 基于位置信息直接修改源文件:保留原始代码风格。

Synvert 采用第二种方式,通过 AST 节点的 start/end 位置信息,在源代码字符串中精确替换。
支持操作包括:

  • replace_with
  • insert_after
  • insert_before
  • delete
  • remove

示例:
将代码 FactoryBot.create(:user) 替换为 FactoryBot.create(:user, :activated)
或在测试方法中自动插入 super 调用。


九、查找与替换实例示例

  1. 删除调试代码
    利用查询语法查找 puts 调用并删除。

  2. 方法命名调整
    在 Rails 2.3 → 3.0 升级场景中,可自动将 find_by_email_and_active(true) 转换为:

    where(email: email, active: true)
    

    并处理类似的其他动态 finder 方法。

  3. 语法升级自动化
    批量替换旧写法、增加必要语法元素(如 super),或更新断言风格。


十、Synvert 的架构组成

  1. Synvert Core:提供查询与重写 API。

  2. CLI 工具 synvert:可读取本地或远程 snippet,批量修改文件。

  3. GUI 与 VSCode 扩展

    • 可视化展示修改 diff;
    • 单选或批量应用修改;
    • 支持自动生成 snippet;
    • 提供输入原始代码与期望输出,即可自动生成对应 snippet。

十一、自动生成 Snippet 逻辑

输入示例代码与目标代码,系统会:

  1. 将两段代码转换为 AST;
  2. 对比节点差异;
  3. 生成相应的“insert / replace / delete”规则;
  4. 构建出 snippet。

此逻辑可应用在 GUI、VSCode 扩展与 API 后端中,目前尚非 AI,而是规则生成。


十二、跨语言支持与扩展

Synvert 不仅支持 Ruby,还拥有 JavaScript 版本,可实现:

  • TypeScript 类型语法转换;
  • CSS/LESS/SASS → SCSS 的文件重命名与语法转换等。

十三、开源与协作

Synvert 的核心组件(synvert-corenode_querynode_mutation)均为开源。
社区可贡献 snippet 规则库(synvert-snippets),在 GUI 中可直接启用。
支持即插即用、可版本更新分发的扩展机制。


十四、常见问题答疑摘要

  • AST 是否能分析动态定义的方法(metaprogramming)?
    只能处理静态语法,动态定义的方法无法直接出现在 AST 中。
  • 代码是否需语法正确?
    必须语法正确才能生成 AST;语法错误文件无法解析。
  • 是否能自动处理 Rails 版本迁移时的参数传递变化?
    可以部分自动化处理,复杂场景需人工复核。
  • 是否能实现 RSpec 到 Minitest 的自动转化?
    可以,通过预定义输入输出规则与 snippet 即可扩展。

十五、总结

通过 AST 技术,可以实现自定义的代码分析、格式化和重写工具。
Synvert 结合同步的 CLI、GUI 与编辑器扩展,将 AST 操作简化为直观的查找与重写过程,使大规模代码重构自动化成为可能。


(整理完)