S16: Wildcard _ => match arm that hides new variants
What it looks like
Section titled “What it looks like”An enum that got a new variant, Refunded:
#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum PaymentStatus { Pending, Paid, Failed, Refunded, // added later}And a match that was written before it:
pub fn customer_message(status: PaymentStatus) -> &'static str { match status { PaymentStatus::Paid => "Thank you for your payment.", PaymentStatus::Failed => "Your payment failed. Please try again.", _ => "We are waiting for your payment.", // now also used for `Refunded` }}When Refunded was added, this code still compiled. A refunded customer now reads “We are waiting for your payment.” The test worse_tells_refunded_customers_to_pay proves it.
Why AI agents produce it
Section titled “Why AI agents produce it”- It is the Java
default:habit. A Javaswitchover an enum often ends withdefault, and before Java 21 the compiler did not check that every constant was handled. - It is shorter. When two variants have the same result,
_ =>saves a line. - It removes error E0004. When a
matchis not exhaustive, adding_ => ...is the quickest way to make it compile.
Why it is a problem
Section titled “Why it is a problem”- It switches off the check that makes enums valuable. Without a wildcard, adding a variant makes the compiler list every
matchthat must handle it. With a wildcard, the compiler says nothing. - The bug is in code that nobody changed. The pull request that adds
Refundedchanges the enum. The wrong message comes from amatchin another file, which is not in the diff. A reviewer has no reason to look there. - The default is often wrong for the new case. The wildcard was written for the variants that existed then. Nobody decided what it means for variants that did not exist yet.
The better version
Section titled “The better version”Worse
_ =>coversPending, and silently alsoRefunded.pub fn customer_message(status: PaymentStatus) -> &'static str {match status {PaymentStatus::Paid => "Thank you for your payment.",PaymentStatus::Failed => "Your payment failed. Please try again.",_ => "We are waiting for your payment.", // now also used for `Refunded`}}BetterEvery variant is listed. The next new variant is a compile error here.
pub fn customer_message(status: PaymentStatus) -> &'static str {match status {PaymentStatus::Pending => "We are waiting for your payment.",PaymentStatus::Paid => "Thank you for your payment.",PaymentStatus::Failed => "Your payment failed. Please try again.",PaymentStatus::Refunded => "Your payment was refunded.",}}
If two variants have the same result, list both with |: PaymentStatus::Pending | PaymentStatus::Failed => .... That keeps the check and the short code.
When it is acceptable
Section titled “When it is acceptable”- Types with no fixed list of variants: integers, strings, and tuples of them. A catch-all is required:
/// Integers and strings have no list of variants, so a catch-all is required.pub fn http_reason(code: u16) -> &'static str { match code { 200 => "OK", 404 => "Not Found", 500..=599 => "Server Error", _ => "Other", }}- Enums from another crate marked
#[non_exhaustive]. The compiler requires a wildcard, because the crate can add variants in a minor version.std::io::ErrorKindis an example. - A deliberate “every other case”, where the rule really holds for all future variants: “only an authorized payment can be captured, every other state is rejected”. See the state machine on Enums and pattern matching. A comment helps the reader.
matches!(x, Variant)andif let, which are about one variant by design.
How to detect it
Section titled “How to detect it”- Clippy
wildcard_enum_match_arm(restriction group, off by default) reports a_arm in amatchon an enum. It confirms the example above. Enable it with#![warn(clippy::wildcard_enum_match_arm)]in a crate that has many domain enums. - Clippy
match_wildcard_for_single_variants(pedantic) reports a_that covers exactly one variant. - Search for
_ =>and check the type of each matched value. Integer and string matches are fine. Matches on your own enums need a reason.
What to tell your agent
Section titled “What to tell your agent”In every match on our own enums, replace the _ => arm with the variantsit covers, listed by name (use | to combine variants with the same result).Keep _ only for integers, strings, and #[non_exhaustive] enums from othercrates. Then enable clippy::wildcard_enum_match_arm for this crate and fixwhat it reports.Related: S15: bool flags or string constants instead of an enum, and Enums and pattern matching.