From 08c77a6eb403768ed64ae567980887984f506ef9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=AE=B8=E6=9D=B0=E5=8F=8B=20Jieyou=20Xu=20=28Joe=29?= Date: Sun, 16 Jul 2023 18:18:38 +0800 Subject: [PATCH] Add infrastructure `#[rustc_confusables]` attribute to allow targeted "no method" errors on standard library types The standard library developer can annotate methods on e.g. `BTreeSet::push` with `#[rustc_confusables("insert")]`. When the user mistypes `btreeset.push()`, `BTreeSet::insert` will be suggested if there are no other candidates to suggest. --- Cargo.lock | 1 + compiler/rustc_attr/src/builtin.rs | 17 +++++ compiler/rustc_feature/src/builtin_attrs.rs | 6 ++ compiler/rustc_hir_typeck/Cargo.toml | 1 + .../rustc_hir_typeck/src/method/suggest.rs | 29 ++++++-- compiler/rustc_passes/messages.ftl | 9 +++ compiler/rustc_passes/src/check_attr.rs | 41 +++++++++++ compiler/rustc_passes/src/errors.rs | 32 +++++++++ compiler/rustc_span/src/symbol.rs | 1 + .../rustc_confusables_across_crate.rs | 11 +++ tests/ui/attributes/rustc_confusables.rs | 47 +++++++++++++ tests/ui/attributes/rustc_confusables.stderr | 68 +++++++++++++++++++ 12 files changed, 259 insertions(+), 4 deletions(-) create mode 100644 tests/ui/attributes/auxiliary/rustc_confusables_across_crate.rs create mode 100644 tests/ui/attributes/rustc_confusables.rs create mode 100644 tests/ui/attributes/rustc_confusables.stderr diff --git a/Cargo.lock b/Cargo.lock index 9f1561b503d..2390850b567 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3665,6 +3665,7 @@ name = "rustc_hir_typeck" version = "0.1.0" dependencies = [ "rustc_ast", + "rustc_attr", "rustc_data_structures", "rustc_errors", "rustc_fluent_macro", diff --git a/compiler/rustc_attr/src/builtin.rs b/compiler/rustc_attr/src/builtin.rs index 372a58857f3..6cce3a56f08 100644 --- a/compiler/rustc_attr/src/builtin.rs +++ b/compiler/rustc_attr/src/builtin.rs @@ -1217,3 +1217,20 @@ pub fn parse_alignment(node: &ast::LitKind) -> Result { Err("not an unsuffixed integer") } } + +/// Read the content of a `rustc_confusables` attribute, and return the list of candidate names. +pub fn parse_confusables(attr: &Attribute) -> Option> { + let meta = attr.meta()?; + let MetaItem { kind: MetaItemKind::List(ref metas), .. } = meta else { return None }; + + let mut candidates = Vec::new(); + + for meta in metas { + let NestedMetaItem::Lit(meta_lit) = meta else { + return None; + }; + candidates.push(meta_lit.symbol); + } + + return Some(candidates); +} diff --git a/compiler/rustc_feature/src/builtin_attrs.rs b/compiler/rustc_feature/src/builtin_attrs.rs index a70671dd9fb..a183cfd8776 100644 --- a/compiler/rustc_feature/src/builtin_attrs.rs +++ b/compiler/rustc_feature/src/builtin_attrs.rs @@ -625,6 +625,12 @@ pub struct BuiltinAttribute { ErrorFollowing, INTERNAL_UNSTABLE ), + rustc_attr!( + rustc_confusables, Normal, + template!(List: r#""name1", "name2", ..."#), + ErrorFollowing, + INTERNAL_UNSTABLE, + ), // Enumerates "identity-like" conversion methods to suggest on type mismatch. rustc_attr!( rustc_conversion_suggestion, Normal, template!(Word), WarnFollowing, INTERNAL_UNSTABLE diff --git a/compiler/rustc_hir_typeck/Cargo.toml b/compiler/rustc_hir_typeck/Cargo.toml index 13e1ea31c4d..ce91d023a0a 100644 --- a/compiler/rustc_hir_typeck/Cargo.toml +++ b/compiler/rustc_hir_typeck/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" smallvec = { version = "1.8.1", features = ["union", "may_dangle"] } tracing = "0.1" rustc_ast = { path = "../rustc_ast" } +rustc_attr = { path = "../rustc_attr" } rustc_data_structures = { path = "../rustc_data_structures" } rustc_errors = { path = "../rustc_errors" } rustc_graphviz = { path = "../rustc_graphviz" } diff --git a/compiler/rustc_hir_typeck/src/method/suggest.rs b/compiler/rustc_hir_typeck/src/method/suggest.rs index e3e0eff23d2..3d7187cb16f 100644 --- a/compiler/rustc_hir_typeck/src/method/suggest.rs +++ b/compiler/rustc_hir_typeck/src/method/suggest.rs @@ -2,13 +2,12 @@ //! found or is otherwise invalid. use crate::errors; -use crate::errors::CandidateTraitNote; -use crate::errors::NoAssociatedItem; +use crate::errors::{CandidateTraitNote, NoAssociatedItem}; use crate::Expectation; use crate::FnCtxt; use rustc_ast::ast::Mutability; -use rustc_data_structures::fx::FxIndexMap; -use rustc_data_structures::fx::FxIndexSet; +use rustc_attr::parse_confusables; +use rustc_data_structures::fx::{FxIndexMap, FxIndexSet}; use rustc_data_structures::unord::UnordSet; use rustc_errors::StashKey; use rustc_errors::{ @@ -1038,6 +1037,28 @@ trait bound{s}", "the {item_kind} was found for\n{}{}", type_candidates, additional_types )); + } else { + 'outer: for inherent_impl_did in self.tcx.inherent_impls(adt.did()) { + for inherent_method in + self.tcx.associated_items(inherent_impl_did).in_definition_order() + { + if let Some(attr) = self.tcx.get_attr(inherent_method.def_id, sym::rustc_confusables) + && let Some(candidates) = parse_confusables(attr) + && candidates.contains(&item_name.name) + { + err.span_suggestion_verbose( + item_name.span, + format!( + "you might have meant to use `{}`", + inherent_method.name.as_str() + ), + inherent_method.name.as_str(), + Applicability::MaybeIncorrect, + ); + break 'outer; + } + } + } } } } else { diff --git a/compiler/rustc_passes/messages.ftl b/compiler/rustc_passes/messages.ftl index a607e483c97..0aa3d6265dc 100644 --- a/compiler/rustc_passes/messages.ftl +++ b/compiler/rustc_passes/messages.ftl @@ -98,6 +98,9 @@ passes_collapse_debuginfo = `collapse_debuginfo` attribute should be applied to macro definitions .label = not a macro definition +passes_confusables = attribute should be applied to an inherent method + .label = not an inherent method + passes_const_impl_const_trait = const `impl`s must be for traits marked with `#[const_trait]` .note = this trait must be annotated with `#[const_trait]` @@ -266,6 +269,9 @@ passes_duplicate_lang_item_crate_depends = .first_definition_path = first definition in `{$orig_crate_name}` loaded from {$orig_path} .second_definition_path = second definition in `{$crate_name}` loaded from {$path} +passes_empty_confusables = + expected at least one confusable name + passes_export_name = attribute should be applied to a free function, impl method or static .label = not a free function, impl method or static @@ -326,6 +332,9 @@ passes_implied_feature_not_exist = passes_incorrect_do_not_recommend_location = `#[do_not_recommend]` can only be placed on trait implementations +passes_incorrect_meta_item = expected a quoted string literal +passes_incorrect_meta_item_suggestion = consider surrounding this with quotes + passes_incorrect_target = `{$name}` language item must be applied to a {$kind} with {$at_least -> [true] at least {$num} diff --git a/compiler/rustc_passes/src/check_attr.rs b/compiler/rustc_passes/src/check_attr.rs index ee14ffa6da8..4d7ebe3fefe 100644 --- a/compiler/rustc_passes/src/check_attr.rs +++ b/compiler/rustc_passes/src/check_attr.rs @@ -183,6 +183,7 @@ fn check_attributes( | sym::rustc_allowed_through_unstable_modules | sym::rustc_promotable => self.check_stability_promotable(&attr, span, target), sym::link_ordinal => self.check_link_ordinal(&attr, span, target), + sym::rustc_confusables => self.check_confusables(&attr, target), _ => true, }; @@ -1985,6 +1986,46 @@ fn check_link_ordinal(&self, attr: &Attribute, _span: Span, target: Target) -> b } } + fn check_confusables(&self, attr: &Attribute, target: Target) -> bool { + match target { + Target::Method(MethodKind::Inherent) => { + let Some(meta) = attr.meta() else { + return false; + }; + let ast::MetaItem { kind: MetaItemKind::List(ref metas), .. } = meta else { + return false; + }; + + let mut candidates = Vec::new(); + + for meta in metas { + let NestedMetaItem::Lit(meta_lit) = meta else { + self.tcx.sess.emit_err(errors::IncorrectMetaItem { + span: meta.span(), + suggestion: errors::IncorrectMetaItemSuggestion { + lo: meta.span().shrink_to_lo(), + hi: meta.span().shrink_to_hi(), + }, + }); + return false; + }; + candidates.push(meta_lit.symbol); + } + + if candidates.is_empty() { + self.tcx.sess.emit_err(errors::EmptyConfusables { span: attr.span }); + return false; + } + + true + } + _ => { + self.tcx.sess.emit_err(errors::Confusables { attr_span: attr.span }); + false + } + } + } + fn check_deprecated(&self, hir_id: HirId, attr: &Attribute, _span: Span, target: Target) { match target { Target::Closure | Target::Expression | Target::Statement | Target::Arm => { diff --git a/compiler/rustc_passes/src/errors.rs b/compiler/rustc_passes/src/errors.rs index 3fe7feb9dfa..eae13f86049 100644 --- a/compiler/rustc_passes/src/errors.rs +++ b/compiler/rustc_passes/src/errors.rs @@ -617,6 +617,38 @@ pub struct LinkOrdinal { pub attr_span: Span, } +#[derive(Diagnostic)] +#[diag(passes_confusables)] +pub struct Confusables { + #[primary_span] + pub attr_span: Span, +} + +#[derive(Diagnostic)] +#[diag(passes_empty_confusables)] +pub(crate) struct EmptyConfusables { + #[primary_span] + pub span: Span, +} + +#[derive(Diagnostic)] +#[diag(passes_incorrect_meta_item, code = "E0539")] +pub(crate) struct IncorrectMetaItem { + #[primary_span] + pub span: Span, + #[subdiagnostic] + pub suggestion: IncorrectMetaItemSuggestion, +} + +#[derive(Subdiagnostic)] +#[multipart_suggestion(passes_incorrect_meta_item_suggestion, applicability = "maybe-incorrect")] +pub(crate) struct IncorrectMetaItemSuggestion { + #[suggestion_part(code = "\"")] + pub lo: Span, + #[suggestion_part(code = "\"")] + pub hi: Span, +} + #[derive(Diagnostic)] #[diag(passes_stability_promotable)] pub struct StabilityPromotable { diff --git a/compiler/rustc_span/src/symbol.rs b/compiler/rustc_span/src/symbol.rs index 4fc440ef947..08925761b39 100644 --- a/compiler/rustc_span/src/symbol.rs +++ b/compiler/rustc_span/src/symbol.rs @@ -1265,6 +1265,7 @@ rustc_clean, rustc_coherence_is_core, rustc_coinductive, + rustc_confusables, rustc_const_stable, rustc_const_unstable, rustc_conversion_suggestion, diff --git a/tests/ui/attributes/auxiliary/rustc_confusables_across_crate.rs b/tests/ui/attributes/auxiliary/rustc_confusables_across_crate.rs new file mode 100644 index 00000000000..2fb2d3ad4c4 --- /dev/null +++ b/tests/ui/attributes/auxiliary/rustc_confusables_across_crate.rs @@ -0,0 +1,11 @@ +#![feature(rustc_attrs)] + +pub struct BTreeSet; + +impl BTreeSet { + #[rustc_confusables("push", "test_b")] + pub fn insert(&self) {} + + #[rustc_confusables("pulled")] + pub fn pull(&self) {} +} diff --git a/tests/ui/attributes/rustc_confusables.rs b/tests/ui/attributes/rustc_confusables.rs new file mode 100644 index 00000000000..352e91d065f --- /dev/null +++ b/tests/ui/attributes/rustc_confusables.rs @@ -0,0 +1,47 @@ +// aux-build: rustc_confusables_across_crate.rs + +#![feature(rustc_attrs)] + +extern crate rustc_confusables_across_crate; + +use rustc_confusables_across_crate::BTreeSet; + +fn main() { + // Misspellings (similarly named methods) take precedence over `rustc_confusables`. + let x = BTreeSet {}; + x.inser(); + //~^ ERROR no method named + //~| HELP there is a method with a similar name + x.foo(); + //~^ ERROR no method named + x.push(); + //~^ ERROR no method named + //~| HELP you might have meant to use `insert` + x.test(); + //~^ ERROR no method named + x.pulled(); + //~^ ERROR no method named + //~| HELP there is a method with a similar name +} + +struct Bar; + +impl Bar { + #[rustc_confusables()] + //~^ ERROR expected at least one confusable name + fn baz() {} + + #[rustc_confusables] + //~^ ERROR malformed `rustc_confusables` attribute input + //~| HELP must be of the form + fn qux() {} + + #[rustc_confusables(invalid_meta_item)] + //~^ ERROR expected a quoted string literal + //~| HELP consider surrounding this with quotes + fn quux() {} +} + +#[rustc_confusables("blah")] +//~^ ERROR attribute should be applied to an inherent method +fn not_inherent_impl_method() {} diff --git a/tests/ui/attributes/rustc_confusables.stderr b/tests/ui/attributes/rustc_confusables.stderr new file mode 100644 index 00000000000..9fd4470cdbb --- /dev/null +++ b/tests/ui/attributes/rustc_confusables.stderr @@ -0,0 +1,68 @@ +error: malformed `rustc_confusables` attribute input + --> $DIR/rustc_confusables.rs:34:5 + | +LL | #[rustc_confusables] + | ^^^^^^^^^^^^^^^^^^^^ help: must be of the form: `#[rustc_confusables("name1", "name2", ...)]` + +error: attribute should be applied to an inherent method + --> $DIR/rustc_confusables.rs:45:1 + | +LL | #[rustc_confusables("blah")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: expected at least one confusable name + --> $DIR/rustc_confusables.rs:30:5 + | +LL | #[rustc_confusables()] + | ^^^^^^^^^^^^^^^^^^^^^^ + +error[E0539]: expected a quoted string literal + --> $DIR/rustc_confusables.rs:39:25 + | +LL | #[rustc_confusables(invalid_meta_item)] + | ^^^^^^^^^^^^^^^^^ + | +help: consider surrounding this with quotes + | +LL | #[rustc_confusables("invalid_meta_item")] + | + + + +error[E0599]: no method named `inser` found for struct `rustc_confusables_across_crate::BTreeSet` in the current scope + --> $DIR/rustc_confusables.rs:12:7 + | +LL | x.inser(); + | ^^^^^ help: there is a method with a similar name: `insert` + +error[E0599]: no method named `foo` found for struct `rustc_confusables_across_crate::BTreeSet` in the current scope + --> $DIR/rustc_confusables.rs:15:7 + | +LL | x.foo(); + | ^^^ method not found in `BTreeSet` + +error[E0599]: no method named `push` found for struct `rustc_confusables_across_crate::BTreeSet` in the current scope + --> $DIR/rustc_confusables.rs:17:7 + | +LL | x.push(); + | ^^^^ method not found in `BTreeSet` + | +help: you might have meant to use `insert` + | +LL | x.insert(); + | ~~~~~~ + +error[E0599]: no method named `test` found for struct `rustc_confusables_across_crate::BTreeSet` in the current scope + --> $DIR/rustc_confusables.rs:20:7 + | +LL | x.test(); + | ^^^^ method not found in `BTreeSet` + +error[E0599]: no method named `pulled` found for struct `rustc_confusables_across_crate::BTreeSet` in the current scope + --> $DIR/rustc_confusables.rs:22:7 + | +LL | x.pulled(); + | ^^^^^^ help: there is a method with a similar name: `pull` + +error: aborting due to 9 previous errors + +Some errors have detailed explanations: E0539, E0599. +For more information about an error, try `rustc --explain E0539`.