Training: relaxed, and pedantic on purpose¶
Level: reference · a template to copy
One line: A whole project to copy — Cargo.toml, clippy.toml, the toolchain and an example — in which nothing stops cargo run, clippy's pedantic and nursery groups plus 31 hand-picked lints explain every line that has a better form, and cargo strict turns those explanations into errors when you want the lesson enforced.
Copy it¶
Then change name in Cargo.toml. Or let the script copy, rename, and make diagnostics name files in full:
For a project that already exists, paste the blocks below into its files, or run lint_profiles.py apply — it adds the lint and profile tables and leaves [package] and [dependencies] alone. Automation has the details, and Production is the strict twin of this page.
The files¶
Every block below is pasted from the template folder by tools/run_examples.py, and CI fails if a block and its file differ — so what you copy here is exactly what the script writes.
Cargo.toml¶
# Training template: relaxed, and pedantic on purpose.
#
# Everything educational is at `warn`, so `cargo run` never stops for style while
# `cargo clippy` -- or RustRover, with Clippy as its linter -- names the idiomatic
# version of each line. The four lints about unused names are off: a practice file
# binds names only to show a form. `cargo strict` (.cargo/config.toml) runs the
# same lints with every warning an error.
#
# Change `name` to your project's folder name; everything else can stay.
[package]
name = "practice"
version = "0.1.0"
edition = "2024"
rust-version = "1.98"
publish = false
[dependencies]
anyhow = "1" # `?` on any error in main, with context
rand = "0.10" # random numbers
regex = "1" # regular expressions
serde = { version = "1", features = ["derive"] } # #[derive(Serialize, Deserialize)]
serde_json = "1" # ...to and from JSON
# Your own code stays unoptimised and debuggable; dependencies are built
# optimised, once, so a regex or a random number costs what it will in release.
[profile.dev.package."*"]
opt-level = 2
[lints.rust]
# Practice-file noise: a name bound only to show a form is not a mistake.
unused_variables = "allow"
unused_imports = "allow"
unused_mut = "allow"
dead_code = "allow"
# Idioms rustc knows and does not warn about by default.
rust_2018_idioms = { level = "warn", priority = -1 }
unsafe_code = "warn"
trivial_casts = "warn"
trivial_numeric_casts = "warn"
unused_qualifications = "warn"
let_underscore_drop = "warn"
unit_bindings = "warn"
redundant_lifetimes = "warn"
unused_lifetimes = "warn"
[lints.clippy]
# The two teaching groups: the idiom std expects, and the lints still in development.
pedantic = { level = "warn", priority = -1 }
nursery = { level = "warn", priority = -1 }
# Panics. Each one points at the API that returns an Option or a Result instead.
unwrap_used = "warn"
expect_used = "warn"
indexing_slicing = "warn"
string_slice = "warn"
panic = "warn"
panic_in_result_fn = "warn"
todo = "warn"
unimplemented = "warn"
unreachable = "warn"
exit = "warn"
# Conversions and arithmetic that say what they do.
as_conversions = "warn"
integer_division = "warn"
lossy_float_literal = "warn"
# Names, patterns and attributes.
shadow_unrelated = "warn"
wildcard_enum_match_arm = "warn"
rest_pat_in_fully_bound_structs = "warn"
ref_patterns = "warn"
allow_attributes_without_reason = "warn"
# Smaller idioms, each with a one-line fix.
if_then_some_else_none = "warn"
iter_over_hash_type = "warn"
verbose_file_reads = "warn"
try_err = "warn"
empty_structs_with_brackets = "warn"
unnecessary_self_imports = "warn"
string_lit_chars_any = "warn"
undocumented_unsafe_blocks = "warn"
map_err_ignore = "warn"
clone_on_ref_ptr = "warn"
rc_buffer = "warn"
mem_forget = "warn"
dbg_macro = "warn"
# Measured and left out:
# min_ident_chars `i`, `x` and `n` are fine names
# arithmetic_side_effects rejects `n + 1`, and changes how everything reads
# shadow_reuse rejects `let line = line.trim();`, which is idiomatic
# let_underscore_must_use rejects `let _ =`, the fix rustc's own message suggests
clippy.toml¶
# Clippy's own settings. Lint LEVELS live in Cargo.toml; this file holds carve-outs
# and thresholds. The MSRV comes from `rust-version` in Cargo.toml.
# Inside #[test] and #[cfg(test)], unwrap, expect, panic, indexing and dbg! are how a
# test says "this must hold", so the lints about them stay quiet there.
allow-unwrap-in-tests = true
allow-expect-in-tests = true
allow-panic-in-tests = true
allow-indexing-slicing-in-tests = true
allow-dbg-in-tests = true
.cargo/config.toml¶
# Cargo settings for this project only.
[alias]
# `cargo strict`: the lints in Cargo.toml with every warning an error, and the four
# unused-name lints back on. Relaxed while you write, strict before you call it done.
# The flags are inline because a file path here is resolved from the directory
# cargo runs in, so `--config some-file.toml` would break in src/.
strict = ["clippy", "--all-targets", "--config", "build.rustflags = ['-W', 'unused', '-D', 'warnings']"]
rust-toolchain.toml¶
# The compiler this project builds with. rustup reads this file and installs the
# version if it is missing. Bump it on purpose, in a commit of its own.
[toolchain]
channel = "1.98.0"
components = ["clippy", "rustfmt", "rust-analyzer"]
src/main.rs¶
//! A practice program the training template has nothing to say about: a die roll
//! from `rand`, words from `regex`, JSON from `serde_json`, and every error passed
//! up with `?` through `anyhow`.
use anyhow::{Context, Result};
use rand::RngExt;
use regex::Regex;
use serde::Serialize;
#[derive(Debug, Serialize)]
struct Roll {
sides: u8,
value: u8,
}
fn main() -> Result<()> {
let value = rand::rng().random_range(1..=6);
let roll = Roll { sides: 6, value };
println!("{}", serde_json::to_string(&roll)?); // {"sides":6,"value":4} -- the value varies
let words = Regex::new(r"\b[a-z]+ing\b").context("the pattern should compile")?;
let text = "reading and writing, then testing the thing";
let found: Vec<&str> = words.find_iter(text).map(|word| word.as_str()).collect();
println!("{found:?}"); // ["reading", "writing", "testing", "thing"]
let total: u32 = "42".parse().context("not a whole number")?;
println!("{}", total + 1); // 43
Ok(())
}
{"sides":6,"value":3}
["reading", "writing", "testing", "thing"]
43
cargo clippy --all-targets and cargo strict both finish with nothing to report.
What it does to a first attempt¶
A first attempt that compiles and runs:
#[derive(Debug, Clone, Copy)]
enum Status {
Draft,
Published,
Archived,
}
fn main() {
let scores = [90, 72, 85];
let i = 1;
println!("{}", scores[i]); // 72
let parsed: i32 = "42".parse().unwrap();
println!("{parsed}"); // 42
let small: u8 = 200;
let wide = small as i64;
println!("{wide}"); // 200
println!("{}", 7 / 2); // 3
let word = "héllo";
println!("{}", &word[0..3]); // hé
for status in [Status::Draft, Status::Published, Status::Archived] {
let shown = match status {
Status::Published => "visible",
_ => "hidden",
};
println!("{status:?}: {shown}"); // Draft: hidden, Published: visible, Archived: hidden
}
}
cargo run runs it and prints every line above. cargo clippy explains eight things about it, as warnings:
| Line | Lint | Clippy's message | The fix |
|---|---|---|---|
| 11 | indexing_slicing ↗ |
indexing may panic | scores.get(i), which returns an Option |
| 13 | unwrap_used ↗ |
used unwrap() on a Result value |
?, with main returning anyhow::Result<()> |
| 16 | needless_type_cast ↗ — from nursery |
this binding is defined as u8 but is always cast to i64 |
convert once, with From |
| 17 | cast_lossless ↗ — from pedantic |
casts from u8 to i64 can be expressed infallibly using From |
i64::from(small) |
| 17 | as_conversions ↗ |
using a potentially dangerous silent as conversion |
the same i64::from(small) |
| 20 | integer_division ↗ |
integer division | 7_i32.div_euclid(2), which says you meant it |
| 23 | string_slice ↗ |
indexing into a string may panic if the index is within a UTF-8 character | word.get(0..3), which returns None instead |
| 28 | wildcard_enum_match_arm ↗ |
wildcard match will also match any future added variants | name them: Status::Draft \| Status::Archived |
cargo strict reports the same eight as errors and fails. The version with every fix applied passes cargo strict with nothing to report:
#[derive(Debug, Clone, Copy)]
enum Status {
Draft,
Published,
Archived,
}
fn main() -> anyhow::Result<()> {
let scores = [90, 72, 85];
let i = 1;
println!("{:?}", scores.get(i)); // Some(72)
let parsed: i32 = "42".parse()?;
println!("{parsed}"); // 42
let small: u8 = 200;
let wide = i64::from(small);
println!("{wide}"); // 200
println!("{}", 7_i32.div_euclid(2)); // 3
let word = "héllo";
println!("{:?}", word.get(0..3)); // Some("hé")
for status in [Status::Draft, Status::Published, Status::Archived] {
let shown = match status {
Status::Published => "visible",
Status::Draft | Status::Archived => "hidden",
};
println!("{status:?}: {shown}"); // Draft: hidden, Published: visible, Archived: hidden
}
Ok(())
}
How loud it is, measured¶
The lint tables above, run over this library's own 627 lesson programs with every level capped at warn: 59 raise nothing. The five lints that fire most:
| Lint | Programs | What the fix teaches |
|---|---|---|
doc_markdown |
347 | an identifier in a doc comment goes in backticks |
missing_const_for_fn |
123 | what const fn is |
unwrap_used |
121 | ?, match and unwrap_or |
shadow_unrelated |
113 | one meaning per name per scope |
indexing_slicing |
107 | .get(i), which returns an Option |
Lesson programs call unwrap and index on purpose, to show what those do, so read this as the order you will meet the lints in rather than as a verdict on the lessons. Five more lints were measured and left out; the last comment in Cargo.toml names four:
| Lint | Programs | Why it is not in the template |
|---|---|---|
min_ident_chars |
512 | i, x and n are fine names |
arithmetic_side_effects |
257 | rejects n + 1, which changes how everything reads |
missing_assert_message |
40 | a message on every assert_eq! in a practice test is noise |
shadow_reuse |
32 | rejects let line = line.trim();, which is idiomatic |
let_underscore_must_use |
19 | rejects let _ =, the fix rustc's own message suggests |
Why each part is there¶
- Four
allows, and nothing else belowwarn. A name bound only to show a form is not a mistake — the four a practice tree turns off.unused_must_useis not among them: an ignoredResultis a dropped error, and it keeps rustc's defaultwarn. warn, notdeny. Training shows every lesson and blocks none of them, and a clippy lint never stopscargo runwhatever its level.cargo strictis the switch:-W unusedputs the four back,-D warningsmakes everything an error. Its flags are inline because a--config some-file.tomlpath is resolved from wherever cargo runs, so it would work at the project root and fail insrc/.nurseryis here and not in production. Lints still in development are useful suggestions and poor gates: atdeny, a toolchain bump can fail code that did not change.[profile.dev.package."*"]. Your code stays unoptimised so a debugger can follow it; dependencies are built optimised once, and everyRegex::newand random number after that runs optimised code.rust-versionandrust-toolchain.toml. The toolchain file picks the compiler;rust-versiontells clippy the oldest Rust the code must build on, so it does not suggest an API that version lacks.- Five dependencies.
anyhow,rand,regex,serdeandserde_jsonare what a practice program reaches for first; delete what you do not use. Each is compiled the first time you build, not on every run. - RustRover shows the
[lints.clippy]half only with Settings → Rust → External Linters set to Clippy — section 1 of the RustRover page. The[lints.rust]half needs nothing.
See also¶
- Production — the strict template, for code that ships
- Clippy — the nine groups, where each setting lives, and the commands
- Automation —
lint_profiles.py new,applyandcheck - Strict clippy lints — the panic set, and what each part of it costs
The outputs, clippy messages and pass/fail results above were captured on 2026-09-14 by running cargo 1.98.0 and clippy in the template and in scratch copies of it; they are verified once, since tools/run_examples.py compiles bare rustc examples and runs neither Cargo nor clippy. The file blocks are the exception: those are checked on every commit. The counts come from running clippy with this template's lint tables over all 627 examples/*.rs on master.
Po polsku¶
To jest szablon treningowy: łagodny, ale celowo drobiazgowy. Wszystkie linty uczące są na poziomie warn, więc cargo run nigdy się nie zatrzymuje, a cargo clippy — albo RustRover z Clippy ustawionym jako linter — przy każdej linijce, którą da się napisać lepiej, podaje tę lepszą postać. Wyłączone są tylko cztery linty o nieużywanych nazwach (unused_variables, unused_imports, unused_mut, dead_code), bo w pliku ćwiczeniowym nazwa związana tylko po to, żeby pokazać formę, nie jest błędem. Kiedy chcesz, żeby lekcja była egzekwowana, uruchom cargo strict: te same linty, a każde ostrzeżenie staje się błędem.
Najprościej skopiować cały katalog template/ i zmienić name w Cargo.toml — albo uruchomić lint_profiles.py new, które zrobi to samo i dopisze pełne ścieżki w komunikatach. Każdy blok na tej stronie jest wklejany z pliku przez tools/run_examples.py, a CI pilnuje, żeby się nie rozjechały, więc to, co kopiujesz, jest dokładnie tym, co zapisuje skrypt. Pierwsza próba z tabeli powyżej działa, ale dostaje osiem uwag: indeksowanie zamiast get, unwrap zamiast ?, rzutowanie as zamiast From, dzielenie całkowite, wycinek napisu w środku znaku UTF-8 i dopasowanie _, które połknie przyszły wariant enuma.
Szukaj po polsku: szablon projektu Rust · Cargo.toml linty · clippy pedantic · clippy.toml · rust-toolchain.toml