跳至內容

以結構方式重構程式碼

本指南說明如何讓代理進行大範圍的機械式改動——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。純文字手段根本無法表達這一點。

依序試兩件事:

  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 收窄到你真正想改的那一個所屬的目錄,而不是試圖在模式裡表達這個差異。

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