Danh sách bài viết

Bài 269: clippy — Lint Cảnh Báo Idiom

Bài 269 của series Rust Cơ Bản — rustc chỉ phát warning khi code không an toàn hoặc không build được. Còn rất nhiều thứ khác — clone thừa, match một nhánh, closure không cần thiết, vòng lặp viết kiểu C — code vẫn compile bình thường nhưng không idiomatic. Đó là phần việc của clippy: linter chính thức của Rust, được Rust team duy trì cùng compiler, hiện có hơn 600 lint chia thành 6 category. Bài này giới thiệu cách chạy cargo clippy, ý nghĩa 6 category (correctness, suspicious, style, complexity, perf, pedantic), 3 lint hay gặp với ví dụ before/after, cách bật/tắt từng lint qua attribute #[allow] / #[warn] / #[deny] trên function/module/crate, cờ -D warnings để CI fail khi có warning, và cargo clippy --fix tự sửa các lint có suggestion rõ ràng.

10/06/2026
10 phút đọc
1 lượt xem
1

Mục Tiêu Bài Học

Sau bài học, bạn sẽ:

  • Hiểu vai trò của clippy như linter idiom bổ sung cho rustc — phát hiện những thứ compile được nhưng không idiomatic.
  • Chạy được cargo clippy trên project, đọc output, hiểu các trường lint name, severity, source span, suggestion.
  • Phân biệt 6 category chính: correctness, suspicious, style, complexity, perf, pedantic — biết category nào bật mặc định và category nào phải opt-in.
  • Sửa được 3 lint kinh điển: needless_clone, single_match, redundant_closure.
  • Dùng attribute #[allow(clippy::...)], #[warn(...)], #[deny(...)] ở mức function, module, hay crate để tinh chỉnh severity của từng lint.
  • Cấu hình CI với cargo clippy -- -D warnings để fail khi có bất kỳ warning nào — phổ biến trên GitHub Actions, GitLab CI.
  • Dùng cargo clippy --fix để tự sửa các lint có suggestion machine-applicable.
2

clippy Là Gì

clippy là linter chính thức của Rust, host trên repo rust-lang/rust-clippy cùng tổ chức với rustc. Tên lấy cảm hứng từ con kẹp giấy "Clippy" của Microsoft Office — một trợ lý hiện ra để góp ý dù không ai hỏi. Khác con kẹp giấy huyền thoại đó, clippy ở đây thực sự hữu ích: hiện 2026 đã có hơn 600 lint, mỗi cái nhắm vào một anti-pattern hoặc cơ hội cải thiện cụ thể trong code Rust.

Khác biệt với rustc: compiler chỉ warning khi code có vấn đề về correctness ở mức ngôn ngữ — biến không dùng, import thừa, mismatched type, dead code. Còn các thứ kiểu "biểu thức này quá phức tạp", "viết theo idiom Rust sẽ ngắn hơn", "có cách viết nhanh hơn cho cùng kết quả" thì compiler không can thiệp. Clippy lấp đúng khoảng trống đó: là layer suggestion về idiom và performance, complementary với compiler warning.

Vì là tool chính thức, clippy được phân phối qua rustup — không cần cài thêm crate. Component clippy đi kèm toolchain mặc định khi bạn cài rustup-init; nếu thiếu thì rustup component add clippy là xong. Phiên bản clippy luôn pin chặt với phiên bản rustc trong cùng channel để tránh mismatch khi parse cú pháp mới.

3

cargo clippy Chạy Như Thế Nào

Lệnh tối giản:

cargo clippy

Tương tự cargo check ở chỗ chỉ analyze + typecheck, không link binary — nhanh hơn full build. Khác là sau khi compiler chạy xong, clippy chạy thêm pass riêng quét toàn bộ AST và HIR, áp các lint mà nó biết. Output điển hình:

$ cargo clippy
    Checking my-app v0.1.0 (/Users/you/my-app)
warning: redundant clone
  --> src/main.rs:8:15
   |
8  |     let y = x.clone();
   |               ^^^^^^^^ help: remove this
   |
   = note: `#[warn(clippy::redundant_clone)]` on by default
   = help: for further information visit
           https://rust-lang.github.io/rust-clippy/master/index.html#redundant_clone

warning: `my-app` (bin "my-app") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s

Đọc output này có 4 phần:

  • Tên lint: clippy::redundant_clone — namespace clippy:: phân biệt với lint của rustc.
  • Source span: file + line + cột chỉ chính xác chỗ vi phạm.
  • Help suggestion: cách clippy đề xuất sửa. Đôi khi là "remove this", đôi khi là replacement đầy đủ.
  • Link tài liệu: URL tới trang giải thích lint chi tiết — vì sao có lint này, ví dụ tốt/xấu, exception khi nào nên #[allow].

Một số flag hữu ích cùng cargo clippy:

  • cargo clippy --all-targets — lint cả test, example, bench, không chỉ lib/bin.
  • cargo clippy --workspace — chạy trên toàn bộ workspace.
  • cargo clippy --release — chạy với profile release (một số lint chỉ trigger ở config tối ưu).
  • cargo clippy -- -W clippy::pedantic — bật thêm category pedantic (mặc định off). Lưu ý dấu -- ngăn cách arg cargo và arg rustc.
4

6 Categories Của Lint

Mỗi lint thuộc đúng một category, có severity mặc định riêng:

  • correctness (default: deny) — code chắc chắn sai, hầu như luôn là bug. Ví dụ: almost_swapped (viết a = b; b = a; quên temp variable). Đây là category bắt buộc — clippy treat như compile error.
  • suspicious (default: warn) — code có thể sai, đa số trường hợp là nhầm. Ví dụ: assign_op_pattern (viết a = a + b thay vì a += b).
  • style (default: warn) — code chạy đúng nhưng không idiomatic, có cách viết ngắn/rõ hơn theo convention community. Ví dụ: single_match, redundant_field_names.
  • complexity (default: warn) — code phức tạp không cần thiết. Ví dụ: needless_return, option_map_unit_fn.
  • perf (default: warn) — code chạy chậm hơn cần thiết. Ví dụ: redundant_clone, or_fun_call (lười evaluate qua or_else).
  • pedantic (default: allow) — lint rất nghiêm khắc, opinionated, đôi khi false-positive cao. Phải opt-in bằng #[warn(clippy::pedantic)] hoặc flag CLI. Ví dụ: must_use_candidate, missing_errors_doc.

Ngoài 6 chính, còn có nursery (lint thử nghiệm chưa stable), cargo (lint cho Cargo.toml), và restriction (lint cấm hẳn một số construct cho codebase đặc thù — vd cấm panic!). Đa số người dùng chỉ cần biết 6 category chính.

Severity mặc định nghĩa là: chạy cargo clippy mà không tùy chỉnh gì, các category correctness, suspicious, style, complexity, perf tự động được kích hoạt ở mức warn (riêng correctnessdeny). Pedantic phải bật thủ công.

5

Ví Dụ Lint Phổ Biến

needless_clone (perf)

Clone không cần thiết — value đã không còn dùng sau đó, có thể move trực tiếp.

// Trước
fn greet(name: String) {
    println!("Hello, {}", name);
}

fn main() {
    let name = String::from("An");
    greet(name.clone());   // warning: redundant clone
}
// Sau
fn main() {
    let name = String::from("An");
    greet(name);           // move trực tiếp, không alloc thừa
}

single_match (style)

match chỉ care đúng 1 nhánh — viết if let ngắn và rõ ý hơn.

// Trước
fn print_some(opt: Option<i32>) {
    match opt {
        Some(n) => println!("{}", n),
        _ => (),                   // warning: you seem to be trying to use match
    }                              //          for an equality check. Consider using if let
}
// Sau
fn print_some(opt: Option<i32>) {
    if let Some(n) = opt {
        println!("{}", n);
    }
}

redundant_closure (complexity)

Closure chỉ wrap function call mà không thêm gì — pass function trực tiếp.

// Trước
let names = vec!["an", "binh", "cuong"];
let upper: Vec<String> = names.iter()
    .map(|s| s.to_string())        // warning: redundant closure
    .collect();
// Sau
let upper: Vec<String> = names.iter()
    .map(ToString::to_string)      // pass method path trực tiếp
    .collect();

Cả 3 lint trên bật mặc định — không cần config gì, chạy cargo clippy trên codebase có pattern này là thấy ngay.

6

allow / warn / deny Attribute

Đôi khi bạn đồng ý với 99% suggestion của clippy nhưng 1 case cụ thể là intentional. Dùng attribute để chỉnh severity từng lint ở scope cần thiết:

  • #[allow(clippy::lint_name)] — tắt lint, không warning.
  • #[warn(clippy::lint_name)] — bật ở mức warning (default cho hầu hết).
  • #[deny(clippy::lint_name)] — coi như error, build fail.
  • #[forbid(clippy::lint_name)] — như deny nhưng không thể override bằng #[allow] ở scope con.

Attribute đặt ở 3 scope:

// 1. Trên function — chỉ áp dụng cho function này
#[allow(clippy::too_many_arguments)]
fn build_request(
    method: &str, url: &str, header_auth: &str,
    header_accept: &str, body: &[u8], timeout_ms: u64,
    retry: u32, follow_redirect: bool,
) {
    // ...
}

// 2. Trên module — áp dụng cho tất cả item trong module
#[allow(clippy::module_name_repetitions)]
mod parser {
    pub struct ParserError;
    pub struct ParserResult;
}

// 3. Trên crate (đầu file lib.rs hoặc main.rs) — toàn crate
#![warn(clippy::pedantic)]      // bật pedantic toàn crate
#![allow(clippy::module_name_repetitions)]
#![deny(clippy::unwrap_used)]   // cấm .unwrap() toàn crate

Quy tắc inheritance: attribute ở scope rộng hơn (crate) là default; scope hẹp hơn (function) override. Một idiom phổ biến: bật pedantic toàn crate, rồi #[allow] những lint pedantic mà team đồng ý "không cần care" — vừa nghiêm khắc vừa thực dụng.

Có thể group nhiều lint trong cùng attribute: #[allow(clippy::needless_clone, clippy::redundant_closure)]. Nếu muốn cấp granular hơn nữa, dùng clippy.toml ở root project — config từng lint với threshold (vd too-many-arguments-threshold = 10) — sẽ bàn sâu trong bài config riêng.

7

-D warnings Trong CI

Mặc định cargo clippy chỉ in warning rồi exit code 0 — CI vẫn green. Không ai sửa, warning tích lũy hàng trăm, mất ý nghĩa. Giải pháp: ép CI fail khi có bất kỳ warning nào bằng flag -D warnings:

cargo clippy --all-targets --workspace -- -D warnings

Dấu -- tách arg của cargo với arg truyền cho rustc (vì clippy chạy như rustc driver). -D warnings nghĩa là "deny tất cả lint nào đang ở mức warn" — bao gồm cả warning từ rustc lẫn từ clippy. Có warning → exit code khác 0 → CI fail.

Step GitHub Actions điển hình:

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy
      - run: cargo clippy --all-targets --workspace -- -D warnings

Vài lưu ý thực tế:

  • Lần đầu áp dụng -D warnings trên codebase cũ thường lộ ra hàng chục warning tồn đọng. Hai cách xử lý: (a) sửa hết một lần rồi commit; (b) sửa dần, tạm #[allow(...)] ở file/function cụ thể với TODO ticket. Tránh #![allow(clippy::all)] ở crate root — vô hiệu hoá toàn bộ giá trị của clippy.
  • CI nên chạy clippy tách step với build test, để khi lint fail không phải đợi build xong mới biết.
  • Local dev: dùng alias cargo cl ánh xạ tới cargo clippy --all-targets -- -D warnings trong ~/.cargo/config.toml để chạy nhanh trước commit, đồng bộ với CI.
8

cargo clippy --fix Tự Sửa

Hầu hết lint có suggestion dạng machine-applicable — clippy biết chính xác đoạn code mới sẽ trông như thế nào. Với những lint này, cargo clippy --fix tự sửa luôn:

$ cargo clippy --fix --allow-dirty --allow-staged
    Checking my-app v0.1.0
      Fixed src/main.rs (2 fixes)
      Fixed src/lib.rs (1 fix)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s

Mặc định --fix yêu cầu working tree clean (không có thay đổi chưa commit) để khi sửa sai bạn có thể git checkout revert. Trong dev flow thực, dùng --allow-dirty--allow-staged để cho phép sửa khi có thay đổi đang dở.

Lưu ý:

  • Không phải lint nào cũng auto-fix được. Lint kiểu "function này quá dài" không có suggestion cụ thể — chỉ in warning, không sửa.
  • Một số fix machine-applicable nhưng kéo theo behavior change subtle (vd lint let_and_return bỏ binding tạm có thể ảnh hưởng drop order). Luôn cargo test sau khi --fix.
  • Có thể combine với rustfmt: cargo clippy --fix && cargo fmt — clippy sửa logic, rustfmt format lại layout. Đây là một bước trong pre-commit hook phổ biến.
  • cargo fix (không có clippy) chỉ fix warning của rustc, không bao gồm lint clippy. Hai tool độc lập.
9

Tổng Kết

  • clippy là linter chính thức của Rust, 600+ lint cho idiom + performance, complementary với warning của rustc.
  • Cài qua rustup component add clippy; chạy bằng cargo clippy tương tự cargo check nhưng có thêm pass lint.
  • 6 category chính: correctness (deny mặc định), suspicious, style, complexity, perf (warn mặc định), pedantic (allow mặc định, phải opt-in).
  • Lint phổ biến: needless_clone (perf), single_match (style → dùng if let), redundant_closure (complexity → pass function path trực tiếp).
  • Attribute #[allow] / #[warn] / #[deny] đặt ở function, module hoặc crate để tinh chỉnh từng lint. #[forbid] không thể override.
  • CI: cargo clippy --all-targets --workspace -- -D warnings để fail khi có bất kỳ warning nào.
  • cargo clippy --fix tự sửa các lint có suggestion machine-applicable. Dùng --allow-dirty trong dev flow. Luôn chạy test sau khi fix.
  • Pair với rustfmt: clippy sửa logic, rustfmt format layout — workflow chuẩn pre-commit.
10

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. Vì sao clippy được gọi là "complementary" với rustc warning thay vì "thay thế"? Nêu 2 ví dụ thứ rustc warning nhưng clippy không, và 2 ví dụ ngược lại.
  2. Bạn chạy cargo clippy trên project mới, hoàn toàn không có warning nào hiện ra. Đồng nghiệp khẳng định "code này pedantic chắc chắn có lỗi". Vì sao output rỗng, và bạn làm gì để xác minh?
  3. Function build_request nhận 8 tham số (intentional vì API external). Clippy báo too_many_arguments. Bạn không muốn refactor, không muốn tắt lint toàn crate. Cách giải quyết tinh tế nhất?
  4. Codebase legacy đang có 200 clippy warning. Manager muốn bật -D warnings trong CI. Đề xuất kế hoạch migration không block PR mới mà vẫn tiến triển dần.
  5. Sau khi chạy cargo clippy --fix, một test trước đó pass giờ fail. Lý do có thể là gì? Bước debug đầu tiên?
  6. Sự khác biệt giữa cargo fixcargo clippy --fix? Khi nào nên chạy cả hai?
Đáp án
  1. rustc focus vào correctness ngôn ngữ (type, borrow, unused), không đụng vào idiom/perf. Clippy focus vào idiom + perf, không lặp lại việc của compiler. Ví dụ rustc báo nhưng clippy không: unused_variables, dead_code. Ví dụ clippy báo nhưng rustc không: needless_clone, single_match. Hai layer độc lập, đều cần.
  2. pedantic category mặc định là allow. Để bật cần thêm #![warn(clippy::pedantic)] ở đầu lib.rs/main.rs, hoặc chạy cargo clippy -- -W clippy::pedantic. Sau khi bật pedantic, thường lộ ra nhiều lint kiểu thiếu #[must_use], doc thiếu section Errors, casting truncate ngầm...
  3. Đặt #[allow(clippy::too_many_arguments)] ngay trên function build_request kèm comment giải thích lý do (vd "match shape của API X"). Scope hẹp, không ảnh hưởng các function khác. Nếu pattern lặp lại trên nhiều function, có thể đặt ở mức module thay vì crate.
  4. Plan: (a) thêm step CI chạy cargo clippy nhưng không -D warnings để theo dõi số warning. (b) Cài cấu hình "warning ratchet" — số warning không được tăng so với baseline trên main branch (có thể implement bằng wrapper script đếm). (c) Sửa dần theo file/module, mỗi PR sửa một phần kèm #![deny(clippy::<lint>)] ở module đã clean để chống regression. (d) Khi tổng warning = 0, bật -D warnings chính thức.
  5. Lý do thường gặp: clippy fix thay đổi behavior subtle. Ví dụ let_and_return bỏ binding tạm có thể ảnh hưởng drop order nếu binding đó là MutexGuard hoặc File. collapsible_if đôi khi rút gọn quá tay làm thay đổi short-circuit semantics. Bước debug: git diff xem fix nào áp dụng, đối chiếu test fail với fix nào ảnh hưởng cùng file, revert fix đó và xác nhận test pass lại — sau đó #[allow] cho đoạn code đó.
  6. cargo fix sửa warning của rustc (vd unused import, deprecated syntax migration giữa edition). cargo clippy --fix sửa lint của clippy. Hai tool độc lập, hai tập warning khác nhau. Nên chạy cả hai trong pre-commit hook: cargo fix --allow-dirty && cargo clippy --fix --allow-dirty && cargo fmt — rustc fix trước (sửa import, deprecation), clippy fix tiếp (idiom), rustfmt cuối (layout). Sau đó chạy test để chắc chắn không vỡ.
11

Bài Tiếp Theo

Bài 270: rust-analyzer — Features Trong VS Code / RustRover — bài tiếp đi sâu vào rust-analyzer, language server chính thức của Rust: các tính năng inlay hint, code action, expand macro, view HIR/MIR, run/debug Codelens, cách tinh chỉnh qua rust-analyzer.toml, và pattern dùng chung với cargo clippy qua setting rust-analyzer.check.command = "clippy" để IDE hiển thị lint clippy ngay trong editor.