Zum Inhalt springen

cat posts/cargo-clippy-rustfmt.md

Cargo, Clippy und rustfmt: das Werkzeug rund um Rust

Was Cargo außer run und test noch kann: check und Release-Builds, rustfmt für einheitlichen Code, Clippy als Linter mit Autofix, Dokumentation mit Doctests und eine CI-Zeile, die alles prüft.

Cargo, Clippy und rustfmt: das Werkzeug rund um Rust

In Teil 1 von Rust lernengeplant hast Du mit cargo new, cargo run und cargo test gearbeitet. Cargo kann mehr, und das meiste davon ist schon installiert: Die Standardinstallation von rustup bringt den Formatter rustfmt, den Linter Clippy und die Dokumentation offline mit.

In Python setzt Du Dir so ein Setup selbst zusammen, etwa aus Ruff, pytest und Sphinx. In Rust ist es einheitlich. Fast jedes Projekt nutzt dieselben Werkzeuge mit denselben Befehlen.

Bauen: check, build, release

$ cargo check            # nur prüfen, keine Programmdatei
$ cargo build            # Debug-Build nach target/debug/
$ cargo build --release  # optimierter Build nach target/release/

cargo check ist der Befehl, den Du am häufigsten brauchst. Er prüft Typen und alle Ausleihen mit &, überspringt aber die Codeerzeugung. Deshalb ist er deutlich schneller als cargo build.

Debug- und Release-Build unterscheiden sich stärker als in den meisten Sprachen. Der Debug-Build ist nicht optimiert und prüft zusätzlich Ganzzahl-Überläufe zur Laufzeit. Ein Release-Build kann um ein Vielfaches schneller sein. Miss Geschwindigkeit deshalb nie im Debug-Build. Die Einstellungen beider Profile lassen sich in der Cargo.toml unter [profile.dev] und [profile.release] ändern, für den Anfang passen die Standardwerte.

rustfmt: ein Stil für alle

$ cargo fmt          # alle Dateien formatieren
$ cargo fmt --check  # nur prüfen, Exit-Code 1 bei Abweichungen

rustfmt ist für Rust, was Black oder ruff format für Python sind. Der Stil ist festgelegt und lässt sich nur wenig konfigurieren. Das ist Absicht: Über Formatierung muss niemand mehr diskutieren, und fremder Rust-Code sieht aus wie Deiner. Wer trotzdem etwas ändern will, legt eine rustfmt.toml an. Die Zeilenbreite liegt standardmäßig bei 100 Zeichen und lässt sich dort zum Beispiel mit max_width = 80 ändern. Viele weitere Optionen gibt es nur in der Nightly-Toolchain.

Clippy: der Linter

Der Compiler meldet Fehler und einige Warnungen. Clippy geht weiter und kennt mehrere hundert Prüfregeln (Lints), die verdächtigen oder umständlichen Code melden. Hier ist absichtlich unidiomatischer Code, wie er beim Umstieg aus Python entsteht:

fn ist_fehler(status: u16) -> bool {
    if (status >= 400) == true { return true; } else { return false; }
}

fn zaehle_fehler(zeilen: &Vec<String>) -> usize {
    let mut anzahl = 0;
    for i in 0..zeilen.len() {
        if zeilen[i].len() == 0 { continue; }
        if ist_fehler(zeilen[i].parse().unwrap()) { anzahl += 1; }
    }
    return anzahl;
}

fn main() {
    let zeilen = vec!["200".to_string(), "404".to_string(), "".to_string()];
    println!("{}", zaehle_fehler(&zeilen));
}

Eine Kleinigkeit vorab: Die Klammern um status >= 400 sind nötig. Rust erlaubt keine verketteten Vergleiche. status >= 400 == true ist ein Compilerfehler, und Pythons 0 <= x < 10 heißt in Rust 0 <= x && x < 10. Clippy schlägt dafür (0..10).contains(&x) vor.

cargo clippy findet in den zwölf Zeilen der beiden Funktionen acht Probleme. Drei davon im Wortlaut:

warning: equality checks against true are unnecessary
 --> src/main.rs:2:8
  |
2 |     if (status >= 400) == true { return true; } else { return false; }
  |        ^^^^^^^^^^^^^^^^^^^^^^^ help: try: `status >= 400`

warning: writing `&Vec` instead of `&[_]` involves a new object where a slice will do
 --> src/main.rs:5:26
  |
5 | fn zaehle_fehler(zeilen: &Vec<String>) -> usize {
  |                          ^^^^^^^^^^^^
  |
help: change this to
  |
5 - fn zaehle_fehler(zeilen: &Vec<String>) -> usize {
5 + fn zaehle_fehler(zeilen: &[String]) -> usize {

warning: the loop variable `i` is only used to index `zeilen`
 --> src/main.rs:7:14
  |
7 |     for i in 0..zeilen.len() {
  |              ^^^^^^^^^^^^^^^
  |
help: consider using an iterator
  |
7 -     for i in 0..zeilen.len() {
7 +     for <item> in &zeilen {

warning: `werkzeug` (bin "werkzeug") generated 8 warnings (run `cargo clippy --fix --bin "werkzeug" -p werkzeug -- ` to apply 6 suggestions)

Die übrigen fünf Meldungen betreffen drei überflüssige return, das if … { true } else { false } und len() == 0 statt is_empty(). Jede Meldung enthält außerdem einen Link auf die Beschreibung des Lints (der Prüfregel). Die habe ich hier weggelassen.

cargo clippy --fix wendet die sicheren Vorschläge selbst an. Es verweigert die Arbeit, wenn das Git-Repository uncommittete Änderungen enthält. Mit --allow-dirty geht es trotzdem. Danach bleiben die beiden Meldungen übrig, bei denen Clippy die Umbauten nicht selbst vornehmen kann: &[String] statt &Vec<String> und die Schleife über die Elemente. Von Hand umgebaut:

fn ist_fehler(status: u16) -> bool {
    status >= 400
}

fn zaehle_fehler(zeilen: &[String]) -> usize {
    let mut anzahl = 0;
    for zeile in zeilen {
        if zeile.is_empty() {
            continue;
        }
        if ist_fehler(zeile.parse().unwrap()) {
            anzahl += 1;
        }
    }
    anzahl
}

Die Schleife über die Elemente ist dieselbe Lektion wie for x in liste statt for i in range(len(liste)) in Python. Den Unterschied zwischen &Vec<String> und &[String] erklärt Teil 4 der Serie.

Lint-Gruppen und Konfiguration

Clippy ordnet seine Lints in Gruppen. correctness meldet Code, der fast sicher falsch ist, und gilt standardmäßig als Fehler: cargo clippy schlägt dann fehl. style, complexity, perf und suspicious warnen. pedantic, nursery, restriction und cargo sind aus. pedantic ist sehr streng, nursery noch in Arbeit, und restriction verbietet gezielt Sprachmittel, die meist völlig in Ordnung sind. Aus dieser Gruppe pickt man sich einzelne Lints heraus. Was in einem Projekt gelten soll, steht seit Rust 1.74 in der Cargo.toml:

[lints.clippy]
pedantic = { level = "warn", priority = -1 }
must_use_candidate = "allow"

Die Zeile mit pedantic schaltet die ganze Gruppe ein. priority = -1 sorgt dafür, dass sie zuerst angewendet wird. Danach greift die Ausnahme für einen einzelnen Lint aus der Gruppe. Ohne die Priorität bricht Clippy mit dem Fehler lint_groups_priority ab: Cargo ignoriert die Reihenfolge der Zeilen in der Tabelle, und es wäre offen, ob Gruppe oder Ausnahme gewinnt. Einzelne Stellen nimmst Du im Code aus, mit #[allow(clippy::needless_range_loop)] direkt über der Funktion.

Dokumentation mit eingebauten Tests

Kommentare mit drei Schrägstrichen sind Dokumentation. Sie stehen über dem Element, das sie beschreiben, und sind in Markdown geschrieben:

/// Prüft, ob ein HTTP-Statuscode einen Fehler meldet.
///
/// Fehler sind alle Codes ab 400, also Client- und Serverfehler.
///
/// ```
/// use werkzeug::ist_fehler;
///
/// assert!(ist_fehler(404));
/// assert!(!ist_fehler(200));
/// ```
pub fn ist_fehler(status: u16) -> bool {
    status >= 400
}

cargo doc --open erzeugt daraus HTML-Seiten im Stil von docs.rs und öffnet sie im Browser. Die Dokumentation aller Abhängigkeiten ist gleich mit dabei, offline.

Der Code-Block im Kommentar ist ein Dokumentationstest (Doctest). cargo test kompiliert und führt ihn aus, ähnlich wie Pythons doctest-Modul, nur ohne dass Du es einschalten musst. Ausgaben vergleicht Rust dabei nicht: Der Test besteht, wenn das Beispiel kompiliert und jedes assert! hält.

Doctests (Dokumentationstests) gibt es nur für Bibliotheken. Lege dafür neben src/main.rs eine src/lib.rs an, Cargo erkennt beide automatisch. Die Funktion muss dort mit pub öffentlich sein, denn der Doctest bindet sie wie ein fremdes Paket über use werkzeug::ist_fehler ein. Mehr zu lib.rs folgt in Teil 10 der Serie. Ein falsches Beispiel fällt sofort auf. Hier behauptete die Doku zu zaehle_fehler, unter den Codes 200 und 500 seien zwei Fehler:

   Doc-tests werkzeug

running 2 tests
test src/lib.rs - ist_fehler (line 7) ... ok
test src/lib.rs - zaehle_fehler (line 19) ... FAILED
...
assertion `left == right` failed
  left: 1
 right: 2

Ein veraltetes Beispiel in der Doku lässt damit die Tests scheitern.

Für Rust selbst gibt es ebenfalls eine Offline-Doku. rustup doc --book öffnet das offizielle Buch „The Rust Programming Language“, rustup doc --std die Referenz der Standardbibliothek.

Abhängigkeiten

$ cargo add clap --features derive
$ cargo remove clap

cargo add trägt eine Abhängigkeit in die Cargo.toml ein. Die genauen Versionen aller Abhängigkeiten landen in der Cargo.lock, ähnlich wie bei uv in der uv.lock. Pakete (Crates) kommen von crates.io. Eine virtuelle Umgebung brauchst Du nicht. Jedes Projekt baut seine Abhängigkeiten in sein eigenes target/-Verzeichnis, Cargo hält dafür nur einen gemeinsamen Download-Cache.

Im Editor: rust-analyzer

rust-analyzer ist der Sprachserver (Language Server) für Rust. Für VS Code gibt es die gleichnamige Erweiterung. Andere Editoren binden ihn über das Language Server Protocol (LSP) ein. Er zeigt viele Fehler schon beim Tippen, die vollständigen Meldungen von cargo check nach jedem Speichern, dazu abgeleitete Typen als graue Einblendungen und Vorschläge zum Umbauen. Eine Einstellung lohnt sich: Mit "rust-analyzer.check.command": "clippy" meldet der Editor beim Speichern auch die Lints von Clippy, nicht nur die Fehler des Compilers.

Alles zusammen in der CI

cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test

-D warnings macht aus jeder Warnung einen Fehler, ohne dass Du das im Code festschreiben musst. --all-targets prüft auch Tests und Beispiele. Lokal bleiben Warnungen Warnungen, in der CI hält diese Zeile den Build an.

Weiterlesen

0 Kommentare

Noch keine Kommentare. Sei der/die Erste!