The Review Checklist
This is the checklist version of the previous two lessons: one page, six sections, each item phrased as a question you can ask about a specific line of code, each linked to the lesson that explains why the question matters. Keep it open in a second tab during a real review.
Ownership & Borrowing
Section titled “Ownership & Borrowing”- Does every
.clone()have a reason you could state in one sentence (“needed because X still uses the original afterward”)? — Ownership & Moves - Could this
&Vec<T>or&Stringparameter be&[T]or&strinstead? — Slices - Does this
Rc<RefCell<T>>need multiple owners and interior mutability, or would a plain field and a&mutborrow do? — Smart Pointers - Does every
unsafeblock carry a// SAFETY:comment explaining which invariant makes it sound? — The Borrow Checker Model - Does every explicit lifetime annotation add information elision would not already infer? — Lifetimes
- Is
'statichere because the data genuinely lives forever, or because it silenced an error? — Lifetimes - Does
mem::forgetorBox::leakappear, and if so, is the leak intentional and worth the comment explaining it? — Drop and RAII
Types & Traits
Section titled “Types & Traits”- Before reaching for
dyn Trait, does the trait actually needSelf-returning or generic methods that would make it dyn-incompatible anyway? — Trait Objects - Is
impl Traitused only in argument or return position, never as a struct field type? — Generics - Does every
#[derive(Copy)]sit on a type small enough that an implicit copy is actually cheap? — Structs - Is a float (
f32/f64) ever used as aHashMap/HashSetkey, or compared with a bare==? — HashMap
Errors & Panics
Section titled “Errors & Panics”- Does every
.unwrap()/.expect(...)sit on a value that is genuinely infallible here, not user input, I/O, or a network response? — Panics - Does any
matchon an enum have a_ =>arm, and if so, could the enum realistically grow a new variant later? — Pattern Matching - Is
.is_some()/.is_ok()ever followed by a separate.unwrap()whereif let/matchwould do both in one step? — Option - Is
Box<dyn Error>used as a public library return type where a caller might need to match on the specific failure? — Custom Errors - Does any
panic!cover a condition the caller could reasonably want to recover from? — Result
Iterators & Collections
Section titled “Iterators & Collections”- Does any
for i in 0..collection.len()loop useionly to index that same collection? — Iterators - Is
.collect()immediately consumed by another iterator-shaped method (.contains(...),.any(...),.len())? — Iterators - Does any string byte-slice (
&s[a..b]) assume ASCII, with no check thata/bland on acharboundary for non-ASCII input? — Vec and String - Does any
ascast between integer types risk silent truncation on input that isn’t a small literal? — Data Types
Concurrency & Async
Section titled “Concurrency & Async”- Does this
Arc<Mutex<T>>need to outlive the function that spawned its threads, or wouldthread::scoperemove the need forArc? — Threads - Could a
Mutex-protected single integer be a plain atomic instead? — Threads - Is any
std::sync::MutexGuardstill in scope across an.awaitpoint? — Async Intro - Does any
async fncallthread::sleepor another blocking call instead of an async-aware equivalent? — Async Intro - Is
mpsc::channel()(unbounded) used where a producer could realistically outpace its consumer? — Channels
Cargo & Tests
Section titled “Cargo & Tests”- Does every
#[should_panic]specifyexpected = "..."? — Cargo Tests - Does every
#[test]function body contain at least oneassert!/assert_eq!/assert_ne!? — Cargo Tests - Does
cargo clippy -- -D warningsrun clean, and has it actually been run with the AI-smell lints from the tool ladder enabled? — Verification Tools - Is the resolver and
rust-versioninCargo.tomlset deliberately, not left to whatever the default happens to be? — Cargo Tests
What the Compiler Says: Modifying a Vec While Iterating It
Section titled “What the Compiler Says: Modifying a Vec While Iterating It”One checklist item — “does anything look like it could invalidate an iterator mid-loop” — is worth seeing rejected outright, because Rust refuses this at compile time rather than letting it corrupt memory at runtime the way it would in a language with less strict aliasing rules:
// @expect-error E0502fn main() { let mut v = vec![1, 2, 3]; for x in &v { v.push(*x); }}error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable --> src/main.rs:4:9 |3 | for x in &v { | -- | | | immutable borrow occurs here | immutable borrow later used here4 | v.push(*x); | ^^^^^^^^^^ mutable borrow occurs herefor x in &v holds an immutable borrow of v for the whole loop; v.push(*x) needs a mutable borrow, which cannot coexist with it. This specific mistake — appending to a collection while iterating over it — is undefined behavior in several other languages (a resized backing array can leave x pointing at freed or stale memory) and a compile error here instead.
The Copy-Pasteable Short Version
Section titled “The Copy-Pasteable Short Version”For a quick pass or a PR description, this compresses every question above into a phrase:
Ownership: unexplained .clone() / &Vec or &String params / Rc<RefCell> as default / unsafe without SAFETY / decorative lifetimes / 'static-to-silence / mem::forget or Box::leakTypes: dyn where impl/enum would do / impl Trait in a field / Copy on a large struct / float as a HashMap key or compared with ==Errors: unwrap on fallible input / _ => on a growable enum / is_some+unwrap / Box<dyn Error> in a library API / panic! on a recoverable conditionIterators: index loop instead of iterator / needless collect / byte-slice on non-ASCII / as-cast truncation riskConcurrency: Arc<Mutex> where scope/atomic would do / lock held across .await / thread::sleep in async / unbounded channelCargo & tests: should_panic without expected / test with no assert / clippy not run with -D warnings / resolver and rust-version left to defaultThe Grep List
Section titled “The Grep List”Before reading a single function body, run this over the file (or the diff) to see where the checklist’s highest-signal items cluster:
grep -nE 'clone\(\)|unwrap\(\)|expect\(|unsafe|'"'"'static|Rc<RefCell|Box<dyn Error|as u|_ =>|mem::forget|#\[allow|\.lock\(\)\.unwrap\(\)' src/**/*.rsA handful of hits spread across a large file is normal. A cluster of five or six inside one function is the checklist’s fastest way to point you at exactly where to start reading closely.
struct Order { items: Vec<(String, u32)>, // (name, price in cents)}
// &[...] not &Vec<...>, &str not &String -- accepts strictly more callersfn total_cents(items: &[(String, u32)]) -> u32 { items.iter().map(|(_, price)| price).sum()}
fn format_receipt(order: &Order) -> String { let mut lines = Vec::new(); for (name, price) in &order.items { // iterator, not an index loop lines.push(format!("{name}: {price}")); } lines.push(format!("total: {}", total_cents(&order.items))); lines.join("\n")}
fn main() { let order = Order { items: vec![ ("coffee".to_string(), 350), ("bagel".to_string(), 275), ], }; println!("{}", format_receipt(&order));}Compiling…
This one passes every item in the checklist above: no .clone() anywhere, &[(String, u32)] instead of &Vec<...>, a plain iterator instead of an index loop, no .unwrap() on anything fallible (there’s nothing fallible here), and no suppressions. Recognizing what a clean pass looks like is as useful as recognizing what a smell looks like — it’s the baseline the rest of this module’s catalog is measured against.
Mental Model
Section titled “Mental Model”A checklist trades depth for coverage: it will not catch a subtle logic bug specific to this one codebase’s domain, but it reliably surfaces the same two dozen shapes of trouble across any Rust file, in a fixed amount of time, regardless of how familiar you are with this particular codebase. That’s exactly the trade a first pass over unfamiliar (or AI-generated) code should make — establish where the risk is concentrated before spending deep-reading time on any one function.
Trade-offs
Section titled “Trade-offs”Running the grep list and checklist on every review adds a few minutes to a pass that “just reading the diff” wouldn’t spend — and on a diff that turns out to be entirely clean, that time produces no findings at all. The alternative, skipping straight to reading logic, is faster on clean diffs and worse on exactly the diffs where it matters most: a file with several of these patterns clustered together, which unstructured reading is the least likely style of review to notice as a cluster rather than as isolated, individually-forgivable choices.
Common Misconceptions
Section titled “Common Misconceptions”- “A checklist item flagged means the code is wrong.” It means the code deserves a specific question asked of it. Several items (a genuinely necessary
.clone(), a legitimately open-endedBox<dyn Error>) are expected to survive the question un-flagged. - “If clippy is clean, the checklist is redundant.” Several checklist items — an unbounded channel,
Arc<Mutex<T>>where a scope would do, a test with no assertion — have no corresponding clippy lint at all. The checklist and the tool ladder (next lesson) cover different, only partially overlapping ground. - “The grep list catches everything the checklist covers.” It catches the syntactically greppable subset.
_ =>catch-alls, decorative lifetimes, and “does this design need multiple owners” all require reading, not just pattern-matching text.
Reading AI-generated code
Section titled “Reading AI-generated code”Running the grep list over a generated file often surfaces several hits in one function, which is exactly the “cluster” signal the checklist is designed to catch.
🐛 Spot the bug
use std::rc::Rc;use std::cell::RefCell;
struct Cache { data: Rc<RefCell<Vec<String>>>,}
fn add_entry(cache: Rc<RefCell<Vec<String>>>, entry: String) { cache.borrow_mut().push(entry);}
fn print_all(cache: Rc<RefCell<Vec<String>>>) { for item in cache.borrow().iter() { println!("{item}"); }}
fn main() { let cache = Rc::new(RefCell::new(Vec::new())); add_entry(cache.clone(), "first".to_string()); add_entry(cache.clone(), "second".to_string()); print_all(cache.clone());}Show the answer
Grepping this file for the checklist’s terms hits Rc<RefCell three times and .clone() three times, all inside a 20-line file with a single thread and no genuine shared-ownership requirement — main is the only caller, and it calls everything sequentially. Nothing here ever needs two owners alive at once or interior mutability across an alias; a plain &mut Vec<String> passed by mutable reference does the same job with no Rc, no RefCell, and no .clone() anywhere: fn add_entry(cache: &mut Vec<String>, entry: String), called as add_entry(&mut cache, "first".to_string()). The grep list does not tell you this by itself — three Rc<RefCell hits in one small file is the signal that says “go read this function and ask whether it needs to be here,” and reading it is what confirms the answer is no.