Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
172 changes: 164 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,35 @@ 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 suggested_typo = false;
if !target.iter().all(|pat| matches!(pat.kind, ast::PatKind::Ident(..)))
&& !origin.iter().all(|(_, pat)| matches!(pat.kind, ast::PatKind::Ident(..)))
{
// The check above is so that when we encounter `match foo { (a | b) => {} }`,
// we don't suggest `(a | a) => {}`, which would never be what the user wants.
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
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 +724,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 +1123,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 +3535,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 +3563,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