diff --git a/src/librustdoc/passes/collect_intra_doc_links.rs b/src/librustdoc/passes/collect_intra_doc_links.rs index 7aee8dba82e1a..531ffd07fe43f 100644 --- a/src/librustdoc/passes/collect_intra_doc_links.rs +++ b/src/librustdoc/passes/collect_intra_doc_links.rs @@ -26,6 +26,7 @@ use rustc_resolve::rustdoc::{ }; use rustc_span::BytePos; use rustc_span::def_id::ModId; +use rustc_span::edit_distance::find_best_match_for_name; use rustc_span::symbol::{Ident, Symbol, sym}; use rustc_structures::CrateType; use smallvec::{SmallVec, smallvec}; @@ -2008,6 +2009,56 @@ fn report_diagnostic( ); } +fn suggest_path_name_typo( + collector: &LinkCollector<'_, '_>, + diag: &mut Diag<'_, ()>, + span: Option, + link_range: &MarkdownLinkRange, + dox: &str, + module: ModId, + unresolved: &str, + has_partial_res: bool, + disambiguator: Option, +) { + if unresolved.chars().count() <= 1 { + // There are too many false positives for single character typos. + return; + } + + let tcx = collector.cx.tcx; + let lookup = Symbol::intern(unresolved); + let children = if let Some(local_module) = module.as_local() { + tcx.module_children_local(local_module.to_local_def_id()) + } else { + tcx.module_children(module.to_def_id()) + }; + let candidates = children + .iter() + .filter(|child| { + disambiguator.is_none_or(|disambiguator| child.res.matches_ns(disambiguator.ns())) + }) + .map(|child| child.ident.name) + .filter(|&name| name != lookup) + .collect::>(); + let Some(candidate) = find_best_match_for_name(&candidates, lookup, None) else { + return; + }; + + let msg = format!("there's a similarly named item `{candidate}`"); + if let (Some(span), MarkdownLinkRange::Destination(range)) = (span, link_range) { + let link = &dox[range.clone()]; + // A partial resolution means that the unresolved name follows a resolved parent path. + let start = if has_partial_res { link.rfind(unresolved) } else { link.find(unresolved) }; + if let Some(start) = start { + let mut suggestion = link.to_owned(); + suggestion.replace_range(start..start + unresolved.len(), candidate.as_str()); + diag.span_suggestion_verbose(span, msg, suggestion, Applicability::MaybeIncorrect); + return; + } + } + diag.help(msg); +} + /// Reports a link that failed to resolve. /// /// This also tries to resolve any intermediate path segments that weren't @@ -2135,6 +2186,20 @@ fn resolution_failure( diag.note(note); } + if !path_is_invalid { + suggest_path_name_typo( + collector, + diag, + sp, + &link_range, + diag_info.dox, + module, + unresolved, + partial_res.is_some(), + disambiguator, + ); + } + if !path_str.contains("::") { if disambiguator.is_none_or(|d| d.ns() == MacroNS) && collector diff --git a/tests/rustdoc-ui/intra-doc/auxiliary/typo-imported-item-issue-159496.md b/tests/rustdoc-ui/intra-doc/auxiliary/typo-imported-item-issue-159496.md new file mode 100644 index 0000000000000..8f880661f5c62 --- /dev/null +++ b/tests/rustdoc-ui/intra-doc/auxiliary/typo-imported-item-issue-159496.md @@ -0,0 +1 @@ +Creates a [Hashmap]. diff --git a/tests/rustdoc-ui/intra-doc/html-as-generics-intra-doc.stderr b/tests/rustdoc-ui/intra-doc/html-as-generics-intra-doc.stderr index 7c81044dbf81b..dbcaec349f5be 100644 --- a/tests/rustdoc-ui/intra-doc/html-as-generics-intra-doc.stderr +++ b/tests/rustdoc-ui/intra-doc/html-as-generics-intra-doc.stderr @@ -10,6 +10,11 @@ note: the lint level is defined here | LL | #![deny(rustdoc::broken_intra_doc_links)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +help: there's a similarly named item `ExistentStruct` + | +LL - /// This [test][NonExistentStruct] thing! +LL + /// This [test][ExistentStruct] thing! + | error: unresolved link to `NonExistentStruct2` --> $DIR/html-as-generics-intra-doc.rs:17:11 @@ -18,6 +23,11 @@ LL | /// This [NonExistentStruct2] thing! | ^^^^^^^^^^^^^^^^^^^^^^^ no item named `NonExistentStruct2` in scope | = help: to escape `[` and `]` characters, add '\' before them like `\[` or `\]` +help: there's a similarly named item `ExistentStruct` + | +LL - /// This [NonExistentStruct2] thing! +LL + /// This [ExistentStruct] thing! + | error: unresolved link to `NonExistentStruct3` --> $DIR/html-as-generics-intra-doc.rs:22:11 @@ -26,6 +36,11 @@ LL | /// This [NonExistentStruct3][] thing! | ^^^^^^^^^^^^^^^^^^^^^^^ no item named `NonExistentStruct3` in scope | = help: to escape `[` and `]` characters, add '\' before them like `\[` or `\]` +help: there's a similarly named item `ExistentStruct` + | +LL - /// This [NonExistentStruct3][] thing! +LL + /// This [ExistentStruct][] thing! + | error: unclosed HTML tag `i32` --> $DIR/html-as-generics-intra-doc.rs:9:25 diff --git a/tests/rustdoc-ui/intra-doc/reference-links.stderr b/tests/rustdoc-ui/intra-doc/reference-links.stderr index 2a30359238ae2..ab6a6a68e500d 100644 --- a/tests/rustdoc-ui/intra-doc/reference-links.stderr +++ b/tests/rustdoc-ui/intra-doc/reference-links.stderr @@ -9,6 +9,10 @@ note: the lint level is defined here | LL | #![deny(rustdoc::broken_intra_doc_links)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +help: there's a similarly named item `Command` + | +LL | //! [a]: std::process::Command + | + error: aborting due to 1 previous error diff --git a/tests/rustdoc-ui/intra-doc/typo-imported-item-issue-159496.rs b/tests/rustdoc-ui/intra-doc/typo-imported-item-issue-159496.rs new file mode 100644 index 0000000000000..03059472298c7 --- /dev/null +++ b/tests/rustdoc-ui/intra-doc/typo-imported-item-issue-159496.rs @@ -0,0 +1,27 @@ +// Suggest similarly named module items, including imports, for unresolved intra-doc links. + +#![deny(rustdoc::broken_intra_doc_links)] + +use std::collections::HashMap; + +/// Creates a [Hashmap]. +//~^ ERROR unresolved link to `Hashmap` +pub fn create() { + let _: Option> = None; +} + +/// Creates a [std::collections::Hashmap]. +//~^ ERROR unresolved link to `std::collections::Hashmap` +pub fn qualified() {} + +/// Creates a [type@Hashmap]. +//~^ ERROR unresolved link to `Hashmap` +pub fn disambiguated() {} + +/// Creates a [`Hashmap`]. +//~^ ERROR unresolved link to `Hashmap` +pub fn generic() {} + +#[doc = include_str!("auxiliary/typo-imported-item-issue-159496.md")] +//~? ERROR unresolved link to `Hashmap` +pub fn included() {} diff --git a/tests/rustdoc-ui/intra-doc/typo-imported-item-issue-159496.stderr b/tests/rustdoc-ui/intra-doc/typo-imported-item-issue-159496.stderr new file mode 100644 index 0000000000000..03e1f9b94e6b3 --- /dev/null +++ b/tests/rustdoc-ui/intra-doc/typo-imported-item-issue-159496.stderr @@ -0,0 +1,71 @@ +error: unresolved link to `Hashmap` + --> $DIR/typo-imported-item-issue-159496.rs:7:16 + | +LL | /// Creates a [Hashmap]. + | ^^^^^^^ no item named `Hashmap` in scope + | + = help: to escape `[` and `]` characters, add '\' before them like `\[` or `\]` +note: the lint level is defined here + --> $DIR/typo-imported-item-issue-159496.rs:3:9 + | +LL | #![deny(rustdoc::broken_intra_doc_links)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +help: there's a similarly named item `HashMap` + | +LL - /// Creates a [Hashmap]. +LL + /// Creates a [HashMap]. + | + +error: unresolved link to `std::collections::Hashmap` + --> $DIR/typo-imported-item-issue-159496.rs:13:16 + | +LL | /// Creates a [std::collections::Hashmap]. + | ^^^^^^^^^^^^^^^^^^^^^^^^^ no item named `Hashmap` in module `collections` + | +help: there's a similarly named item `HashMap` + | +LL - /// Creates a [std::collections::Hashmap]. +LL + /// Creates a [std::collections::HashMap]. + | + +error: unresolved link to `Hashmap` + --> $DIR/typo-imported-item-issue-159496.rs:17:16 + | +LL | /// Creates a [type@Hashmap]. + | ^^^^^^^^^^^^ no item named `Hashmap` in scope + | + = help: to escape `[` and `]` characters, add '\' before them like `\[` or `\]` +help: there's a similarly named item `HashMap` + | +LL - /// Creates a [type@Hashmap]. +LL + /// Creates a [type@HashMap]. + | + +error: unresolved link to `Hashmap` + --> $DIR/typo-imported-item-issue-159496.rs:21:17 + | +LL | /// Creates a [`Hashmap`]. + | ^^^^^^^^^^^^^^^^ no item named `Hashmap` in scope + | + = help: to escape `[` and `]` characters, add '\' before them like `\[` or `\]` +help: there's a similarly named item `HashMap` + | +LL - /// Creates a [`Hashmap`]. +LL + /// Creates a [`HashMap`]. + | + +error: unresolved link to `Hashmap` + --> $DIR/auxiliary/typo-imported-item-issue-159496.md:1:12 + | +LL | Creates a [Hashmap]. + | ^^^^^^^ no item named `Hashmap` in scope + | + = help: to escape `[` and `]` characters, add '\' before them like `\[` or `\]` +help: there's a similarly named item `HashMap` + | +LL - Creates a [Hashmap]. +LL + Creates a [HashMap]. + | + +error: aborting due to 5 previous errors +