Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

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::Variant remove ambiguity?
  • Was a use removed 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.