跳到內容

以結構方式重構程式碼

本指南說明如何讓代理進行大範圍的機械式變更——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()——而那通常正是你要找的錯誤。沒有任何文字搜尋能表達這一點。

依序試這兩件事:

  1. **把它縮小。**先試 foo($X),再試 if (a) { foo($X); }。本身不是合法程式碼的模式,通常也不是你真正想找的建構。
  2. **指明語言。**只有在 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