以結構方式重構程式碼
本指南說明如何讓代理進行大範圍的機械式變更——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()——而那通常正是你要找的錯誤。沒有任何文字搜尋能表達這一點。
當模式什麼都找不到時
標題為「當模式什麼都找不到時」的章節依序試這兩件事:
- **把它縮小。**先試
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 |
相關內容
標題為「相關內容」的章節- 程式碼智慧——為什麼結構化比對不是多繞幾步的正規表示式
- 內建代理工具——完整的引數清單
- 用程式碼圖搜尋程式碼——符號與影響範圍那一面