15.2 使用 Clippy 檢查程式碼品質#
一個專屬的 lint 工具#
Rust 編譯器 rustc 本身就會發出警告,例如未使用的變數、未到不了的程式碼分支。這些警告聚焦在正確性:它們要嘛指出程式碼可能有錯,要嘛指出程式碼毫無疑問是錯的。但 rustc 的警告集合非常保守,它不會主動告訴你「這段寫法有更簡潔的等價形式」。
Clippy 是 Rust 官方的 lint 工具,專門補上這一塊。它以 rustc 的外掛形式運作,同時執行 rustc 本身的檢查與額外的 lint 集合,涵蓋可讀性、慣例寫法與潛在的效能問題。它的定位是與編譯器警告互補,而非取代。
cargo clippy
cargo clippy --fix # 自動修正部分 lint
cargo clippy -- -D warnings # 把警告視為錯誤
cargo clippy 隨 rustup 工具鏈一同安裝,不需要額外下載。安裝後在專案根目錄執行即可,它會先檢查整個 crate,再逐條列出 lint。輸出格式與編譯器警告一致,包含檔案、行號、程式碼片段與說明。
warning: this expression creates a reference which is immediately dereferenced by the compiler
--> src/lib.rs:10:20
|
10 | match &items[0] {
| ^^^^^^^^^ help: change this to: `match items[0]`
|
= note: `#[warn(clippy::needless_borrow)]` on by default
lint 名稱 clippy::needless_borrow 讓你可以精準地允許或禁止單一條規則,這在下面討論如何控制檢查強度時非常重要。
lint 群組#
Clippy 的規則依性質分成數個群組,可以整組開啟或關閉。
| 群組 | 定位 | 預設 | 典型例子 |
|---|---|---|---|
correctness | 幾乎確定是錯誤的程式碼 | 開啟 | 忘記 ?、錯誤的型別轉換 |
style | 違反慣例寫法 | 開啟 | needless_return、redundant_clone |
complexity | 可以更簡單 | 開啟 | single_match、let_and_return |
perf | 可能的效能問題 | 開啟 | redundant_clone、useless_vec |
pedantic | 允許更多寫法,風格建議 | 關閉 | must_use_candidate、missing_errors_doc |
nursery | 尚在培育中的規則,會變動 | 關閉 | missing_const_for_fn |
cargo | 套件清單相關 | 開啟 | multiple_crate_versions |
預設的行為是:correctness、style、complexity、perf、cargo 開啟,pedantic 與 nursery 關啟。
常見的 lint 與它們的意義#
下表列出初學者最常遇到、也最容易理解的幾條規則。
| lint | 觸發的情況 | 建議的修正 |
|---|---|---|
needless_return | 函式最後一行是 return expr; | 直接寫 expr |
redundant_clone | 對不會再使用的值呼叫 clone() | 移除該次複製 |
let_and_return | 先綁定再立刻 return | 直接回傳 |
single_match | 只匹配一個分支的 match | 改用 if let |
manual_map | 以 match 實作 Option::map | 改用 .map() |
useless_vec | 只用 vec! 建立空向量 | 改用 Vec::new() |
collapsible_if | if 巢狀在 if 內部 | 合併條件 |
needless_borrow | 對已經是引用的值再加 & | 移除額外的 & |
question_mark | 手寫 match 做錯誤傳播 | 改用 ? 運算子 |
type_complexity | 型別嵌套過深 | 引入型別別名 |
這些 lint 的共同點是:它們指出的是等價寫法,不是錯誤。程式碼在觸發 lint 時依然是正確的,只是有更清楚的表達方式。
完整的前後對照#
下面這段程式碼會觸發多條 lint:
pub fn find_largest(items: &Vec<i32>) -> Option<i32> {
let mut largest = None;
for item in items.iter() {
match largest {
Some(current) => {
if *item > current {
largest = Some(*item);
}
}
None => {
largest = Some(*item);
}
}
}
return largest;
}
執行 cargo clippy 會得到如下診斷:
warning: this argument creates a reference which is immediately dereferenced by the compiler
--> src/lib.rs:2:28
|
2 | pub fn find_largest(items: &Vec<i32>) -> Option<i32> {
| ^^^^^ help: change this to: `&[i32]`
|
= note: `#[warn(clippy::ptr_arg)]` on by default
warning: you probably don't need to clone this
--> src/lib.rs:8:28
|
8 | largest = Some(*item);
| ^^^^^ help: use `item` here instead
修正後的版本更短,也更符合 Rust 的慣例:
pub fn find_largest(items: &[i32]) -> Option<i32> {
let mut largest = None;
for &item in items {
if item > largest.unwrap_or(i32::MIN) {
largest = Some(item);
}
}
largest
}
值得注意的是 &Vec<i32> 改成 &[i32] 這件事。Clippy 的 ptr_arg 會提醒你:接受 &Vec<T> 限制了呼叫端必須提供一個真正的 Vec,而接受 &[T] 則同時接受 Vec、陣列 與切片,介面更有彈性。這類建議往往不只是風格問題,還關乎 API 的可用性。
為什麼有些規則預設關閉#
pedantic 群組裡的建議有時不符合團隊的風格偏好。例如 must_use_candidate 會建議所有回傳 Result 的公開函式都加上 #[must_use],這在風格上合理,但某些專案會覺得過度干擾。missing_errors_doc 要求錯誤回傳型別的文件寫明可能的錯誤,也往往過於嚴格。
nursery 群組的規則仍在演進,行為可能隨版本改變,適合在自己的專案裡試用,但不建議在教學或新手專案中全面開啟。
允許特定 lint 的時機#
當某條規則的建議不適合你的情況,可以用 #[allow] 屬性局部關閉:
#[allow(clippy::too_many_arguments)]
pub fn connect(host: &str, port: u16, user: &str, pass: &str, timeout: u64) -> bool {
true
}
更好的做法是先嘗試重構讓規則自然滿足,例如把參數改為一個結構體。屬性也可以加在 crate 層級:
#![allow(clippy::module_name_repetitions)]
濫用 #[allow] 是危險的:它會讓真正的問題被永久隱藏。實務上建議每次允許時,在程式碼中留下一行註解說明原因,並且盡量把允許範圍限制在最小的區塊。另一個選擇是在 Cargo.toml 中統一管理:
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
module_name_repetitions = "allow"
Cargo 的 [lints] 表格(Rust 1.74 起可用)讓 lint 設定成為專案設定檔的一部分,比散落在各處的屬性更容易檢視與稽核。
與 rustfmt 的分工#
rustfmt 與 Clippy 經常被混為一談,但兩者處理的是不同層面的問題。rustfmt 管的是格式:縮排、逗號位置、換行方式。它不看語意,只依一套明確的規則把程式碼排整齊,任何兩位開發者跑 rustfmt 都會得到相同的結果。Clippy 管的是語意與慣例:它讀懂程式碼的行為,判斷某種寫法是否冗餘、是否可能出錯、是否可以用更清楚的形式表達。
兩者是互補而非競爭的。一個合理的分工是:rustfmt 保證外觀一致,Clippy 保證寫法合理。可以放心地讓 rustfmt 自動改動空白與換行,因為它不改變語意;而 Clippy 的修正建議通常需要人來判斷,因為它可能改變程式的結構。
cargo fmt
cargo fmt -- --check # 只檢查不修改,適合放進 CI
在持續整合中把 Clippy 當成必要檢查#
在 CI 中,Clippy 應該與編譯、測試並列為一道關卡。最常見的設定是把警告升級為錯誤:
- name: Clippy
run: cargo clippy --all-targets --all-features -- -D warnings
--all-targets 會連測試程式碼與範例一起檢查,這很重要,因為測試碼碼同樣需要可讀性。-D warnings 則讓任何 lint 都直接讓建置失敗,避免警告在長期累積後被忽略。
如果專案使用了 pedantic,常見的實務做法是開啟整組、再個別允用少數不符合團隊習慣的規則,而不是全部預設關閉。
與編譯器警告的差異#
回到最初的定位:編譯器關注正確性,Clippy 關注可讀性與慣例。rustc 會告訴你「這個變數永遠不會被讀取」,因為那是明確的問題;它不會告訴你「這個函式可以寫成更簡潔的形式」,因為那涉及風格判斷。
兩者的關係不是競爭,而是層次。rustc 的警告是語言層面的必然推論,Clippy 的建議是社群層面的經驗累積。因此教學情境中應先讓學生看懂編譯器警告,再引入 Clippy。學生若連「未使用的變數」代表什麼都還沒掌握,就直接面對上百條 Clippy 規則,只會學會忽略警告訊息。先建立對編譯期回饋的信任,之後的工具才有意義。
同樣地,cargo clippy 輸出的訊息量本身就可能淹沒重點。建議教學專案初期只開啟預設群組,並把每次修正都當成一次練習:讀懂訊息、判斷建議是否合理、動手修改、理解差異。
flowchart TD
A["cargo build / cargo check"] --> B["rustc 編譯器警告"]
B --> C["聚焦正確性:未使用變數、死程式碼"]
C --> D["建立對編譯期回饋的基本信任"]
D --> E["cargo clippy"]
E --> F["style / complexity / perf 群組"]
F --> G{"建議是否合理?"}
G -- 合理 --> H["修改並理解原因"]
G -- 不適合團隊 --> I["局部 #[allow] 並註明理由"]
H --> J["cargo fmt 統一格式"]
I --> J
J --> K["cargo test 驗證行為未變"]
K --> L["CI:cargo clippy -- -D warnings"]最後的 cargo test 那一環不能省略。Clippy 的修正本質上是改寫程式碼,即使是最保守的建議也可能有隱蔽的副作用。讓測試套件在每一次風格修正後依然全數通過,才能確認這次修改只是改變了寫法而沒有改變行為——這也正是下一節「自動生成 API 文件」所要呼應的:文件的正確性同樣需要類似的紀律來維護。