Skip to content

Smell catalogue

A smell is a pattern that compiles but makes the code worse: it can crash, it costs performance or maintenance, or it is not idiomatic. AI agents produce these patterns often, usually because they translate Java directly or because they fixed a compiler error the fast way.

Each smell has a permanent ID, from S01 to S38. The IDs never change and are never reused. Use them in review comments and in instructions to your agent: “Fix the S02 and S08 findings in importer.rs.”

  • High: can cause a crash, data loss, a security problem, or a deadlock.
  • Medium: costs performance or maintenance.
  • Low: style. Not idiomatic, but correct.

Every smell page has the same sections: what it looks like, why AI agents produce it, why it is a problem, the better version, when it is acceptable, how to detect it, and what to tell your agent.

Pages are being written milestone by milestone. A row without a link has no page yet.

IDSmellSeverityConcepts
S01clone() to satisfy the borrow checkerA copy of the data is made only to make a borrow error go away.Severity: MediumClone and Copy, Borrowing, Ownership and moves
S02unwrap() / expect() in non-test codeThe program stops (panics) on a failure that the caller could handle.Severity: HighResult, not exceptions, Option, not null
S03Rc<RefCell<T>> object graph (Java translated directly)A Java object graph copied with shared, mutable, reference-counted cells.Severity: MediumSmart pointers, Ownership and moves
S04Arc<Mutex<T>> where no sharing across threads existsThread-safe wrappers in code that only one thread uses.Severity: LowSmart pointers, Threads and shared state
S05Owned parameters (String, Vec<T>) where a borrow is sufficientThe function takes ownership of data that it only reads.Severity: LowOwnership and moves, Borrowing, Strings
S06&String, &Vec<T>, &Box<T> parametersA reference to the container where a slice (&str, &[T]) or &T is more flexible.Severity: LowBorrowing, Strings, Collections
S07Result<T, String> or stringly-typed errorsThe caller cannot match on the kind of error, only read a message.Severity: MediumResult, not exceptions
S08Ignored Result (let _ =, .ok();)An error is thrown away silently.Severity: HighResult, not exceptions
S09Error context lost (map_err(|_| ...))The original error is replaced, so the cause is no longer available.Severity: MediumResult, not exceptions
S10Index-based loops instead of iteratorsfor i in 0..v.len() with v[i], where an iterator is clearer and has no bounds checks.Severity: LowIterators vs streams, Collections
S11collect() then iterate againAn intermediate collection that allocates for no reason.Severity: LowIterators vs streams
S12Getters and setters for every fieldJavaBean accessors that add code but no encapsulation.Severity: LowStructs and impl
S13Trait hierarchy that copies a Java class hierarchyDeep supertrait chains that imitate inheritance.Severity: MediumTraits vs interfaces
S14Deref used to imitate inheritanceA wrapper type that auto-dereferences to a "base class".Severity: MediumTraits vs interfaces, Smart pointers
S15bool flags or string constants instead of an enumStates encoded as flags or strings, so invalid combinations are possible.Severity: MediumEnums and pattern matching
S16Wildcard _ => match arm that hides new variantsThe compiler cannot tell you when a new variant is not handled.Severity: MediumEnums and pattern matching
S17Struct with many Option fields that are really statesWhich fields are set depends on a hidden state that an enum should express.Severity: MediumEnums and pattern matching, Option, not null, Structs and impl
S18Blocking call inside async codeA blocking call stops the runtime worker thread and stalls other tasks.Page not written yet.Severity: HighAsync
S19std::sync::Mutex guard held across .awaitA lock that stays held while the task is suspended, which can deadlock.Page not written yet.Severity: HighAsync, Threads and shared state
S20unsafe without a // SAFETY: comment, or unsafe to avoid the borrow checkerUnchecked code with no written proof that it is correct.Page not written yet.Severity: HighUnsafe and FFI
S21Unnecessary lifetime annotations, or 'static to silence errorsAnnotations that elision makes unnecessary, or a 'static bound added to stop an error.Severity: LowLifetimes
S22Box<dyn Trait> where a generic or enum is sufficientDynamic dispatch and a heap allocation where the set of types is known.Severity: LowTraits vs interfaces, Generics, Smart pointers
S23as casts that can truncate or change signA numeric conversion that silently produces a wrong value.Page not written yet.Severity: HighResult, not exceptions
S24Indexing (v[i], s[a..b]) on untrusted inputOut-of-range input makes the program panic.Severity: HighCollections, Strings
S25#[allow(...)] to silence clippy or compiler warningsA warning is hidden instead of fixed, with no reason given.Severity: MediumTooling
S26Hallucinated crate, API, or feature flagA dependency, function, or feature that does not exist, or does not do what the code expects.Severity: HighModules, crates, and Cargo
S27Outdated idioms (try!, extern crate, lazy_static, old edition)Old patterns that the current standard library or edition replaces.Severity: LowModules, crates, and Cargo, Macros
S28Too many or unknown dependenciesCrates added for small tasks, or from unknown authors.Severity: MediumModules, crates, and Cargo
S29static mut or global mutable stateShared mutable state that is unsafe or hard to test.Page not written yet.Severity: HighThreads and shared state, Unsafe and FFI
S30Mocks for everything; tests with sleepTests that are brittle, slow, or test the mocks instead of the code.Severity: LowTesting
S31Function with too many parameters (no builder or config struct)Long parameter lists that are easy to call in the wrong order.Severity: LowStructs and impl
S32String building with + in a loop, format! used only to convertExtra allocations and noise when building or converting strings.Severity: LowStrings
S33async everywhere, or two async runtimesAsync code with no I/O, or two runtimes in one program.Page not written yet.Severity: MediumAsync
S34Unbounded tokio::spawn or unbounded channelsNo back-pressure, so memory use can grow without limit.Page not written yet.Severity: MediumAsync, Threads and shared state
S35Rc cycles (memory leak)Two values that own each other are never freed.Severity: MediumSmart pointers
S36panic! used for normal errorsAn expected failure stops the program instead of returning an error.Severity: HighResult, not exceptions
S37Primitive types where a newtype gives safety (u64 for IDs)Two different IDs have the same type, so the compiler cannot catch a mix-up.Severity: LowStructs and impl
S38pub on everythingEvery field and function is public, so the API has no boundary.Severity: LowStructs and impl, Modules, crates, and Cargo