ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

oh-my-pi AST 结构改写实战:ast_edit 工具与 ast-grep 模式语法深度解析

oh-my-pi AST 结构改写实战:ast_edit 工具与 ast-grep 模式语法深度解析 oh-my-pi AST 结构改写实战ast_edit 工具与 ast-grep 模式语法深度解析【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi导读本文围绕 oh-my-pi 编码 Agent 中的ast_edit工具系统讲解基于 ast-grep 的 AST 结构化改写能力——从元变量模式语法、ops/paths入参契约到先预览、后确认的双阶段执行流程再到 Rust 原生层tree-sitter ast-grep的匹配、去重、重叠检测与原子写入原理。读完本文你将掌握如何在编码 Agent 会话中安全地完成跨文件 codemod并理解结构改写与文本替换的本质区别。一、什么是 ast_edit为什么结构改写比文本替换更安全在 oh-my-pi 的编码 Agent 工具集中ast_edit定位为面向 AST 结构的结构化改写工具Structural AST-aware rewrites via ast-grep其模型侧提示词位于 packages/coding-agent/src/prompts/tools/ast-edit.md工具实现位于 packages/coding-agent/src/tools/ast-edit.ts。它适用于文本替换不安全的 codemod 场景。文本替换是按字符串匹配的很容易误伤同名子串例如把注释、字符串字面量里的内容一并改掉而ast_edit把源码解析成语法树按语法结构匹配与改写天然避免这类问题。一个关键设计是混合语言路径完全可行每个文件按自身扩展名推断语言独立解析一条模式只改写它能解析成功的文件。这一点由原生层实现支撑——在 crates/pi-natives/src/ast.rs 中每个候选文件独立调用resolve_language推断语言每条改写规则按候选集中发现的每种语言分别编译。对应测试ast_edit_rewrites_mixed_language_tree_per_file验证了在同时含a.ts与b.rs的目录中执行箭头函数改写TypeScript 文件被改写、Rust 文件原样保留且整次调用不报错。二、模式语法元变量Metavariable规则详解ast_edit与ast_grep共享同一套 ast-grep 模式语法详见 docs/tools/ast-grep.md。元变量是结构化匹配的核心模式中的pat里的元变量会一一替换substitute进out中。2.1 三种元变量形态写法含义示例$NAME捕获一个AST 节点并绑定$A $A()$_匹配一个AST 节点不绑定class $_ { … }$$$NAME捕获零个或多个AST 节点并绑定console.log($$$ARGS)$$$匹配零个或多个AST 节点不绑定foo($$$)使用要点提示词中的硬性约束务必使用$$$NAME不要用$$NAME——后者是非法写法元变量名必须大写且必须代表完整节点whole node——像prefix$VAR这种节点片段式写法会匹配失败同一个元变量出现两次 ⇒ 两处必须匹配完全相同的代码。例如$A $A能匹配x x但匹配不了x y。2.2 模式的语法约束改写模式必须能解析为单个 AST 节点。非独立节点non-standalone的模式需要包裹例如class $_ { … }TypeScript 场景要容忍类型注解async function $NAME($$$ARGS): $_ { $$$BODY }这样的模式才能覆盖带返回类型的函数声明1:1 替换原则捕获内容按原样替换进out不做拆分或合并。除非语法本身允许该位置展开多个兄弟节点否则一个捕获不能膨胀成多个兄弟节点这一约束在 docs/tools/ast-edit.md 中有明确说明。2.3 删除节点空outout为空字符串即删除匹配节点。提示词给出的经典例子{pat:console.log($$$),out:}三、入参契约ops 与 pathsast_edit的 TS 层通过type()定义了严格 schema见 ast-edit.ts字段类型必填说明ops{ pat: string; out: string }[]是一条或多条改写规则。pat必须非空重复的pat会在原生执行前直接报错out为空表示删除匹配节点pathsstring[]是一个或多个文件、目录、glob或指向本地文件路径的内部 URL。至少需要一个非空条目3.1 参数校验执行前AstEditTool.execute()在调用原生层之前会先做四道校验ops[].pat为空 → 抛ToolErrorops为空数组 → 抛ToolErrorops中出现重复pat→ 抛ToolErrorDuplicate rewrite pattern合法ops被转换为Recordpattern, replacement传给原生层。3.2 官方示例六则工具类内置了六个示例见 ast-edit.ts覆盖了最常见的 codemod 场景// 1. 跨 TypeScript 文件重命名调用点 { ops: [{ pat: oldApi($$$ARGS), out: newApi($$$ARGS) }], paths: [src/**/*.ts] } // 2. 删除匹配的调用 { ops: [{ pat: console.log($$$ARGS), out: }], paths: [src/**/*.ts] } // 3. 改写 import 来源路径 { ops: [{ pat: import { $$$IMPORTS } from \old-package\, out: import { $$$IMPORTS } from \new-package\ }], paths: [src/**/*.ts] } // 4. 现代化改造为可选链同一元变量强制同一性 { ops: [{ pat: $A $A(), out: $A?.() }], paths: [src/**/*.ts] } // 5. 借助捕获交换两个实参 { ops: [{ pat: assertEqual($A, $B), out: assertEqual($B, $A) }], paths: [tests/**/*.ts] } // 6. Python——将 print 调用改为 logging { ops: [{ pat: print($$$ARGS), out: logger.info($$$ARGS) }], paths: [src/**/*.py] }注意示例 4$A $A()→$A?.()之所以能安全改写正是因为同一元变量必须匹配相同代码的约束保证了$A两处一定是同一个表达式从而等价于可选链调用。四、执行流程预览Preview→ 确认Resolve/Rejectast_edit最核心的安全机制是永不直接落盘。提示词原文明确指出Matches areSTAGED as a proposal, not applied: finalize by writing a one-sentence reason toxd://resolve(apply) orxd://reject(discard).完整流程如下与 docs/tools/ast-edit.md 的 Flow 章节一致校验与规范化TS 层校验ops读取环境变量PI_MAX_AST_FILES默认 1000作为maxFiles上限路径解析经由 packages/coding-agent/src/tools/path-utils.ts 的resolveToolSearchScope()处理文件/目录/glob/内部 URL 的解析与分区与ast_grep共用同一套逻辑外部 URL 会被拒绝Cannot rewrite external URLast_edit只作用于本地文件第一遍永远 dry-runrunAstEditOnce(...)以dryRun: true、failOnParseError: false调用原生astEdit(...)产出预览 diff渲染预览按文件分组输出-/两行式 diffhashline 模式为-LINE:before/LINE:after普通模式为-LINE:COLUMN before/LINE:COLUMN after并在结果开头写入Staged as a proposal — files NOT modified yet...提示指名xd://resolve/xd://reject两个设备路径见 resolve.ts排队确认动作若预览命中替换queueResolveHandler(...)注册一个 pending resolve invoker并在会话中表面SoftToolRequirementtoolName: write携带xd://resolve或xd://reject谓词执行 applyAgent 以write xd://resolve 理由落定回调以dryRun: false重跑同一组改写并比较总数与每文件计数验证预览是否仍新鲜stale 防护若重跑结果与预览不一致如其他工具已改动文件返回Preview is stale / no longer matches; ...错误结果而非静默成功丢弃write xd://reject 理由仅返回丢弃消息不触碰任何文件。该预览-确认契约在测试 packages/coding-agent/test/tools/ast-edit.test.ts 中得到验证registers a pending action that apply writes changes用例确认预览结果applied: false、apply 后文本写入modernWrap(x, value)fails stale pending apply when preview no longer matches用例确认在预览后篡改文件会导致 apply 返回isError: true且文件保持被篡改后的内容不变。五、原生实现原理crates/pi-natives/src/ast.rs 深潜ast_edit的匹配与改写引擎位于 crates/pi-natives/src/ast.rsRust napi-rs通过task::blocking在阻塞工作线程上执行取消与超时通过CancelToken::heartbeat()协作式实现。5.1 改写规则规范化normalize_rewrite_map(...)将ops转换出的(pattern, rewrite)对按模式字符串排序——即原生层执行顺序是按模式字典序而非模型传入ops的原始顺序ast.rs。这是一处容易被忽略的语义细节设计者用它保证规则确定性。5.2 候选文件收集collect_candidates(...)使用pi_walker做目录扫描关键参数与ast_grep一致hidden(true)、gitignore(true)、skip_git(true)——包含隐藏文件、遵循 gitignore、跳过 .git 目录node_modules默认跳过除非glob 文本中显式提到node_modulesglob 语义*.ts只匹配直接子级**/*.ts递归匹配原生测试glob_star_matches_only_direct_children与glob_double_star_matches_recursively分别验证了两种行为。5.3 按语言独立编译与匹配每个候选文件先经resolve_language推断语言无法推断时报 parse issuebest-effort 模式下不中断每条规则对候选集中出现的每种语言分别compile_pattern——在某语言下编译失败的规则跳过该语言的文件并记录 parse issue不会让整次调用失败。随后用language.ast_grep(source)建树对find_all(...)命中的每个匹配调用matched.replace_by(rewrite)生成编辑。值得注意的是模式编译的兜底逻辑crates/pi-ast/src/ops.rs当模式因MultipleNode解析出多个根节点例如key: $V这种片段失败时会自动尝试单节点包裹后再编译只有包裹仍失败才报Invalid pattern。5.4 编辑去重与重叠检测字节级去重多条规则命中同一节点且产生完全相同的替换position、deleted_length、inserted_text 均一致时只计入一次确定性编辑避免误判为重叠重叠即报错apply_edits(...)ops.rs检测到重叠编辑会返回Overlapping replacements detected; refine pattern to avoid ambiguous edits测试ast_edit_dedupes_identical_matches_across_rules验证foo($X)→qux($X)与foo(bar)→qux(bar)同时命中foo(bar)时只应用一次。5.5 原子写入失败绝不半途落盘原生层将每个文件的改动先在内存中暂存pending_writes全部计算通过后才统一落盘。测试ast_edit_does_not_partially_write_when_apply_fails构造了a.ts 可干净改写、b.ts 因嵌套范围重叠而报错的场景断言两个文件在失败后均保持原内容——后一个文件的计算失败不会让前一个文件被部分修改。六、限制与防护帽Limits Caps限制项取值出处文件数上限PI_MAX_AST_FILES默认 1000TS 层$envpos(..., 1000)读取ast-edit.ts原生maxFiles/maxReplacements提供时钳制到至少 1wrapper 不设maxReplacements故替换数实际无上限ast.rs解析错误展示去重后最多PARSE_ERRORS_LIMIT 20条parseErrorsTotal为去重前总数render-utils.ts预览 diff 截断每个before/after仅展示首行截断至 120 字符ast-edit.ts目录扫描隐藏文件开、gitignore 开、默认跳过 node_modulesast.rs当命中maxFiles上限时结果尾部追加Limit reached; narrow paths.TUI 状态栏同步显示 warning。七、错误语义与边界行为parse 失败 ≠ 干净无操作提示词强调 Parse issues → malformed rewrite, not clean no-op——模式编译失败或文件含 tree-sitter 错误节点会被记录为 parse issue 并随结果返回而不是静默跳过整条规则全部编译失败原生ast_edit返回0 替换 parseErrors 已填充的成功结果而非抛错语法错误树文件被跳过含 error node 的文件不会得到部分改写这与ast_grep的报错但仍搜索不同——改写的破坏性决定了它更保守工具拒绝改写外部 URLast_edit只作用于本地文件幂等性不保证foo($A)→foo($A)这类输出等于输入的规则预览为 0 替换而改写结果仍能匹配自身模式的规则在重复调用时可能持续产生替换需 Agent 自行留意。另外ast_edit并不向模型暴露原生层的lang、strictness、selector、maxReplacements、failOnParseError、timeoutMs字段——运行期固定为先预览、smart 严格度、best-effort 解析模式的调用形态模型侧不需要也不应关心这些底层参数docs/tools/ast-edit.md Notes 章节。八、与 Edit 工具的取舍提示词最后给出明确的分工建议For one-off text edits, prefer the Edit tool.即一次性、单点的文本修改 → 用 Edit 工具更轻量直接结构性、跨文件的 codemod重命名 API、迁移 import、删除调试日志、现代语法改造→ 用ast_edit让语法树保证匹配的精确性当文本替换可能误伤注释、字符串、相似子串时优先考虑结构改写。一句话总结这套设计用语法树换确定性用预览-确认换安全性用每语言独立编译换多语言通用性——这正是 oh-my-pi 在代码改造类任务中平衡自动化的效率与误改的代价的核心思路。【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表