1
Fork 0

Only report reference-style link errors once

Co-authored-by: Joshua Nelson <joshua@yottadb.com>
This commit is contained in:
Dániel Buga 2020-10-12 16:52:20 +02:00
parent e821a6ef78
commit 4b612dd9cc
6 changed files with 175 additions and 67 deletions

View file

@ -37,7 +37,9 @@ use crate::doctest;
use crate::html::highlight; use crate::html::highlight;
use crate::html::toc::TocBuilder; use crate::html::toc::TocBuilder;
use pulldown_cmark::{html, BrokenLink, CodeBlockKind, CowStr, Event, Options, Parser, Tag}; use pulldown_cmark::{
html, BrokenLink, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag,
};
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
@ -327,8 +329,6 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> {
type Item = Event<'a>; type Item = Event<'a>;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
use pulldown_cmark::LinkType;
let mut event = self.inner.next(); let mut event = self.inner.next();
// Replace intra-doc links and remove disambiguators from shortcut links (`[fn@f]`). // Replace intra-doc links and remove disambiguators from shortcut links (`[fn@f]`).
@ -1123,7 +1123,13 @@ crate fn plain_text_summary(md: &str) -> String {
s s
} }
crate fn markdown_links(md: &str) -> Vec<(String, Range<usize>)> { crate struct MarkdownLink {
pub kind: LinkType,
pub link: String,
pub range: Range<usize>,
}
crate fn markdown_links(md: &str) -> Vec<MarkdownLink> {
if md.is_empty() { if md.is_empty() {
return vec![]; return vec![];
} }
@ -1163,7 +1169,11 @@ crate fn markdown_links(md: &str) -> Vec<(String, Range<usize>)> {
let mut push = |link: BrokenLink<'_>| { let mut push = |link: BrokenLink<'_>| {
let span = span_for_link(&CowStr::Borrowed(link.reference), link.span); let span = span_for_link(&CowStr::Borrowed(link.reference), link.span);
links.borrow_mut().push((link.reference.to_owned(), span)); links.borrow_mut().push(MarkdownLink {
kind: LinkType::ShortcutUnknown,
link: link.reference.to_owned(),
range: span,
});
None None
}; };
let p = Parser::new_with_broken_link_callback(md, opts(), Some(&mut push)).into_offset_iter(); 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>)> {
let iter = Footnotes::new(HeadingLinks::new(p, None, &mut ids)); let iter = Footnotes::new(HeadingLinks::new(p, None, &mut ids));
for ev in iter { for ev in iter {
if let Event::Start(Tag::Link(_, dest, _)) = ev.0 { if let Event::Start(Tag::Link(kind, dest, _)) = ev.0 {
debug!("found link: {}", dest); debug!("found link: {}", dest);
let span = span_for_link(&dest, ev.1); let span = span_for_link(&dest, ev.1);
links.borrow_mut().push((dest.into_string(), span)); links.borrow_mut().push(MarkdownLink { kind, link: dest.into_string(), range: span });
} }
} }

View file

@ -25,6 +25,8 @@ use rustc_span::symbol::Symbol;
use rustc_span::DUMMY_SP; use rustc_span::DUMMY_SP;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use pulldown_cmark::LinkType;
use std::borrow::Cow; use std::borrow::Cow;
use std::cell::Cell; use std::cell::Cell;
use std::convert::{TryFrom, TryInto}; use std::convert::{TryFrom, TryInto};
@ -34,7 +36,7 @@ use std::ops::Range;
use crate::clean::{self, utils::find_nearest_parent_module, Crate, Item, ItemLink, PrimitiveType}; use crate::clean::{self, utils::find_nearest_parent_module, Crate, Item, ItemLink, PrimitiveType};
use crate::core::DocContext; use crate::core::DocContext;
use crate::fold::DocFolder; use crate::fold::DocFolder;
use crate::html::markdown::markdown_links; use crate::html::markdown::{markdown_links, MarkdownLink};
use crate::passes::Pass; use crate::passes::Pass;
use super::span_of_attrs; use super::span_of_attrs;
@ -265,8 +267,9 @@ struct LinkCollector<'a, 'tcx> {
/// because `clean` and the disambiguator code expect them to be different. /// because `clean` and the disambiguator code expect them to be different.
/// See the code for associated items on inherent impls for details. /// See the code for associated items on inherent impls for details.
kind_side_channel: Cell<Option<(DefKind, DefId)>>, kind_side_channel: Cell<Option<(DefKind, DefId)>>,
/// Cache the resolved links so we can avoid resolving (and emitting errors for) the same link /// Cache the resolved links so we can avoid resolving (and emitting errors for) the same link.
visited_links: FxHashMap<ResolutionInfo, CachedLink>, /// The link will be `None` if it could not be resolved (i.e. the error was cached).
visited_links: FxHashMap<ResolutionInfo, Option<CachedLink>>,
} }
impl<'a, 'tcx> LinkCollector<'a, 'tcx> { impl<'a, 'tcx> LinkCollector<'a, 'tcx> {
@ -901,16 +904,8 @@ impl<'a, 'tcx> DocFolder for LinkCollector<'a, 'tcx> {
}; };
// NOTE: if there are links that start in one crate and end in another, this will not resolve them. // NOTE: if there are links that start in one crate and end in another, this will not resolve them.
// This is a degenerate case and it's not supported by rustdoc. // This is a degenerate case and it's not supported by rustdoc.
for (ori_link, link_range) in markdown_links(&doc) { for md_link in markdown_links(&doc) {
let link = self.resolve_link( let link = self.resolve_link(&item, &doc, &self_name, parent_node, krate, md_link);
&item,
&doc,
&self_name,
parent_node,
krate,
ori_link,
link_range,
);
if let Some(link) = link { if let Some(link) = link {
item.attrs.links.push(link); item.attrs.links.push(link);
} }
@ -942,27 +937,26 @@ impl LinkCollector<'_, '_> {
self_name: &Option<String>, self_name: &Option<String>,
parent_node: Option<DefId>, parent_node: Option<DefId>,
krate: CrateNum, krate: CrateNum,
ori_link: String, ori_link: MarkdownLink,
link_range: Range<usize>,
) -> Option<ItemLink> { ) -> Option<ItemLink> {
trace!("considering link '{}'", ori_link); trace!("considering link '{}'", ori_link.link);
// Bail early for real links. // Bail early for real links.
if ori_link.contains('/') { if ori_link.link.contains('/') {
return None; return None;
} }
// [] is mostly likely not supposed to be a link // [] is mostly likely not supposed to be a link
if ori_link.is_empty() { if ori_link.link.is_empty() {
return None; return None;
} }
let cx = self.cx; let cx = self.cx;
let link = ori_link.replace("`", ""); let link = ori_link.link.replace("`", "");
let parts = link.split('#').collect::<Vec<_>>(); let parts = link.split('#').collect::<Vec<_>>();
let (link, extra_fragment) = if parts.len() > 2 { let (link, extra_fragment) = if parts.len() > 2 {
// A valid link can't have multiple #'s // A valid link can't have multiple #'s
anchor_failure(cx, &item, &link, dox, link_range, AnchorFailure::MultipleAnchors); anchor_failure(cx, &item, &link, dox, ori_link.range, AnchorFailure::MultipleAnchors);
return None; return None;
} else if parts.len() == 2 { } else if parts.len() == 2 {
if parts[0].trim().is_empty() { if parts[0].trim().is_empty() {
@ -1018,7 +1012,7 @@ impl LinkCollector<'_, '_> {
path_str, path_str,
disambiguator, disambiguator,
dox, dox,
link_range, ori_link.range,
smallvec![ResolutionFailure::NoParentItem], smallvec![ResolutionFailure::NoParentItem],
); );
return None; return None;
@ -1058,7 +1052,7 @@ impl LinkCollector<'_, '_> {
path_str, path_str,
disambiguator, disambiguator,
dox, dox,
link_range, ori_link.range,
smallvec![err_kind], smallvec![err_kind],
); );
return None; return None;
@ -1074,15 +1068,22 @@ impl LinkCollector<'_, '_> {
return None; return None;
} }
let key = ResolutionInfo { let diag_info = DiagnosticInfo {
module_id, item,
dis: disambiguator, dox,
path_str: path_str.to_owned(), ori_link: &ori_link.link,
extra_fragment, link_range: ori_link.range.clone(),
}; };
let diag = let (mut res, mut fragment) = self.resolve_with_disambiguator_cached(
DiagnosticInfo { item, dox, ori_link: &ori_link, link_range: link_range.clone() }; ResolutionInfo {
let (mut res, mut fragment) = self.resolve_with_disambiguator_cached(key, diag)?; module_id,
dis: disambiguator,
path_str: path_str.to_owned(),
extra_fragment,
},
diag_info,
matches!(ori_link.kind, LinkType::Reference | LinkType::Shortcut),
)?;
// Check for a primitive which might conflict with a module // Check for a primitive which might conflict with a module
// Report the ambiguity and require that the user specify which one they meant. // Report the ambiguity and require that the user specify which one they meant.
@ -1101,7 +1102,7 @@ impl LinkCollector<'_, '_> {
&item, &item,
path_str, path_str,
dox, dox,
link_range, ori_link.range,
AnchorFailure::RustdocAnchorConflict(prim), AnchorFailure::RustdocAnchorConflict(prim),
); );
return None; return None;
@ -1111,7 +1112,7 @@ impl LinkCollector<'_, '_> {
} else { } else {
// `[char]` when a `char` module is in scope // `[char]` when a `char` module is in scope
let candidates = vec![res, prim]; let candidates = vec![res, prim];
ambiguity_error(cx, &item, path_str, dox, link_range, candidates); ambiguity_error(cx, &item, path_str, dox, ori_link.range, candidates);
return None; return None;
} }
} }
@ -1129,14 +1130,22 @@ impl LinkCollector<'_, '_> {
specified.descr() specified.descr()
); );
diag.note(&note); diag.note(&note);
suggest_disambiguator(resolved, diag, path_str, dox, sp, &link_range); suggest_disambiguator(resolved, diag, path_str, dox, sp, &ori_link.range);
}; };
report_diagnostic(cx, BROKEN_INTRA_DOC_LINKS, &msg, &item, dox, &link_range, callback); report_diagnostic(
cx,
BROKEN_INTRA_DOC_LINKS,
&msg,
&item,
dox,
&ori_link.range,
callback,
);
}; };
match res { match res {
Res::Primitive(_) => match disambiguator { Res::Primitive(_) => match disambiguator {
Some(Disambiguator::Primitive | Disambiguator::Namespace(_)) | None => { Some(Disambiguator::Primitive | Disambiguator::Namespace(_)) | None => {
Some(ItemLink { link: ori_link, link_text, did: None, fragment }) Some(ItemLink { link: ori_link.link, link_text, did: None, fragment })
} }
Some(other) => { Some(other) => {
report_mismatch(other, Disambiguator::Primitive); report_mismatch(other, Disambiguator::Primitive);
@ -1179,11 +1188,11 @@ impl LinkCollector<'_, '_> {
if self.cx.tcx.privacy_access_levels(LOCAL_CRATE).is_exported(hir_src) if self.cx.tcx.privacy_access_levels(LOCAL_CRATE).is_exported(hir_src)
&& !self.cx.tcx.privacy_access_levels(LOCAL_CRATE).is_exported(hir_dst) && !self.cx.tcx.privacy_access_levels(LOCAL_CRATE).is_exported(hir_dst)
{ {
privacy_error(cx, &item, &path_str, dox, link_range); privacy_error(cx, &item, &path_str, dox, &ori_link);
} }
} }
let id = clean::register_res(cx, rustc_hir::def::Res::Def(kind, id)); let id = clean::register_res(cx, rustc_hir::def::Res::Def(kind, id));
Some(ItemLink { link: ori_link, link_text, did: Some(id), fragment }) Some(ItemLink { link: ori_link.link, link_text, did: Some(id), fragment })
} }
} }
} }
@ -1192,28 +1201,47 @@ impl LinkCollector<'_, '_> {
&mut self, &mut self,
key: ResolutionInfo, key: ResolutionInfo,
diag: DiagnosticInfo<'_>, diag: DiagnosticInfo<'_>,
cache_resolution_failure: bool,
) -> Option<(Res, Option<String>)> { ) -> Option<(Res, Option<String>)> {
// Try to look up both the result and the corresponding side channel value // Try to look up both the result and the corresponding side channel value
if let Some(ref cached) = self.visited_links.get(&key) { if let Some(ref cached) = self.visited_links.get(&key) {
self.kind_side_channel.set(cached.side_channel); match cached {
return Some(cached.res.clone()); Some(cached) => {
self.kind_side_channel.set(cached.side_channel.clone());
return Some(cached.res.clone());
}
None if cache_resolution_failure => return None,
None => {
// Although we hit the cache and found a resolution error, this link isn't
// supposed to cache those. Run link resolution again to emit the expected
// resolution error.
}
}
} }
let res = self.resolve_with_disambiguator(&key, diag); let res = self.resolve_with_disambiguator(&key, diag);
// Cache only if resolved successfully - don't silence duplicate errors // Cache only if resolved successfully - don't silence duplicate errors
if let Some(res) = &res { if let Some(res) = res {
// Store result for the actual namespace // Store result for the actual namespace
self.visited_links.insert( self.visited_links.insert(
key, key,
CachedLink { Some(CachedLink {
res: res.clone(), res: res.clone(),
side_channel: self.kind_side_channel.clone().into_inner(), side_channel: self.kind_side_channel.clone().into_inner(),
}, }),
); );
}
res Some(res)
} else {
if cache_resolution_failure {
// For reference-style links we only want to report one resolution error
// so let's cache them as well.
self.visited_links.insert(key, None);
}
None
}
} }
/// After parsing the disambiguator, resolve the main part of the link. /// After parsing the disambiguator, resolve the main part of the link.
@ -1964,13 +1992,7 @@ fn suggest_disambiguator(
} }
/// Report a link from a public item to a private one. /// Report a link from a public item to a private one.
fn privacy_error( fn privacy_error(cx: &DocContext<'_>, item: &Item, path_str: &str, dox: &str, link: &MarkdownLink) {
cx: &DocContext<'_>,
item: &Item,
path_str: &str,
dox: &str,
link_range: Range<usize>,
) {
let sym; let sym;
let item_name = match item.name { let item_name = match item.name {
Some(name) => { Some(name) => {
@ -1982,7 +2004,7 @@ fn privacy_error(
let msg = let msg =
format!("public documentation for `{}` links to private item `{}`", item_name, path_str); format!("public documentation for `{}` links to private item `{}`", item_name, path_str);
report_diagnostic(cx, PRIVATE_INTRA_DOC_LINKS, &msg, item, dox, &link_range, |diag, sp| { report_diagnostic(cx, PRIVATE_INTRA_DOC_LINKS, &msg, item, dox, &link.range, |diag, sp| {
if let Some(sp) = sp { if let Some(sp) = sp {
diag.span_label(sp, "this item is private"); diag.span_label(sp, "this item is private");
} }

View file

@ -0,0 +1,20 @@
#![deny(broken_intra_doc_links)]
/// Links to [a] [link][a]
/// And also a [third link][a]
/// And also a [reference link][b]
///
/// Other links to the same target should still emit error: [ref] //~ERROR unresolved link to `ref`
/// Duplicate [ref] //~ERROR unresolved link to `ref`
///
/// Other links to other targets should still emit error: [ref2] //~ERROR unresolved link to `ref2`
/// Duplicate [ref2] //~ERROR unresolved link to `ref2`
///
/// [a]: ref
//~^ ERROR unresolved link to `ref`
/// [b]: ref2
//~^ ERROR unresolved link to
/// [ref][]
//~^ ERROR unresolved link
pub fn f() {}

View file

@ -0,0 +1,63 @@
error: unresolved link to `ref`
--> $DIR/reference-link-reports-error-once.rs:13:10
|
LL | /// [a]: ref
| ^^^ no item named `ref` in scope
|
note: the lint level is defined here
--> $DIR/reference-link-reports-error-once.rs:1:9
|
LL | #![deny(broken_intra_doc_links)]
| ^^^^^^^^^^^^^^^^^^^^^^
= help: to escape `[` and `]` characters, add '\' before them like `\[` or `\]`
error: unresolved link to `ref2`
--> $DIR/reference-link-reports-error-once.rs:15:10
|
LL | /// [b]: ref2
| ^^^^ no item named `ref2` in scope
|
= help: to escape `[` and `]` characters, add '\' before them like `\[` or `\]`
error: unresolved link to `ref`
--> $DIR/reference-link-reports-error-once.rs:7:62
|
LL | /// Other links to the same target should still emit error: [ref]
| ^^^ no item named `ref` in scope
|
= help: to escape `[` and `]` characters, add '\' before them like `\[` or `\]`
error: unresolved link to `ref`
--> $DIR/reference-link-reports-error-once.rs:8:16
|
LL | /// Duplicate [ref]
| ^^^ no item named `ref` in scope
|
= help: to escape `[` and `]` characters, add '\' before them like `\[` or `\]`
error: unresolved link to `ref2`
--> $DIR/reference-link-reports-error-once.rs:10:60
|
LL | /// Other links to other targets should still emit error: [ref2]
| ^^^^ no item named `ref2` in scope
|
= help: to escape `[` and `]` characters, add '\' before them like `\[` or `\]`
error: unresolved link to `ref2`
--> $DIR/reference-link-reports-error-once.rs:11:16
|
LL | /// Duplicate [ref2]
| ^^^^ no item named `ref2` in scope
|
= help: to escape `[` and `]` characters, add '\' before them like `\[` or `\]`
error: unresolved link to `ref`
--> $DIR/reference-link-reports-error-once.rs:18:6
|
LL | /// [ref][]
| ^^^ no item named `ref` in scope
|
= help: to escape `[` and `]` characters, add '\' before them like `\[` or `\]`
error: aborting due to 7 previous errors

View file

@ -4,4 +4,3 @@
//! //!
//! [a]: std::process::Comman //! [a]: std::process::Comman
//~^ ERROR unresolved //~^ ERROR unresolved
//~| ERROR unresolved

View file

@ -10,11 +10,5 @@ note: the lint level is defined here
LL | #![deny(broken_intra_doc_links)] LL | #![deny(broken_intra_doc_links)]
| ^^^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^^^
error: unresolved link to `std::process::Comman` error: aborting due to previous error
--> $DIR/reference-links.rs:5:10
|
LL | //! [a]: std::process::Comman
| ^^^^^^^^^^^^^^^^^^^^ no item named `Comman` in module `process`
error: aborting due to 2 previous errors