RFA-553 · Case file with fixtures · Case 525 of 694 · Compiler evidence
Rust forbid Is a Lint Policy That Inner allow Cannot Override
deny makes a lint an error while retaining scoped override; forbid locks the policy for descendants. Choose the level according to governance needs.
- 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
- Forbid is an inherited non-overridable policy rather than only another spelling for deny-level severity.
- First discriminating check
- Find the outer source or command-line policy, satisfy it, or change forbid to deny only when reviewed scoped exceptions are intentionally allowed.
deny and forbid both turn a triggered lint into a hard error, but they differ in governance. forbid says descendants may not weaken the decision. E0453 appears when an inner allow tries anyway.
The failing fixture forbids non_snake_case at crate level and allows it on main. The inner exception contradicts the locked outer policy.
Lint levels form a scoped policy
Rust attributes can change lint levels for a crate, module, item, or expression where supported. An inner scope normally refines an outer setting.
With deny, a narrow allow can intentionally exempt generated or externally constrained code. With forbid, that escape hatch is closed. The official E0453 page identifies this exact distinction.
I choose forbid only when preventing local suppression is part of the policy, not because the word sounds stronger in a configuration template.
The repaired code follows the locked rule
The repaired fixture removes the contradictory allowance and renames LegacyName to legacy_name. The code satisfies the crate-level contract.
This is the preferred repair when the violation has no external reason. Suppressing a naming lint for an ordinary local would add policy debt without preserving compatibility.
If the name were fixed by a wire format or FFI symbol, the project should decide whether the outer rule ought to be deny instead, or whether constrained code belongs in a separately generated or configured boundary.
Command-line policy can be the outer scope
Build systems may pass -D warnings, -F unsafe_code, or other lint flags. A source file can then fail even when no enclosing attribute appears nearby.
The rustc book explains lint levels and command-line controls. I inspect CI flags, workspace configuration, and wrapper commands when E0453 seems to have no source-level parent.
Reproducing with the same command matters. Running plain cargo check locally may not carry the policy used in release CI.
Cap-lints changes dependency treatment
Cargo and rustc can cap lint severity so dependency warnings do not usually break downstream builds. Workspace members, path dependencies, and direct rustc invocations can behave differently.
I keep critical safety policies in the crate that owns the code and test them there. I do not assume a downstream project's flags will enforce my intended standard inside every dependency.
Forbid is useful for narrow non-negotiable boundaries
#![forbid(unsafe_code)] can express that one crate must contain no unsafe blocks or implementations and prevent a nested module from opting out. This can be powerful when architecture isolates unsafe code into a reviewed lower-level crate.
Using forbid(warnings) broadly is brittle because new compiler versions may add warnings. Specific lint names make compatibility intent clearer.
The Reference lists the semantics of lint check attributes. I review toolchain upgrades against policies that convert new diagnostics into errors.
A local allow should carry evidence
When the outer level is overridable, I add the smallest possible exception with a reason. A module-wide allowance for one generated identifier can hide future unrelated violations.
For recurring external names, serde rename attributes or explicit mapping may preserve wire compatibility while keeping Rust identifiers idiomatic. Suppression is not always the best boundary design.
Workspace policy needs one documented owner
Lint levels can come from crate attributes, Cargo configuration, workspace scripts, and CI flags. If several layers compete, engineers cannot tell where an exception should be discussed. I keep one documented source of truth and test the command developers are expected to run. A locked lint is most useful when its reason and scope are understood; otherwise teams add wrapper scripts or disable checks locally, which defeats the governance that forbid was meant to provide.
My E0453 checklist
- Where was the lint first set to
forbid? - Did the setting come from source, rustc flags, or CI wrappers?
- Is the inner exception genuinely necessary?
- Should the code satisfy the rule instead?
- If exceptions are valid, should the parent use
denyrather thanforbid? - Can external naming be translated at a serialization boundary?
- Is the lint specific and stable across supported toolchains?
- Does any allowance have narrow scope and a useful reason?
The core principle is that forbid is an inheritance rule for policy, not merely a higher severity. I use it for deliberately non-overridable boundaries and use deny when reviewed local exceptions are part of the design.