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
65 changes: 65 additions & 0 deletions src/librustdoc/passes/collect_intra_doc_links.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -2008,6 +2009,56 @@ fn report_diagnostic(
);
}

fn suggest_path_name_typo(
collector: &LinkCollector<'_, '_>,
diag: &mut Diag<'_, ()>,
span: Option<rustc_span::Span>,
link_range: &MarkdownLinkRange,
dox: &str,
module: ModId,
unresolved: &str,
has_partial_res: bool,
disambiguator: Option<Disambiguator>,
) {
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::<Vec<_>>();
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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Creates a [Hashmap].
15 changes: 15 additions & 0 deletions tests/rustdoc-ui/intra-doc/html-as-generics-intra-doc.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -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<i32>] thing!
LL + /// This [test][ExistentStruct<i32>] thing!
|

error: unresolved link to `NonExistentStruct2`
--> $DIR/html-as-generics-intra-doc.rs:17:11
Expand All @@ -18,6 +23,11 @@ LL | /// This [NonExistentStruct2<i32>] 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<i32>] thing!
LL + /// This [ExistentStruct<i32>] thing!
|

error: unresolved link to `NonExistentStruct3`
--> $DIR/html-as-generics-intra-doc.rs:22:11
Expand All @@ -26,6 +36,11 @@ LL | /// This [NonExistentStruct3<i32>][] 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<i32>][] thing!
LL + /// This [ExistentStruct<i32>][] thing!
|

error: unclosed HTML tag `i32`
--> $DIR/html-as-generics-intra-doc.rs:9:25
Expand Down
4 changes: 4 additions & 0 deletions tests/rustdoc-ui/intra-doc/reference-links.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -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

27 changes: 27 additions & 0 deletions tests/rustdoc-ui/intra-doc/typo-imported-item-issue-159496.rs
Original file line number Diff line number Diff line change
@@ -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<HashMap<(), ()>> = 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<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() {}
71 changes: 71 additions & 0 deletions tests/rustdoc-ui/intra-doc/typo-imported-item-issue-159496.stderr
Original file line number Diff line number Diff line change
@@ -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<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<Hashmap>`].
LL + /// Creates a [`HashMap<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

Loading