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.
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
- Rust lernen, Teil 1: Hallo, Compilergeplant
- Ruff: Linter und Formatter in einem, das Gegenstück für Python
- The Cargo Book
- Clippy-Lints mit Suche und Gruppen
- rustfmt-Konfiguration
- rustdoc: Documentation tests
- rust-analyzer
Tastenkürzel
- j k
- nach unten / oben scrollen
- n p
- nächster / vorheriger Abschnitt
- g G
- Anfang / Ende des Artikels
- t
- Gliederung zeigen
- ?
- diese Hilfe zeigen
- Esc
- schließen
0 Kommentare
Noch keine Kommentare. Sei der/die Erste!
Anmelden um einen Kommentar zu hinterlassen.