Skip to content

Commit 8018418

Browse files
committed
Auto merge of #77859 - bugadani:no-duplicate-ref-link-error, r=jyn514
Rustdoc: only report broken ref-style links once This PR assigns the markdown `LinkType` to each parsed link and passes this information into the link collector. If a link can't be resolved in `resolve_with_disambiguator`, the failure is cached for the link types where we only want to report the error once (namely `Shortcut` and `Reference`). Fixes #77681
2 parents db69136 + 4b612dd commit 8018418

6 files changed

+175
-67
lines changed

src/librustdoc/html/markdown.rs

+17-7
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ use crate::doctest;
3737
use crate::html::highlight;
3838
use crate::html::toc::TocBuilder;
3939

40-
use pulldown_cmark::{html, BrokenLink, CodeBlockKind, CowStr, Event, Options, Parser, Tag};
40+
use pulldown_cmark::{
41+
html, BrokenLink, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag,
42+
};
4143

4244
#[cfg(test)]
4345
mod tests;
@@ -327,8 +329,6 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> {
327329
type Item = Event<'a>;
328330

329331
fn next(&mut self) -> Option<Self::Item> {
330-
use pulldown_cmark::LinkType;
331-
332332
let mut event = self.inner.next();
333333

334334
// Replace intra-doc links and remove disambiguators from shortcut links (`[fn@f]`).
@@ -1123,7 +1123,13 @@ crate fn plain_text_summary(md: &str) -> String {
11231123
s
11241124
}
11251125

1126-
crate fn markdown_links(md: &str) -> Vec<(String, Range<usize>)> {
1126+
crate struct MarkdownLink {
1127+
pub kind: LinkType,
1128+
pub link: String,
1129+
pub range: Range<usize>,
1130+
}
1131+
1132+
crate fn markdown_links(md: &str) -> Vec<MarkdownLink> {
11271133
if md.is_empty() {
11281134
return vec![];
11291135
}
@@ -1163,7 +1169,11 @@ crate fn markdown_links(md: &str) -> Vec<(String, Range<usize>)> {
11631169

11641170
let mut push = |link: BrokenLink<'_>| {
11651171
let span = span_for_link(&CowStr::Borrowed(link.reference), link.span);
1166-
links.borrow_mut().push((link.reference.to_owned(), span));
1172+
links.borrow_mut().push(MarkdownLink {
1173+
kind: LinkType::ShortcutUnknown,
1174+
link: link.reference.to_owned(),
1175+
range: span,
1176+
});
11671177
None
11681178
};
11691179
let p = Parser::new_with_broken_link_callback(md, opts(), Some(&mut push)).into_offset_iter();
@@ -1174,10 +1184,10 @@ crate fn markdown_links(md: &str) -> Vec<(String, Range<usize>)> {
11741184
let iter = Footnotes::new(HeadingLinks::new(p, None, &mut ids));
11751185

11761186
for ev in iter {
1177-
if let Event::Start(Tag::Link(_, dest, _)) = ev.0 {
1187+
if let Event::Start(Tag::Link(kind, dest, _)) = ev.0 {
11781188
debug!("found link: {}", dest);
11791189
let span = span_for_link(&dest, ev.1);
1180-
links.borrow_mut().push((dest.into_string(), span));
1190+
links.borrow_mut().push(MarkdownLink { kind, link: dest.into_string(), range: span });
11811191
}
11821192
}
11831193

src/librustdoc/passes/collect_intra_doc_links.rs

+74-52
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ use rustc_span::symbol::Symbol;
2525
use rustc_span::DUMMY_SP;
2626
use smallvec::{smallvec, SmallVec};
2727

28+
use pulldown_cmark::LinkType;
29+
2830
use std::borrow::Cow;
2931
use std::cell::Cell;
3032
use std::convert::{TryFrom, TryInto};
@@ -34,7 +36,7 @@ use std::ops::Range;
3436
use crate::clean::{self, utils::find_nearest_parent_module, Crate, Item, ItemLink, PrimitiveType};
3537
use crate::core::DocContext;
3638
use crate::fold::DocFolder;
37-
use crate::html::markdown::markdown_links;
39+
use crate::html::markdown::{markdown_links, MarkdownLink};
3840
use crate::passes::Pass;
3941

4042
use super::span_of_attrs;
@@ -265,8 +267,9 @@ struct LinkCollector<'a, 'tcx> {
265267
/// because `clean` and the disambiguator code expect them to be different.
266268
/// See the code for associated items on inherent impls for details.
267269
kind_side_channel: Cell<Option<(DefKind, DefId)>>,
268-
/// Cache the resolved links so we can avoid resolving (and emitting errors for) the same link
269-
visited_links: FxHashMap<ResolutionInfo, CachedLink>,
270+
/// Cache the resolved links so we can avoid resolving (and emitting errors for) the same link.
271+
/// The link will be `None` if it could not be resolved (i.e. the error was cached).
272+
visited_links: FxHashMap<ResolutionInfo, Option<CachedLink>>,
270273
}
271274

272275
impl<'a, 'tcx> LinkCollector<'a, 'tcx> {
@@ -901,16 +904,8 @@ impl<'a, 'tcx> DocFolder for LinkCollector<'a, 'tcx> {
901904
};
902905
// NOTE: if there are links that start in one crate and end in another, this will not resolve them.
903906
// This is a degenerate case and it's not supported by rustdoc.
904-
for (ori_link, link_range) in markdown_links(&doc) {
905-
let link = self.resolve_link(
906-
&item,
907-
&doc,
908-
&self_name,
909-
parent_node,
910-
krate,
911-
ori_link,
912-
link_range,
913-
);
907+
for md_link in markdown_links(&doc) {
908+
let link = self.resolve_link(&item, &doc, &self_name, parent_node, krate, md_link);
914909
if let Some(link) = link {
915910
item.attrs.links.push(link);
916911
}
@@ -942,27 +937,26 @@ impl LinkCollector<'_, '_> {
942937
self_name: &Option<String>,
943938
parent_node: Option<DefId>,
944939
krate: CrateNum,
945-
ori_link: String,
946-
link_range: Range<usize>,
940+
ori_link: MarkdownLink,
947941
) -> Option<ItemLink> {
948-
trace!("considering link '{}'", ori_link);
942+
trace!("considering link '{}'", ori_link.link);
949943

950944
// Bail early for real links.
951-
if ori_link.contains('/') {
945+
if ori_link.link.contains('/') {
952946
return None;
953947
}
954948

955949
// [] is mostly likely not supposed to be a link
956-
if ori_link.is_empty() {
950+
if ori_link.link.is_empty() {
957951
return None;
958952
}
959953

960954
let cx = self.cx;
961-
let link = ori_link.replace("`", "");
955+
let link = ori_link.link.replace("`", "");
962956
let parts = link.split('#').collect::<Vec<_>>();
963957
let (link, extra_fragment) = if parts.len() > 2 {
964958
// A valid link can't have multiple #'s
965-
anchor_failure(cx, &item, &link, dox, link_range, AnchorFailure::MultipleAnchors);
959+
anchor_failure(cx, &item, &link, dox, ori_link.range, AnchorFailure::MultipleAnchors);
966960
return None;
967961
} else if parts.len() == 2 {
968962
if parts[0].trim().is_empty() {
@@ -1018,7 +1012,7 @@ impl LinkCollector<'_, '_> {
10181012
path_str,
10191013
disambiguator,
10201014
dox,
1021-
link_range,
1015+
ori_link.range,
10221016
smallvec![ResolutionFailure::NoParentItem],
10231017
);
10241018
return None;
@@ -1058,7 +1052,7 @@ impl LinkCollector<'_, '_> {
10581052
path_str,
10591053
disambiguator,
10601054
dox,
1061-
link_range,
1055+
ori_link.range,
10621056
smallvec![err_kind],
10631057
);
10641058
return None;
@@ -1074,15 +1068,22 @@ impl LinkCollector<'_, '_> {
10741068
return None;
10751069
}
10761070

1077-
let key = ResolutionInfo {
1078-
module_id,
1079-
dis: disambiguator,
1080-
path_str: path_str.to_owned(),
1081-
extra_fragment,
1071+
let diag_info = DiagnosticInfo {
1072+
item,
1073+
dox,
1074+
ori_link: &ori_link.link,
1075+
link_range: ori_link.range.clone(),
10821076
};
1083-
let diag =
1084-
DiagnosticInfo { item, dox, ori_link: &ori_link, link_range: link_range.clone() };
1085-
let (mut res, mut fragment) = self.resolve_with_disambiguator_cached(key, diag)?;
1077+
let (mut res, mut fragment) = self.resolve_with_disambiguator_cached(
1078+
ResolutionInfo {
1079+
module_id,
1080+
dis: disambiguator,
1081+
path_str: path_str.to_owned(),
1082+
extra_fragment,
1083+
},
1084+
diag_info,
1085+
matches!(ori_link.kind, LinkType::Reference | LinkType::Shortcut),
1086+
)?;
10861087

10871088
// Check for a primitive which might conflict with a module
10881089
// Report the ambiguity and require that the user specify which one they meant.
@@ -1101,7 +1102,7 @@ impl LinkCollector<'_, '_> {
11011102
&item,
11021103
path_str,
11031104
dox,
1104-
link_range,
1105+
ori_link.range,
11051106
AnchorFailure::RustdocAnchorConflict(prim),
11061107
);
11071108
return None;
@@ -1111,7 +1112,7 @@ impl LinkCollector<'_, '_> {
11111112
} else {
11121113
// `[char]` when a `char` module is in scope
11131114
let candidates = vec![res, prim];
1114-
ambiguity_error(cx, &item, path_str, dox, link_range, candidates);
1115+
ambiguity_error(cx, &item, path_str, dox, ori_link.range, candidates);
11151116
return None;
11161117
}
11171118
}
@@ -1129,14 +1130,22 @@ impl LinkCollector<'_, '_> {
11291130
specified.descr()
11301131
);
11311132
diag.note(&note);
1132-
suggest_disambiguator(resolved, diag, path_str, dox, sp, &link_range);
1133+
suggest_disambiguator(resolved, diag, path_str, dox, sp, &ori_link.range);
11331134
};
1134-
report_diagnostic(cx, BROKEN_INTRA_DOC_LINKS, &msg, &item, dox, &link_range, callback);
1135+
report_diagnostic(
1136+
cx,
1137+
BROKEN_INTRA_DOC_LINKS,
1138+
&msg,
1139+
&item,
1140+
dox,
1141+
&ori_link.range,
1142+
callback,
1143+
);
11351144
};
11361145
match res {
11371146
Res::Primitive(_) => match disambiguator {
11381147
Some(Disambiguator::Primitive | Disambiguator::Namespace(_)) | None => {
1139-
Some(ItemLink { link: ori_link, link_text, did: None, fragment })
1148+
Some(ItemLink { link: ori_link.link, link_text, did: None, fragment })
11401149
}
11411150
Some(other) => {
11421151
report_mismatch(other, Disambiguator::Primitive);
@@ -1179,11 +1188,11 @@ impl LinkCollector<'_, '_> {
11791188
if self.cx.tcx.privacy_access_levels(LOCAL_CRATE).is_exported(hir_src)
11801189
&& !self.cx.tcx.privacy_access_levels(LOCAL_CRATE).is_exported(hir_dst)
11811190
{
1182-
privacy_error(cx, &item, &path_str, dox, link_range);
1191+
privacy_error(cx, &item, &path_str, dox, &ori_link);
11831192
}
11841193
}
11851194
let id = clean::register_res(cx, rustc_hir::def::Res::Def(kind, id));
1186-
Some(ItemLink { link: ori_link, link_text, did: Some(id), fragment })
1195+
Some(ItemLink { link: ori_link.link, link_text, did: Some(id), fragment })
11871196
}
11881197
}
11891198
}
@@ -1192,28 +1201,47 @@ impl LinkCollector<'_, '_> {
11921201
&mut self,
11931202
key: ResolutionInfo,
11941203
diag: DiagnosticInfo<'_>,
1204+
cache_resolution_failure: bool,
11951205
) -> Option<(Res, Option<String>)> {
11961206
// Try to look up both the result and the corresponding side channel value
11971207
if let Some(ref cached) = self.visited_links.get(&key) {
1198-
self.kind_side_channel.set(cached.side_channel);
1199-
return Some(cached.res.clone());
1208+
match cached {
1209+
Some(cached) => {
1210+
self.kind_side_channel.set(cached.side_channel.clone());
1211+
return Some(cached.res.clone());
1212+
}
1213+
None if cache_resolution_failure => return None,
1214+
None => {
1215+
// Although we hit the cache and found a resolution error, this link isn't
1216+
// supposed to cache those. Run link resolution again to emit the expected
1217+
// resolution error.
1218+
}
1219+
}
12001220
}
12011221

12021222
let res = self.resolve_with_disambiguator(&key, diag);
12031223

12041224
// Cache only if resolved successfully - don't silence duplicate errors
1205-
if let Some(res) = &res {
1225+
if let Some(res) = res {
12061226
// Store result for the actual namespace
12071227
self.visited_links.insert(
12081228
key,
1209-
CachedLink {
1229+
Some(CachedLink {
12101230
res: res.clone(),
12111231
side_channel: self.kind_side_channel.clone().into_inner(),
1212-
},
1232+
}),
12131233
);
1214-
}
12151234

1216-
res
1235+
Some(res)
1236+
} else {
1237+
if cache_resolution_failure {
1238+
// For reference-style links we only want to report one resolution error
1239+
// so let's cache them as well.
1240+
self.visited_links.insert(key, None);
1241+
}
1242+
1243+
None
1244+
}
12171245
}
12181246

12191247
/// After parsing the disambiguator, resolve the main part of the link.
@@ -1964,13 +1992,7 @@ fn suggest_disambiguator(
19641992
}
19651993

19661994
/// Report a link from a public item to a private one.
1967-
fn privacy_error(
1968-
cx: &DocContext<'_>,
1969-
item: &Item,
1970-
path_str: &str,
1971-
dox: &str,
1972-
link_range: Range<usize>,
1973-
) {
1995+
fn privacy_error(cx: &DocContext<'_>, item: &Item, path_str: &str, dox: &str, link: &MarkdownLink) {
19741996
let sym;
19751997
let item_name = match item.name {
19761998
Some(name) => {
@@ -1982,7 +2004,7 @@ fn privacy_error(
19822004
let msg =
19832005
format!("public documentation for `{}` links to private item `{}`", item_name, path_str);
19842006

1985-
report_diagnostic(cx, PRIVATE_INTRA_DOC_LINKS, &msg, item, dox, &link_range, |diag, sp| {
2007+
report_diagnostic(cx, PRIVATE_INTRA_DOC_LINKS, &msg, item, dox, &link.range, |diag, sp| {
19862008
if let Some(sp) = sp {
19872009
diag.span_label(sp, "this item is private");
19882010
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#![deny(broken_intra_doc_links)]
2+
3+
/// Links to [a] [link][a]
4+
/// And also a [third link][a]
5+
/// And also a [reference link][b]
6+
///
7+
/// Other links to the same target should still emit error: [ref] //~ERROR unresolved link to `ref`
8+
/// Duplicate [ref] //~ERROR unresolved link to `ref`
9+
///
10+
/// Other links to other targets should still emit error: [ref2] //~ERROR unresolved link to `ref2`
11+
/// Duplicate [ref2] //~ERROR unresolved link to `ref2`
12+
///
13+
/// [a]: ref
14+
//~^ ERROR unresolved link to `ref`
15+
/// [b]: ref2
16+
//~^ ERROR unresolved link to
17+
18+
/// [ref][]
19+
//~^ ERROR unresolved link
20+
pub fn f() {}

0 commit comments

Comments
 (0)