结构化重构代码
本指南介绍如何让智能体完成一次大范围的机械式改动 —— 一次 codemod、一次批量 API 迁移、一种你想让它消失的写法 —— 方法是结构化搜索与重写,并让改动在真正写入之前先暂存起来供人审查。
ast_grep 按形状匹配代码。ast_edit 重写它。resolve 提交或丢弃。
开始之前
标题为“开始之前”的部分结构化工具覆盖代码图能够解析的那些语言:Dart、JavaScript、TypeScript、TSX、PHP、Python、Rust、Zig、C、C++、Go、Java、Ruby、C#、Swift、Kotlin、R、Assembly、MATLAB 和 Ada。它们需要预先暂存 tree-sitter 语法,而在正式发布的构建里这些语法始终都已就位 —— 缺少某个语法会让服务器直接停止,而绝不会让搜索降级。
先找到形状
标题为“先找到形状”的部分不要重写你还没有看过的东西。让智能体去搜索:
找出所有对我们刚刚做过空值检查的可空对象调用
dispose()的地方。
ast_grep(pattern: "if ($X != null) $X.dispose()", language: "dart")输出是 file:line 加上每个位置的匹配源码。
模式语法能带来什么
标题为“模式语法能带来什么”的部分| 标记 | 匹配的内容 |
|---|---|
$NAME |
任意单个节点,会被捕获 |
$$$NAME |
任意一串同级节点 —— 参数、语句 |
$_ |
任意内容,捕获但不记名 |
最关键的一点:重复出现的 $NAME 必须捕获相同的文本。
if ($X != null) $X.dispose()它会找到那种“销毁自己刚刚检查过的对象”的守卫。它不会找到 if (a != null) b.dispose() —— 而那通常正是你要找的 bug。任何文本搜索都表达不了这一点。
模式什么都没找到时
标题为“模式什么都没找到时”的部分按顺序尝试两件事:
- 把它改小。 先用
foo($X),再试if (a) { foo($X); }。一段单独看并不是合法代码的模式,往往并不是你想要的那个结构。 - 标明语言。 只有当
path指向单个文件时,language:才会被推断出来。
没有任何解析上下文接受的模式会被报告为无法解析 —— 工具会如实说明,而不是对一段从未按它表面样子被解读的东西报告“无匹配”。
重写,并在落地前先看一眼
标题为“重写,并在落地前先看一眼”的部分ast_edit( pattern: "if ($X != null) $X.dispose()", rewrite: "$X?.dispose()", language: "dart",)结果是一份提案:
(proposed) ast_edit: if ($X != null) $X.dispose() → $X?.dispose()12 replacements in 4 files: lib/features/a/x.dart (3) lib/features/b/y.dart (5) …Nothing has been written. Call resolve with edit_id "edit_1" and action"accept" to apply it, or "discard" to drop it.此时磁盘上还什么都没有。读一遍这份清单,抽查几个搜索报告过的位置,然后:
resolve(edit_id: "edit_1", action: "accept")或者
resolve(edit_id: "edit_1", action: "discard")用编译器检查你的成果
标题为“用编译器检查你的成果”的部分以结构化方式做这件事的意义就在于它足够“宽”。而正是这种大范围改动,最容易让一个看起来合理的重写弄坏类型。在完成 resolve 之后请求诊断:
lsp(action: "diagnostics", file: "*")file: "*" 运行的是项目自己的检查器(dart analyze、tsc --noEmit、cargo check、go build),而不是针对单文件的查询 —— 在一次触及四十个文件的改动之后,这才是形态正确的问题。
完整示例:迁移一次调用
标题为“完整示例:迁移一次调用”的部分假设 log(msg, level) 要改成 log(level: level, message: msg)。
ast_grep(pattern: "log($MSG, $LEVEL)", language: "dart")先看看命中结果。如果其中一些其实是另一个双参数 log,就把 path 限定到拥有你想要的那个 log 的目录来收窄模式,而不是试图在模式里表达这种区别。
ast_edit( pattern: "log($MSG, $LEVEL)", rewrite: "log(level: $LEVEL, message: $MSG)", path: "lib/features/telemetry", language: "dart",)然后再 resolve,再执行 lsp(action: "diagnostics", file: "*")。
什么时候该用别的工具
标题为“什么时候该用别的工具”的部分| 场景 | 更合适的工具 |
|---|---|
| 重命名一个符号 | lsp_rename —— 它跟随的是引用,而不是形状 |
| 查找一个字符串、一条注释或一个文件名 | search(内容)/ search_files(名称) |
| “改了这里会破坏什么?” | 代码图 |
| 语法不覆盖的语言 | search 加上 edit |