Skip to content

← Shape Your Data: Structs, Enums, Pattern Matching step 8 of 26

Medium Primitives

The wildcard trap: _ arms freeze your enum against the future

A supervisor process receives text lines describing signals and must turn each line into a status string. Implement:

pub fn handle(events: Vec<String>) -> Vec<String>

Return one status per input line, in the same order. An empty input returns an empty vector.

line status
start running
stop halted
pause paused
resume running
reconnect N retrying #N
anything else unknown

The exact parsing rules, which the starter's parse function already implements correctly:

  • A line is split on whitespace (str::split_whitespace), so leading, trailing and repeated whitespace is ignored. " stop " is halted.
  • Keywords are lowercase and case-sensitive: "Start" is unknown.
  • A keyword followed by extra words is unknown: "start now" and "reconnect 3 4" are both unknown.
  • N must parse as a u32 (0 through 4,294,967,295) using str::parse. That accepts an optional leading + and leading zeros, so "reconnect +007" is retrying #7. A missing number, a leading - (even -0), any character other than the ASCII digits 0 to 9 after the optional +, or a value above u32::MAX makes the line unknown.
  • The number in the output is the parsed value written in plain decimal.

The bug in the starter

The starter's describe function matches on this enum:

enum Signal {
    Start,
    Stop,
    Pause,
    Resume,
    Reconnect { attempt: u32 },
}

Picture the code as it was in an earlier version, when Signal had only Start, Stop and Pause. Back then the last arm, _ => "paused", meant "the remaining variant", which could only be Pause. Since then, someone added Resume and Reconnect.

Adding those variants did not cause a compile error. The _ pattern (the wildcard) matches any value, so it silently took over both new variants, and the starter now reports a resumed process and a reconnection attempt as paused.

Compare that with a match that names every variant. Rust requires a match to be exhaustive: every possible value must be handled by some arm. If every arm names a variant and a new variant is added, every such match stops compiling with error E0004 ("non-exhaustive patterns") until you decide what the new variant should do. A wildcard arm always satisfies the exhaustiveness check, so it also removes that reminder.

Two checks

The starter enables one extra Clippy lint at the top of the file:

#![warn(clippy::wildcard_enum_match_arm)]

This lint warns when a _ arm in a match on an enum covers two or more of its variants. It belongs to Clippy's restriction group. Lints in that group are off by default, and Clippy's documentation advises against enabling the whole group, because some of its lints conflict with each other or with ordinary Rust style. Enabling one chosen restriction lint, as here, is the intended use.

Rust submissions are graded on compilation, the test cases and cargo clippy -- -D warnings. The -D warnings flag turns every warning into an error, so this lint makes the starter fail Clippy as well as the resume and reconnect tests.

Keep the attribute in your submission. Deleting it would remove the second check.

Clippy reports a wildcard that covers exactly one variant under a different lint, match_wildcard_for_single_variants, which is not enabled here. Naming all five variants avoids both.

When a wildcard is the right choice

The goal is to use a wildcard deliberately, not to avoid it everywhere.

  • #[non_exhaustive] enums from another crate. This attribute tells users of the enum that new variants may be added in future versions. Outside the crate that defines it, the compiler rejects a match that does not include a wildcard (or another catch-all pattern) with E0004, even if you name every variant that exists today. std::io::ErrorKind is an example: matching it outside std requires a _ arm. With wildcard_enum_match_arm enabled, Clippy also asks you to name the known variants you mean before that _.
  • Large enums where many variants share one behavior. A single wildcard arm can be clearer than a long list of variants with identical bodies. The trade-off is that a new variant will get that shared behavior without any compiler error, so this works best when that is the correct default.

Ignoring fields without ignoring variants

Often a _ is written because the arm does not need a variant's data. You can ignore the data while still naming the variant:

Signal::Reconnect { .. } => "reconnecting".to_string(),

The .. ignores all fields of a struct-like variant, such as Reconnect { attempt: u32 }. For a tuple-like variant, for example a hypothetical Message(String, u32), the equivalent is Message(..). Because the variant is still named, adding another variant to the enum still makes the match fail to compile until you handle it.

Related Clippy lints

These three lints are in Clippy's pedantic group, which is also off by default. You can enable them individually, as this problem does with wildcard_enum_match_arm.

  • match_wildcard_for_single_variants: a _ arm that covers exactly one variant. Clippy suggests naming that variant.
  • match_wild_err_arm: an Err(_) arm that panics, which discards the error value instead of reporting it.
  • single_match_else: a two-arm match that if let ... else would usually express more simply.