S38: pub on everything
What it looks like
Section titled “What it looks like”An account with an overdraft rule. Every field and every method is pub:
pub struct Account { pub balance_cents: i64, pub overdraft_limit_cents: i64, pub history: Vec<i64>,}
impl Account { pub fn withdraw(&mut self, cents: i64) -> bool { if self.balance_cents - cents < -self.overdraft_limit_cents { return false; } self.apply(-cents); true }
pub fn apply(&mut self, delta: i64) { self.balance_cents += delta; self.history.push(delta); }}Why AI agents produce it
Section titled “Why AI agents produce it”- It fixes privacy errors. When code in another module cannot reach a field (error E0616) or a function (E0603), adding
pubis the quickest fix. The agent adds it and moves on. - It is the opposite habit from Java, applied by mistake. In Java, a field without a modifier is visible in its package. In Rust, an item without
pubis private to its module. An agent that hits “private” errors often makes everythingpubto be safe. - Generated examples are small. In a one-file example,
pubcosts nothing. In a real crate, it is part of the API.
Why it is a problem
Section titled “Why it is a problem”- The rule is not enforced.
withdrawchecks the overdraft limit. Butbalance_centsispub, so any code can set it to any value.applyispub, so any code can change the balance without a check.historyispub, so the history can disagree with the balance. The testworse_lets_any_code_break_the_rulessets the balance directly and callsapply, and after that the history no longer matches the balance. - A negative
centsis a deposit.withdraw(-500)adds money, because the parameter isi64. Nothing in the type stops it. - The API has no boundary. In a library, every
pubitem is a promise. Changing or removing it later is a breaking change for users of the crate. - Reviewers cannot see what is internal. When everything is
pub, a reader cannot tell the entry points from the helpers.
The better version
Section titled “The better version”WorseAll fields
pub, apubhelper that skips the check, andi64amounts that can be negative.pub struct Account {pub balance_cents: i64,pub overdraft_limit_cents: i64,pub history: Vec<i64>,}impl Account {pub fn withdraw(&mut self, cents: i64) -> bool {if self.balance_cents - cents < -self.overdraft_limit_cents {return false;}self.apply(-cents);true}pub fn apply(&mut self, delta: i64) {self.balance_cents += delta;self.history.push(delta);}}BetterPrivate fields, read-only accessors, a private
apply, andu32amounts.withdrawreturns aResult.#[derive(Debug, Default)]pub struct Account {balance_cents: i64,overdraft_limit_cents: i64,history: Vec<i64>,}#[derive(Debug, PartialEq, Eq)]pub struct LimitExceeded;impl Account {pub fn new(overdraft_limit_cents: i64) -> Self {Self {overdraft_limit_cents,..Self::default()}}pub fn balance_cents(&self) -> i64 {self.balance_cents}pub fn history(&self) -> &[i64] {&self.history // callers can read, not change}pub fn deposit(&mut self, cents: u32) {self.apply(i64::from(cents));}pub fn withdraw(&mut self, cents: u32) -> Result<(), LimitExceeded> {let cents = i64::from(cents);if self.balance_cents - cents < -self.overdraft_limit_cents {return Err(LimitExceeded);}self.apply(-cents);Ok(())}fn apply(&mut self, delta: i64) {// private: the only way to change the balance, and it always records historyself.balance_cents += delta;self.history.push(delta);}}
The “better” Account has one way to change the balance, apply, and it is private. Callers can only deposit and withdraw, and both go through apply, so the history always matches. history() returns &[i64]: callers can read it, but not change it. The u32 parameters make negative amounts impossible to pass.
When it is acceptable
Section titled “When it is acceptable”- Plain data types with no rules: a DTO, a configuration struct, a row from a database. Public fields are correct there, and getters are noise. See S12: Getters and setters for every field.
- Binary crates (applications), where nothing outside the crate uses the API.
pubstill documents the entry points of each module, butpub(crate)andpubmean the same in practice. - Test helpers, in a
#[cfg(test)]module.
How to detect it
Section titled “How to detect it”- No Clippy lint reports this pattern.
- For each struct with
pubfields, ask: is there a rule that connects the fields, or that limits a value? If yes, those fields must be private. - Look for
pub fnhelpers that skip a check that anotherpub fnmakes. - In a library,
cargo docshows the public API. A page full of helpers and internal types is the smell. - The
unreachable_pubrustc lint (allowed by default) reportspubitems that cannot be reached from outside the crate, whichpub(crate)should mark.
What to tell your agent
Section titled “What to tell your agent”Make the fields of Account private and the apply helper private. Keep pubonly for the operations that callers need: new, deposit, withdraw, andread-only accessors. Accessors return a value or a borrow (&[i64]), never&mut. Use unsigned types for amounts that cannot be negative, and returna Result from withdraw. Use pub(crate) for items that only this crate uses.Run the tests.Related: S12: Getters and setters for every field, Structs and impl, and Modules, crates, and Cargo.