From 563242367fa4af682f3ce4d9a35c2df7e461ad83 Mon Sep 17 00:00:00 2001 From: Haoran Wang Date: Mon, 25 May 2026 14:05:18 +0800 Subject: [PATCH] Fix unused_variables typo suggestions for const patterns When `unused_variables` finds a similarly named const or enum variant, it suggests replacing the unused binding with a pattern matching that item. The item was printed with a trimmed path, which is not necessarily resolvable from the binding site, so the `MachineApplicable` suggestion could fail to compile. Print the item with `with_crate_prefix!`: a `crate::`-rooted, re-export-aware path that resolves from any binding site in the crate. A const declared inside the current body has no such path, so it is printed by its bare in-scope name. Also suppress const typo suggestions for bare `let x = ...` bindings, where a const pattern would be refutable. --- compiler/rustc_mir_transform/src/liveness.rs | 33 ++++- ...-variables-const-pattern-typo.e2015.stderr | 95 ++++++++++++++ ...-variables-const-pattern-typo.e2021.stderr | 95 ++++++++++++++ .../unused-variables-const-pattern-typo.rs | 116 ++++++++++++++++++ 4 files changed, 334 insertions(+), 5 deletions(-) create mode 100644 tests/ui/lint/unused/unused-variables-const-pattern-typo.e2015.stderr create mode 100644 tests/ui/lint/unused/unused-variables-const-pattern-typo.e2021.stderr create mode 100644 tests/ui/lint/unused/unused-variables-const-pattern-typo.rs diff --git a/compiler/rustc_mir_transform/src/liveness.rs b/compiler/rustc_mir_transform/src/liveness.rs index c449cf6867395..deeb9b98ec164 100644 --- a/compiler/rustc_mir_transform/src/liveness.rs +++ b/compiler/rustc_mir_transform/src/liveness.rs @@ -10,7 +10,7 @@ use rustc_middle::mir::visit::{ MutatingUseContext, NonMutatingUseContext, NonUseContext, PlaceContext, Visitor, }; use rustc_middle::mir::*; -use rustc_middle::ty::print::with_no_trimmed_paths; +use rustc_middle::ty::print::with_crate_prefix; use rustc_middle::ty::{self, Ty, TyCtxt}; use rustc_mir_dataflow::fmt::DebugWithContext; use rustc_mir_dataflow::{Analysis, Backward, ResultsCursor}; @@ -168,13 +168,15 @@ fn is_capture(place: PlaceRef<'_>) -> bool { } } -/// Give a diagnostic when an unused variable may be a typo of a unit variant or a struct. +/// Give a diagnostic when an unused variable may be a typo of a unit variant, +/// unit struct, or const. fn maybe_suggest_unit_pattern_typo<'tcx>( tcx: TyCtxt<'tcx>, body_def_id: DefId, name: Symbol, span: Span, ty: Ty<'tcx>, + allow_consts: bool, ) -> Option { if let ty::Adt(adt_def, _) = ty.peel_refs().kind() { let variant_names: Vec<_> = adt_def @@ -191,19 +193,26 @@ fn maybe_suggest_unit_pattern_typo<'tcx>( { return Some(errors::PatternTypo { span, - code: with_no_trimmed_paths!(tcx.def_path_str(variant.def_id)), + code: pattern_item_path(tcx, body_def_id, variant.def_id), kind: tcx.def_descr(variant.def_id), item_name: variant.name, }); } } + if !allow_consts { + return None; + } + // Look for consts of the same type with similar names as well, // not just unit structs and variants. let constants = tcx .hir_body_owners() .filter(|&def_id| { + // `const _: T = ...` items have no name reachable from a pattern: writing + // `path::_` is not valid syntax, so they must never appear in a suggestion. matches!(tcx.def_kind(def_id), DefKind::Const { .. }) + && tcx.item_name(def_id.to_def_id()) != kw::Underscore && tcx.type_of(def_id).instantiate_identity().skip_norm_wip() == ty && tcx.visibility(def_id).is_accessible_from(body_def_id, tcx) }) @@ -215,7 +224,7 @@ fn maybe_suggest_unit_pattern_typo<'tcx>( { return Some(errors::PatternTypo { span, - code: with_no_trimmed_paths!(tcx.def_path_str(def_id)), + code: pattern_item_path(tcx, body_def_id, def_id.to_def_id()), kind: "constant", item_name, }); @@ -224,6 +233,16 @@ fn maybe_suggest_unit_pattern_typo<'tcx>( None } +fn pattern_item_path(tcx: TyCtxt<'_>, body_def_id: DefId, item_def_id: DefId) -> String { + // A body-local const is in scope only by its bare name; everything else is a + // module-level item that `with_crate_prefix!` prints as a valid `crate::` path. + if tcx.is_descendant_of(item_def_id, body_def_id) { + return tcx.item_name(item_def_id).to_string(); + } + + with_crate_prefix!(tcx.def_path_str(item_def_id)) +} + /// Return whether we should consider the current place as a drop guard and skip reporting. fn maybe_drop_guard<'tcx>( tcx: TyCtxt<'tcx>, @@ -977,15 +996,19 @@ impl<'a, 'tcx> AssignmentResult<'a, 'tcx> { && introductions.iter().any(|intro| intro.span.eq_ctxt(def_span)); let maybe_suggest_typo = || { - if let LocalKind::Arg = self.body.local_kind(local) { + if matches!(self.body.local_kind(local), LocalKind::Arg) { None } else { + // In a bare `let x = ...`, replacing `x` with a const path would make the + // pattern refutable. Keep the existing unit ADT typo behavior unchanged. + let allow_consts = !matches!(binding.opt_match_place, Some((None, _))); maybe_suggest_unit_pattern_typo( tcx, self.body.source.def_id(), name, def_span, decl.ty, + allow_consts, ) } }; diff --git a/tests/ui/lint/unused/unused-variables-const-pattern-typo.e2015.stderr b/tests/ui/lint/unused/unused-variables-const-pattern-typo.e2015.stderr new file mode 100644 index 0000000000000..13810928ebcdf --- /dev/null +++ b/tests/ui/lint/unused/unused-variables-const-pattern-typo.e2015.stderr @@ -0,0 +1,95 @@ +error: unused variable: `x` + --> $DIR/unused-variables-const-pattern-typo.rs:17:18 + | +LL | let Some(x) = x else { return }; + | ^ + | +note: the lint level is defined here + --> $DIR/unused-variables-const-pattern-typo.rs:9:9 + | +LL | #![deny(unused_variables)] + | ^^^^^^^^^^^^^^^^ +help: you might have meant to pattern match on the similarly named constant `X` + | +LL - let Some(x) = x else { return }; +LL + let Some(enclosed::X) = x else { return }; + | +help: if this is intentional, prefix it with an underscore + | +LL | let Some(_x) = x else { return }; + | + + +error: unused variable: `x` + --> $DIR/unused-variables-const-pattern-typo.rs:27:13 + | +LL | let x = system::Y; + | ^ help: if this is intentional, prefix it with an underscore: `_x` + +error: unused variable: `good` + --> $DIR/unused-variables-const-pattern-typo.rs:37:18 + | +LL | let Some(good) = x else { return }; + | ^^^^ + | +help: you might have meant to pattern match on the similarly named constant `GOOD` + | +LL - let Some(good) = x else { return }; +LL + let Some(same_module::GOOD) = x else { return }; + | +help: if this is intentional, prefix it with an underscore + | +LL | let Some(_good) = x else { return }; + | + + +error: unused variable: `local` + --> $DIR/unused-variables-const-pattern-typo.rs:46:14 + | +LL | let Some(local) = x else { return }; + | ^^^^^ + | +help: you might have meant to pattern match on the similarly named constant `LOCAL` + | +LL - let Some(local) = x else { return }; +LL + let Some(LOCAL) = x else { return }; + | +help: if this is intentional, prefix it with an underscore + | +LL | let Some(_local) = x else { return }; + | + + +error: unused variable: `ready` + --> $DIR/unused-variables-const-pattern-typo.rs:60:18 + | +LL | let Some(ready) = x else { return }; + | ^^^^^ + | +help: you might have meant to pattern match on the similarly named variant `Ready` + | +LL - let Some(ready) = x else { return }; +LL + let Some(variants::State::Ready) = x else { return }; + | +help: if this is intentional, prefix it with an underscore + | +LL | let Some(_ready) = x else { return }; + | + + +error: unused variable: `x` + --> $DIR/unused-variables-const-pattern-typo.rs:80:14 + | +LL | let Some(x) = x else { return }; + | ^ help: if this is intentional, prefix it with an underscore: `_x` + +error: unused variable: `value` + --> $DIR/unused-variables-const-pattern-typo.rs:96:14 + | +LL | let Some(value) = x else { return }; + | ^^^^^ help: if this is intentional, prefix it with an underscore: `_value` + +error: unused variable: `xyzzy` + --> $DIR/unused-variables-const-pattern-typo.rs:111:14 + | +LL | let Some(xyzzy) = x else { return }; + | ^^^^^ help: if this is intentional, prefix it with an underscore: `_xyzzy` + +error: aborting due to 8 previous errors + diff --git a/tests/ui/lint/unused/unused-variables-const-pattern-typo.e2021.stderr b/tests/ui/lint/unused/unused-variables-const-pattern-typo.e2021.stderr new file mode 100644 index 0000000000000..8a0aeb9dc03c6 --- /dev/null +++ b/tests/ui/lint/unused/unused-variables-const-pattern-typo.e2021.stderr @@ -0,0 +1,95 @@ +error: unused variable: `x` + --> $DIR/unused-variables-const-pattern-typo.rs:17:18 + | +LL | let Some(x) = x else { return }; + | ^ + | +note: the lint level is defined here + --> $DIR/unused-variables-const-pattern-typo.rs:9:9 + | +LL | #![deny(unused_variables)] + | ^^^^^^^^^^^^^^^^ +help: you might have meant to pattern match on the similarly named constant `X` + | +LL - let Some(x) = x else { return }; +LL + let Some(crate::enclosed::X) = x else { return }; + | +help: if this is intentional, prefix it with an underscore + | +LL | let Some(_x) = x else { return }; + | + + +error: unused variable: `x` + --> $DIR/unused-variables-const-pattern-typo.rs:27:13 + | +LL | let x = system::Y; + | ^ help: if this is intentional, prefix it with an underscore: `_x` + +error: unused variable: `good` + --> $DIR/unused-variables-const-pattern-typo.rs:37:18 + | +LL | let Some(good) = x else { return }; + | ^^^^ + | +help: you might have meant to pattern match on the similarly named constant `GOOD` + | +LL - let Some(good) = x else { return }; +LL + let Some(crate::same_module::GOOD) = x else { return }; + | +help: if this is intentional, prefix it with an underscore + | +LL | let Some(_good) = x else { return }; + | + + +error: unused variable: `local` + --> $DIR/unused-variables-const-pattern-typo.rs:46:14 + | +LL | let Some(local) = x else { return }; + | ^^^^^ + | +help: you might have meant to pattern match on the similarly named constant `LOCAL` + | +LL - let Some(local) = x else { return }; +LL + let Some(LOCAL) = x else { return }; + | +help: if this is intentional, prefix it with an underscore + | +LL | let Some(_local) = x else { return }; + | + + +error: unused variable: `ready` + --> $DIR/unused-variables-const-pattern-typo.rs:60:18 + | +LL | let Some(ready) = x else { return }; + | ^^^^^ + | +help: you might have meant to pattern match on the similarly named variant `Ready` + | +LL - let Some(ready) = x else { return }; +LL + let Some(crate::variants::State::Ready) = x else { return }; + | +help: if this is intentional, prefix it with an underscore + | +LL | let Some(_ready) = x else { return }; + | + + +error: unused variable: `x` + --> $DIR/unused-variables-const-pattern-typo.rs:80:14 + | +LL | let Some(x) = x else { return }; + | ^ help: if this is intentional, prefix it with an underscore: `_x` + +error: unused variable: `value` + --> $DIR/unused-variables-const-pattern-typo.rs:96:14 + | +LL | let Some(value) = x else { return }; + | ^^^^^ help: if this is intentional, prefix it with an underscore: `_value` + +error: unused variable: `xyzzy` + --> $DIR/unused-variables-const-pattern-typo.rs:111:14 + | +LL | let Some(xyzzy) = x else { return }; + | ^^^^^ help: if this is intentional, prefix it with an underscore: `_xyzzy` + +error: aborting due to 8 previous errors + diff --git a/tests/ui/lint/unused/unused-variables-const-pattern-typo.rs b/tests/ui/lint/unused/unused-variables-const-pattern-typo.rs new file mode 100644 index 0000000000000..14e545e1adfcb --- /dev/null +++ b/tests/ui/lint/unused/unused-variables-const-pattern-typo.rs @@ -0,0 +1,116 @@ +//@ revisions: e2015 e2021 +//@[e2015] edition:2015 +//@[e2021] edition:2021 + +// Regression test for https://github.com/rust-lang/rust/issues/147595. +// The `unused_variables` typo suggestion must not print a pattern path that +// cannot be resolved from the binding site. + +#![deny(unused_variables)] + +mod enclosed { + pub const X: i32 = 1; +} + +mod separated { + pub fn let_else_path_qualification(x: Option) { + let Some(x) = x else { return }; + //~^ ERROR unused variable: `x` + //~| HELP you might have meant to pattern match on the similarly named constant `X` + //~| HELP if this is intentional, prefix it with an underscore + } +} + +mod utils { + pub fn simple_binding_with_imported_const() { + use crate::system; + let x = system::Y; + //~^ ERROR unused variable: `x` + //~| HELP if this is intentional, prefix it with an underscore + } +} + +mod same_module { + const GOOD: i32 = 0; + + pub fn same_module_const_suggestion(x: Option) { + let Some(good) = x else { return }; + //~^ ERROR unused variable: `good` + //~| HELP you might have meant to pattern match on the similarly named constant `GOOD` + //~| HELP if this is intentional, prefix it with an underscore + } +} + +fn function_local_const_suggestion(x: Option) { + const LOCAL: i32 = 0; + let Some(local) = x else { return }; + //~^ ERROR unused variable: `local` + //~| HELP you might have meant to pattern match on the similarly named constant `LOCAL` + //~| HELP if this is intentional, prefix it with an underscore +} + +mod variants { + pub enum State { + Ready, + } +} + +mod separated_variant { + pub fn cross_module_variant_path(x: Option) { + let Some(ready) = x else { return }; + //~^ ERROR unused variable: `ready` + //~| HELP you might have meant to pattern match on the similarly named variant `Ready` + //~| HELP if this is intentional, prefix it with an underscore + } +} + +mod system { + pub const Y: u32 = 0; +} + +// `const _: T = ...` items have no name that can be written in a pattern: a +// suggestion of `path::_` would be syntactically invalid. Such items must be +// excluded from typo suggestions even when their type and name distance would +// otherwise match. +mod anonymous_const { + pub const _: () = (); +} + +fn no_anonymous_const_suggestion(x: Option<()>) { + let Some(x) = x else { return }; + //~^ ERROR unused variable: `x` + //~| HELP if this is intentional, prefix it with an underscore +} + +// A `const` that is not accessible from the binding site (private to another +// module) must not be offered as a suggestion: applying the rewrite would +// produce an error about a private item, replacing one diagnostic with +// another. The `value` binding deliberately matches the private const's name +// up to case, which is the strongest score in `find_best_match_for_name`. +mod private_const { + #[allow(dead_code)] + const VALUE: i32 = 0; +} + +fn no_inaccessible_const_suggestion(x: Option) { + let Some(value) = x else { return }; + //~^ ERROR unused variable: `value` + //~| HELP if this is intentional, prefix it with an underscore +} + +// A `const` whose name is textually far from the binding (beyond the default +// `find_best_match_for_name` threshold of `max(len, 3) / 3`) must not be +// suggested either: the user did not "almost" mean this constant. The binding +// `xyzzy` is deliberately chosen to be a distance of at least 5 from every +// const in this file. +mod distant_name { + pub const ENTIRELY_UNRELATED_LONGER_NAME: i32 = 0; +} + +fn no_distant_name_suggestion(x: Option) { + let Some(xyzzy) = x else { return }; + //~^ ERROR unused variable: `xyzzy` + //~| HELP if this is intentional, prefix it with an underscore +} + +fn main() {}