以結構方式重構程式碼
本指南說明如何讓代理進行大範圍的機械式改動——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 語法,而在正式組建中一向如此——缺少語法會令伺服器停止,而不是令搜尋降級。
先找出形狀
標題為「先找出形狀」的章節絕不要重寫你沒親眼看過的東西。請代理搜尋:
找出每一處我們對剛做過 null 檢查的可為 null 值呼叫
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 收窄到你真正想改的那一個所屬的目錄,而不是試圖在模式裡表達這個差異。
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 |
相關內容
標題為「相關內容」的章節- 程式碼智能 — 為何結構化匹配不是「多繞幾步的正則表達式」
- 內建代理工具 — 完整的引數清單
- 以程式碼圖搜尋程式碼 — 符號與影響面這一側