RFA-458 · Case file with fixtures · Case 430 of 694 · Compiler evidence
Rust Enum Variant Patterns Should Use Qualified Paths
Enum variants are qualified unless imported. Write Method::Get or deliberately import the variant; otherwise a bare name may become an irrefutable binding and silently change the match's meaning.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all Rust targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- Enum variants remain qualified unless imported, so the unresolved short identifier is parsed as a new irrefutable binding rather than a variant path.
- First discriminating check
- Qualify the variant with its enum, check removed imports and unreachable arms, and deny the lint where a mistaken catch-all affects policy.
I wrote Get => true while matching a Method enum, but I had not imported the variant. With bindings_with_variant_name denied, Rust emitted E0170. The bare identifier was a new binding, not Method::Get.
The failing fixture represents a dangerous typo because a fresh identifier pattern matches every value. Without a strong lint level, later arms can become unreachable while the first arm appears visually specific.
Variants do not enter scope automatically
Declaring enum Method { Get, Post } creates variants at paths Method::Get and Method::Post. The short name is available only if it is imported into the current scope.
The E0170 explanation recommends qualified names as good practice. Qualification makes the pattern unambiguously a path pattern rather than a binding.
This rule also helps when two enums both have variants named Ready or Error.
A lowercase-looking name can still bind everything
Rust naming conventions encourage UpperCamelCase variants, so the lint can notice when a binding resembles one. But syntax and name resolution, not capitalization alone, determine meaning.
A bare unknown identifier introduces a local that matches any input. I never rely only on visual style for critical routing code. Qualified paths and exhaustive arms give stronger evidence.
The path-pattern Reference explains that paths can refer to enum variants and suitable constants.
Qualify every variant for searchable code
The repaired fixture uses:
match method {
Method::Get => true,
Method::Post => false,
}
I generally prefer this form in libraries and domain code. Searching for Method::Get finds constructions and matches together, and a reviewer can identify the owning type without reading surrounding declarations.
The exhaustive pair also means a future method variant creates compilation work.
Imports are valid when the scope is already clear
Inside a compact function dedicated to one enum, use Method::{Get, Post}; can reduce repetition while remaining explicit about what entered scope. A glob import use Method::*; is shorter but can make collisions and review harder as the enum grows.
The use-declaration Reference defines how imports create local aliases for paths.
This is a readability choice, but it has semantic consequences for pattern resolution.
Denying the lint protects behaviour
The fixture uses #![deny(bindings_with_variant_name)] so a warning becomes a build failure. This is appropriate where mistaking a variant for a catch-all could route authorization, protocol commands, or state transitions incorrectly.
I treat warning policy as part of verification. A fixture that expects E0170 needs the lint level pinned because compiler defaults and workspace flags affect whether compilation stops.
For applications, a workspace-level lint configuration can enforce the same protection consistently.
Watch imports during refactors
Removing a variant import can change a short pattern from a path into a fresh binding. Usually lints and unreachable-pattern warnings reveal this, but the semantic shift is severe enough that I avoid fragile unqualified names in long-lived code.
Renaming a variant may create the same problem if the match arm is edited incompletely. Compiler warnings deserve attention even when tests cover only the expected variant.
Constants share the resolution boundary
A constant path in a pattern compares a structural value; an unknown bare name binds. Qualified constants such as codes::OK make that distinction clear.
Statics have another restriction and cannot be used the same way as constants. I choose an enum for a closed set of named states and constants for fixed scalar values.
My E0170 checklist
This matters especially in security-sensitive negative matches. A mistaken catch-all in Denied => reject() may reject everything and be noticed quickly, but the same mistake in Allowed => accept() can become permissive. I write positive and negative tests for every routing arm and keep the discriminant path explicit. Compilation and linting catch the naming failure; behavioural tests confirm that the policy attached to each variant is still the policy the system applies.
- Does the identifier resolve to a variant in this scope?
- Would
EnumName::Variantremove ambiguity? - Was a
useremoved or hidden by conditional compilation? - Is the current arm accidentally irrefutable?
- Are later arms reported unreachable?
- Should the lint be denied across the workspace?
- Would exhaustive qualified arms catch future variants?
The core principle is that a name that looks like a variant is not enough. It must resolve as a variant path. I qualify important enum patterns so their behaviour, ownership, and future evolution remain visible to both the compiler and the reviewer.