Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions compiler/rustc_passes/messages.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,7 @@ passes_unused_var_maybe_capture_ref = unused variable: `{$name}`

passes_unused_var_remove_field = unused variable: `{$name}`
passes_unused_var_remove_field_suggestion = try removing the field
passes_unused_var_typo = you might have meant to pattern match on the similarly named {$kind} `{$item_name}`

passes_unused_variable_args_in_macro = `{$name}` is captured in macro and introduced a unused variable

Expand Down
18 changes: 18 additions & 0 deletions compiler/rustc_passes/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1346,6 +1346,22 @@ pub(crate) struct UnusedVarRemoveFieldSugg {
#[note]
pub(crate) struct UnusedVarAssignedOnly {
pub name: String,
#[subdiagnostic]
pub typo: Option<PatternTypo>,
}

#[derive(Subdiagnostic)]
#[multipart_suggestion(
passes_unused_var_typo,
style = "verbose",
applicability = "machine-applicable"
)]
pub(crate) struct PatternTypo {
#[suggestion_part(code = "{code}")]
pub span: Span,
pub code: String,
pub item_name: String,
pub kind: String,
}

#[derive(LintDiagnostic)]
Expand Down Expand Up @@ -1413,6 +1429,8 @@ pub(crate) struct UnusedVariableTryPrefix {
#[subdiagnostic]
pub sugg: UnusedVariableSugg,
pub name: String,
#[subdiagnostic]
pub typo: Option<PatternTypo>,
}

#[derive(Subdiagnostic)]
Expand Down
50 changes: 49 additions & 1 deletion compiler/rustc_passes/src/liveness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,10 @@ use rustc_hir::{Expr, HirId, HirIdMap, HirIdSet, find_attr};
use rustc_index::IndexVec;
use rustc_middle::query::Providers;
use rustc_middle::span_bug;
use rustc_middle::ty::print::with_no_trimmed_paths;
use rustc_middle::ty::{self, RootVariableMinCaptureList, Ty, TyCtxt};
use rustc_session::lint;
use rustc_span::edit_distance::find_best_match_for_name;
use rustc_span::{BytePos, Span, Symbol};
use tracing::{debug, instrument};

Expand Down Expand Up @@ -1688,6 +1690,51 @@ impl<'tcx> Liveness<'_, 'tcx> {
let is_assigned =
if ln == self.exit_ln { false } else { self.assigned_on_exit(ln, var) };

let mut typo = None;
for (hir_id, _, span) in &hir_ids_and_spans {
let ty = self.typeck_results.node_type(*hir_id);
if let ty::Adt(adt, _) = ty.peel_refs().kind() {
let name = Symbol::intern(&name);
let adt_def = self.ir.tcx.adt_def(adt.did());
let variant_names: Vec<_> = adt_def
.variants()
.iter()
.filter(|v| matches!(v.ctor, Some((CtorKind::Const, _))))
.map(|v| v.name)
.collect();
if let Some(name) = find_best_match_for_name(&variant_names, name, None)
&& let Some(variant) = adt_def.variants().iter().find(|v| {
v.name == name && matches!(v.ctor, Some((CtorKind::Const, _)))
})
{
typo = Some(errors::PatternTypo {
span: *span,
code: with_no_trimmed_paths!(self.ir.tcx.def_path_str(variant.def_id)),
kind: self.ir.tcx.def_descr(variant.def_id).to_string(),
item_name: variant.name.to_string(),
});
}
}
}
if typo.is_none() {
for (hir_id, _, span) in &hir_ids_and_spans {
let ty = self.typeck_results.node_type(*hir_id);
// Look for consts of the same type with similar names as well, not just unit
// structs and variants.
for def_id in self.ir.tcx.hir_body_owners() {
if let DefKind::Const = self.ir.tcx.def_kind(def_id)
&& self.ir.tcx.type_of(def_id).instantiate_identity() == ty
{
typo = Some(errors::PatternTypo {
span: *span,
code: with_no_trimmed_paths!(self.ir.tcx.def_path_str(def_id)),
kind: "constant".to_string(),
item_name: self.ir.tcx.item_name(def_id).to_string(),
});
}
}
}
}
if is_assigned {
self.ir.tcx.emit_node_span_lint(
lint::builtin::UNUSED_VARIABLES,
Expand All @@ -1696,7 +1743,7 @@ impl<'tcx> Liveness<'_, 'tcx> {
.into_iter()
.map(|(_, _, ident_span)| ident_span)
.collect::<Vec<_>>(),
errors::UnusedVarAssignedOnly { name },
errors::UnusedVarAssignedOnly { name, typo },
)
} else if can_remove {
let spans = hir_ids_and_spans
Expand Down Expand Up @@ -1788,6 +1835,7 @@ impl<'tcx> Liveness<'_, 'tcx> {
name,
sugg,
string_interp: suggestions,
typo,
},
);
}
Expand Down
2 changes: 2 additions & 0 deletions compiler/rustc_resolve/messages.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,8 @@ resolve_variable_bound_with_different_mode =
.label = bound in different ways
.first_binding_span = first binding

resolve_variable_is_a_typo = you might have meant to use the similarly named previously used binding `{$typo}`

resolve_variable_is_not_bound_in_all_patterns =
variable `{$name}` is not bound in all patterns

Expand Down
166 changes: 158 additions & 8 deletions compiler/rustc_resolve/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -661,8 +661,12 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> {
ResolutionError::VariableNotBoundInPattern(binding_error, parent_scope) => {
let BindingError { name, target, origin, could_be_path } = binding_error;

let target_sp = target.iter().copied().collect::<Vec<_>>();
let origin_sp = origin.iter().copied().collect::<Vec<_>>();
let mut target_sp = target.iter().map(|pat| pat.span).collect::<Vec<_>>();
target_sp.sort();
target_sp.dedup();
let mut origin_sp = origin.iter().map(|(span, _)| *span).collect::<Vec<_>>();
origin_sp.sort();
origin_sp.dedup();

let msp = MultiSpan::from_spans(target_sp.clone());
let mut err = self
Expand All @@ -671,8 +675,29 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> {
for sp in target_sp {
err.subdiagnostic(errors::PatternDoesntBindName { span: sp, name });
}
for sp in origin_sp {
err.subdiagnostic(errors::VariableNotInAllPatterns { span: sp });
for sp in &origin_sp {
err.subdiagnostic(errors::VariableNotInAllPatterns { span: *sp });
}
let mut target_visitor = BindingVisitor::default();
for pat in &target {
target_visitor.visit_pat(pat);
}
target_visitor.identifiers.sort();
target_visitor.identifiers.dedup();
let mut origin_visitor = BindingVisitor::default();
for (_, pat) in &origin {
origin_visitor.visit_pat(pat);
}
origin_visitor.identifiers.sort();
origin_visitor.identifiers.dedup();
// Find if the binding could have been a typo
let mut suggested_typo = false;
if let Some(typo) =
find_best_match_for_name(&target_visitor.identifiers, name.name, None)
&& !origin_visitor.identifiers.contains(&typo)
{
err.subdiagnostic(errors::PatternBindingTypo { spans: origin_sp, typo });
suggested_typo = true;
}
if could_be_path {
let import_suggestions = self.lookup_import_candidates(
Expand All @@ -693,10 +718,86 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> {
},
);

if import_suggestions.is_empty() {
if import_suggestions.is_empty() && !suggested_typo {
let kinds = [
DefKind::Ctor(CtorOf::Variant, CtorKind::Const),
DefKind::Ctor(CtorOf::Struct, CtorKind::Const),
DefKind::Const,
DefKind::AssocConst,
];
let mut local_names = vec![];
self.add_module_candidates(
parent_scope.module,
&mut local_names,
&|res| matches!(res, Res::Def(_, _)),
None,
);
let local_names: FxHashSet<_> = local_names
.into_iter()
.filter_map(|s| match s.res {
Res::Def(_, def_id) => Some(def_id),
_ => None,
})
.collect();

let mut local_suggestions = vec![];
let mut suggestions = vec![];
for kind in kinds {
if let Some(suggestion) = self.early_lookup_typo_candidate(
ScopeSet::All(Namespace::ValueNS),
&parent_scope,
name,
&|res: Res| match res {
Res::Def(k, _) => k == kind,
_ => false,
},
) && let Res::Def(kind, mut def_id) = suggestion.res
{
if let DefKind::Ctor(_, _) = kind {
def_id = self.tcx.parent(def_id);
}
let kind = kind.descr(def_id);
if local_names.contains(&def_id) {
// The item is available in the current scope. Very likely to
// be a typo. Don't use the full path.
local_suggestions.push((
suggestion.candidate,
suggestion.candidate.to_string(),
kind,
));
} else {
suggestions.push((
suggestion.candidate,
self.def_path_str(def_id),
kind,
));
}
}
}
let suggestions = if !local_suggestions.is_empty() {
// There is at least one item available in the current scope that is a
// likely typo. We only show those.
local_suggestions
} else {
suggestions
};
for (name, sugg, kind) in suggestions {
err.span_suggestion_verbose(
span,
format!(
"you might have meant to use the similarly named {kind} `{name}`",
),
sugg,
Applicability::MaybeIncorrect,
);
suggested_typo = true;
}
}
if import_suggestions.is_empty() && !suggested_typo {
let help_msg = format!(
"if you meant to match on a variant or a `const` item, consider \
making the path in the pattern qualified: `path::to::ModOrType::{name}`",
"if you meant to match on a unit struct, unit variant or a `const` \
item, consider making the path in the pattern qualified: \
`path::to::ModOrType::{name}`",
);
err.span_help(span, help_msg);
}
Expand Down Expand Up @@ -1016,6 +1117,39 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> {
.emit()
}

fn def_path_str(&self, mut def_id: DefId) -> String {
// We can't use `def_path_str` in resolve.
let mut path = vec![def_id];
while let Some(parent) = self.tcx.opt_parent(def_id) {
def_id = parent;
path.push(def_id);
if def_id.is_top_level_module() {
break;
}
}
// We will only suggest importing directly if it is accessible through that path.
path.into_iter()
.rev()
.map(|def_id| {
self.tcx
.opt_item_name(def_id)
.map(|name| {
match (
def_id.is_top_level_module(),
def_id.is_local(),
self.tcx.sess.edition(),
) {
(true, true, Edition::Edition2015) => String::new(),
(true, true, _) => kw::Crate.to_string(),
(true, false, _) | (false, _, _) => name.to_string(),
}
})
.unwrap_or_else(|| "_".to_string())
})
.collect::<Vec<String>>()
.join("::")
}

pub(crate) fn add_scope_set_candidates(
&mut self,
suggestions: &mut Vec<TypoSuggestion>,
Expand Down Expand Up @@ -3395,7 +3529,7 @@ impl UsePlacementFinder {
}
}

impl<'tcx> visit::Visitor<'tcx> for UsePlacementFinder {
impl<'tcx> Visitor<'tcx> for UsePlacementFinder {
fn visit_crate(&mut self, c: &Crate) {
if self.target_module == CRATE_NODE_ID {
let inject = c.spans.inject_use_span;
Expand Down Expand Up @@ -3423,6 +3557,22 @@ impl<'tcx> visit::Visitor<'tcx> for UsePlacementFinder {
}
}

#[derive(Default)]
struct BindingVisitor {
identifiers: Vec<Symbol>,
spans: FxHashMap<Symbol, Vec<Span>>,
}

impl<'tcx> Visitor<'tcx> for BindingVisitor {
fn visit_pat(&mut self, pat: &ast::Pat) {
if let ast::PatKind::Ident(_, ident, _) = pat.kind {
self.identifiers.push(ident.name);
self.spans.entry(ident.name).or_default().push(ident.span);
}
visit::walk_pat(self, pat);
}
}

fn search_for_any_use_in_items(items: &[Box<ast::Item>]) -> Option<Span> {
for item in items {
if let ItemKind::Use(..) = item.kind
Expand Down
12 changes: 12 additions & 0 deletions compiler/rustc_resolve/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -986,6 +986,18 @@ pub(crate) struct VariableNotInAllPatterns {
pub(crate) span: Span,
}

#[derive(Subdiagnostic)]
#[multipart_suggestion(
resolve_variable_is_a_typo,
applicability = "maybe-incorrect",
style = "verbose"
)]
pub(crate) struct PatternBindingTypo {
#[suggestion_part(code = "{typo}")]
pub(crate) spans: Vec<Span>,
pub(crate) typo: Symbol,
}

#[derive(Diagnostic)]
#[diag(resolve_name_defined_multiple_time)]
#[note]
Expand Down
Loading
Loading