Rollup merge of #112014 - notriddle:notriddle/intra-doc-weird-syntax, r=GuillaumeGomez,fmease

rustdoc: get unnormalized link destination for suggestions

Fixes #110111

This bug, and the workaround in this PR, is closely linked to [raphlinus/pulldown-cmark#441], getting offsets of link components. In particular, pulldown-cmark doesn't provide the offsets of the contents of a link.

To work around this, rustdoc parser parts of a link definition itself.

[raphlinus/pulldown-cmark#441]: https://github.com/raphlinus/pulldown-cmark/issues/441
This commit is contained in:
Guillaume Gomez 2023-05-27 13:38:33 +02:00 committed by GitHub
commit 30896536f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 639 additions and 74 deletions

View File

@ -1237,7 +1237,27 @@ pub(crate) fn plain_text_summary(md: &str, link_names: &[RenderedLink]) -> Strin
pub(crate) struct MarkdownLink { pub(crate) struct MarkdownLink {
pub kind: LinkType, pub kind: LinkType,
pub link: String, pub link: String,
pub range: Range<usize>, pub range: MarkdownLinkRange,
}
#[derive(Clone, Debug)]
pub(crate) enum MarkdownLinkRange {
/// Normally, markdown link warnings point only at the destination.
Destination(Range<usize>),
/// In some cases, it's not possible to point at the destination.
/// Usually, this happens because backslashes `\\` are used.
/// When that happens, point at the whole link, and don't provide structured suggestions.
WholeLink(Range<usize>),
}
impl MarkdownLinkRange {
/// Extracts the inner range.
pub fn inner_range(&self) -> &Range<usize> {
match self {
MarkdownLinkRange::Destination(range) => range,
MarkdownLinkRange::WholeLink(range) => range,
}
}
} }
pub(crate) fn markdown_links<R>( pub(crate) fn markdown_links<R>(
@ -1257,9 +1277,9 @@ pub(crate) fn markdown_links<R>(
if md_start <= s_start && s_end <= md_end { if md_start <= s_start && s_end <= md_end {
let start = s_start.offset_from(md_start) as usize; let start = s_start.offset_from(md_start) as usize;
let end = s_end.offset_from(md_start) as usize; let end = s_end.offset_from(md_start) as usize;
start..end MarkdownLinkRange::Destination(start..end)
} else { } else {
fallback MarkdownLinkRange::WholeLink(fallback)
} }
}; };
@ -1267,6 +1287,7 @@ pub(crate) fn markdown_links<R>(
// For diagnostics, we want to underline the link's definition but `span` will point at // For diagnostics, we want to underline the link's definition but `span` will point at
// where the link is used. This is a problem for reference-style links, where the definition // where the link is used. This is a problem for reference-style links, where the definition
// is separate from the usage. // is separate from the usage.
match link { match link {
// `Borrowed` variant means the string (the link's destination) may come directly from // `Borrowed` variant means the string (the link's destination) may come directly from
// the markdown text and we can locate the original link destination. // the markdown text and we can locate the original link destination.
@ -1275,10 +1296,82 @@ pub(crate) fn markdown_links<R>(
CowStr::Borrowed(s) => locate(s, span), CowStr::Borrowed(s) => locate(s, span),
// For anything else, we can only use the provided range. // For anything else, we can only use the provided range.
CowStr::Boxed(_) | CowStr::Inlined(_) => span, CowStr::Boxed(_) | CowStr::Inlined(_) => MarkdownLinkRange::WholeLink(span),
} }
}; };
let span_for_offset_backward = |span: Range<usize>, open: u8, close: u8| {
let mut open_brace = !0;
let mut close_brace = !0;
for (i, b) in md.as_bytes()[span.clone()].iter().copied().enumerate().rev() {
let i = i + span.start;
if b == close {
close_brace = i;
break;
}
}
if close_brace < span.start || close_brace >= span.end {
return MarkdownLinkRange::WholeLink(span);
}
let mut nesting = 1;
for (i, b) in md.as_bytes()[span.start..close_brace].iter().copied().enumerate().rev() {
let i = i + span.start;
if b == close {
nesting += 1;
}
if b == open {
nesting -= 1;
}
if nesting == 0 {
open_brace = i;
break;
}
}
assert!(open_brace != close_brace);
if open_brace < span.start || open_brace >= span.end {
return MarkdownLinkRange::WholeLink(span);
}
// do not actually include braces in the span
let range = (open_brace + 1)..close_brace;
MarkdownLinkRange::Destination(range.clone())
};
let span_for_offset_forward = |span: Range<usize>, open: u8, close: u8| {
let mut open_brace = !0;
let mut close_brace = !0;
for (i, b) in md.as_bytes()[span.clone()].iter().copied().enumerate() {
let i = i + span.start;
if b == open {
open_brace = i;
break;
}
}
if open_brace < span.start || open_brace >= span.end {
return MarkdownLinkRange::WholeLink(span);
}
let mut nesting = 0;
for (i, b) in md.as_bytes()[open_brace..span.end].iter().copied().enumerate() {
let i = i + open_brace;
if b == close {
nesting -= 1;
}
if b == open {
nesting += 1;
}
if nesting == 0 {
close_brace = i;
break;
}
}
assert!(open_brace != close_brace);
if open_brace < span.start || open_brace >= span.end {
return MarkdownLinkRange::WholeLink(span);
}
// do not actually include braces in the span
let range = (open_brace + 1)..close_brace;
MarkdownLinkRange::Destination(range.clone())
};
Parser::new_with_broken_link_callback( Parser::new_with_broken_link_callback(
md, md,
main_body_opts(), main_body_opts(),
@ -1287,11 +1380,20 @@ pub(crate) fn markdown_links<R>(
.into_offset_iter() .into_offset_iter()
.filter_map(|(event, span)| match event { .filter_map(|(event, span)| match event {
Event::Start(Tag::Link(link_type, dest, _)) if may_be_doc_link(link_type) => { Event::Start(Tag::Link(link_type, dest, _)) if may_be_doc_link(link_type) => {
preprocess_link(MarkdownLink { let range = match link_type {
kind: link_type, // Link is pulled from the link itself.
range: span_for_link(&dest, span), LinkType::ReferenceUnknown | LinkType::ShortcutUnknown => {
link: dest.into_string(), span_for_offset_backward(span, b'[', b']')
}) }
LinkType::CollapsedUnknown => span_for_offset_forward(span, b'[', b']'),
LinkType::Inline => span_for_offset_backward(span, b'(', b')'),
// Link is pulled from elsewhere in the document.
LinkType::Reference | LinkType::Collapsed | LinkType::Shortcut => {
span_for_link(&dest, span)
}
LinkType::Autolink | LinkType::Email => unreachable!(),
};
preprocess_link(MarkdownLink { kind: link_type, range, link: dest.into_string() })
} }
_ => None, _ => None,
}) })

View File

@ -31,7 +31,7 @@
use crate::clean::{self, utils::find_nearest_parent_module}; use crate::clean::{self, utils::find_nearest_parent_module};
use crate::clean::{Crate, Item, ItemLink, PrimitiveType}; use crate::clean::{Crate, Item, ItemLink, PrimitiveType};
use crate::core::DocContext; use crate::core::DocContext;
use crate::html::markdown::{markdown_links, MarkdownLink}; use crate::html::markdown::{markdown_links, MarkdownLink, MarkdownLinkRange};
use crate::lint::{BROKEN_INTRA_DOC_LINKS, PRIVATE_INTRA_DOC_LINKS}; use crate::lint::{BROKEN_INTRA_DOC_LINKS, PRIVATE_INTRA_DOC_LINKS};
use crate::passes::Pass; use crate::passes::Pass;
use crate::visit::DocVisitor; use crate::visit::DocVisitor;
@ -248,7 +248,7 @@ struct DiagnosticInfo<'a> {
item: &'a Item, item: &'a Item,
dox: &'a str, dox: &'a str,
ori_link: &'a str, ori_link: &'a str,
link_range: Range<usize>, link_range: MarkdownLinkRange,
} }
struct LinkCollector<'a, 'tcx> { struct LinkCollector<'a, 'tcx> {
@ -833,7 +833,7 @@ fn visit_item(&mut self, item: &Item) {
enum PreprocessingError { enum PreprocessingError {
/// User error: `[std#x#y]` is not valid /// User error: `[std#x#y]` is not valid
MultipleAnchors, MultipleAnchors,
Disambiguator(Range<usize>, String), Disambiguator(MarkdownLinkRange, String),
MalformedGenerics(MalformedGenerics, String), MalformedGenerics(MalformedGenerics, String),
} }
@ -873,6 +873,7 @@ pub(crate) struct PreprocessedMarkdownLink(
/// `link_buffer` is needed for lifetime reasons; it will always be overwritten and the contents ignored. /// `link_buffer` is needed for lifetime reasons; it will always be overwritten and the contents ignored.
fn preprocess_link( fn preprocess_link(
ori_link: &MarkdownLink, ori_link: &MarkdownLink,
dox: &str,
) -> Option<Result<PreprocessingInfo, PreprocessingError>> { ) -> Option<Result<PreprocessingInfo, PreprocessingError>> {
// [] is mostly likely not supposed to be a link // [] is mostly likely not supposed to be a link
if ori_link.link.is_empty() { if ori_link.link.is_empty() {
@ -906,9 +907,15 @@ fn preprocess_link(
Err((err_msg, relative_range)) => { Err((err_msg, relative_range)) => {
// Only report error if we would not have ignored this link. See issue #83859. // Only report error if we would not have ignored this link. See issue #83859.
if !should_ignore_link_with_disambiguators(link) { if !should_ignore_link_with_disambiguators(link) {
let no_backticks_range = range_between_backticks(ori_link); let disambiguator_range = match range_between_backticks(&ori_link.range, dox) {
let disambiguator_range = (no_backticks_range.start + relative_range.start) MarkdownLinkRange::Destination(no_backticks_range) => {
..(no_backticks_range.start + relative_range.end); MarkdownLinkRange::Destination(
(no_backticks_range.start + relative_range.start)
..(no_backticks_range.start + relative_range.end),
)
}
mdlr @ MarkdownLinkRange::WholeLink(_) => mdlr,
};
return Some(Err(PreprocessingError::Disambiguator(disambiguator_range, err_msg))); return Some(Err(PreprocessingError::Disambiguator(disambiguator_range, err_msg)));
} else { } else {
return None; return None;
@ -947,7 +954,7 @@ fn preprocess_link(
fn preprocessed_markdown_links(s: &str) -> Vec<PreprocessedMarkdownLink> { fn preprocessed_markdown_links(s: &str) -> Vec<PreprocessedMarkdownLink> {
markdown_links(s, |link| { markdown_links(s, |link| {
preprocess_link(&link).map(|pp_link| PreprocessedMarkdownLink(pp_link, link)) preprocess_link(&link, s).map(|pp_link| PreprocessedMarkdownLink(pp_link, link))
}) })
} }
@ -1060,22 +1067,12 @@ fn resolve_link(
// valid omission. See https://github.com/rust-lang/rust/pull/80660#discussion_r551585677 // valid omission. See https://github.com/rust-lang/rust/pull/80660#discussion_r551585677
// for discussion on the matter. // for discussion on the matter.
let kind = self.cx.tcx.def_kind(id); let kind = self.cx.tcx.def_kind(id);
self.verify_disambiguator( self.verify_disambiguator(path_str, kind, id, disambiguator, item, &diag_info)?;
path_str,
ori_link,
kind,
id,
disambiguator,
item,
&diag_info,
)?;
} else { } else {
match disambiguator { match disambiguator {
Some(Disambiguator::Primitive | Disambiguator::Namespace(_)) | None => {} Some(Disambiguator::Primitive | Disambiguator::Namespace(_)) | None => {}
Some(other) => { Some(other) => {
self.report_disambiguator_mismatch( self.report_disambiguator_mismatch(path_str, other, res, &diag_info);
path_str, ori_link, other, res, &diag_info,
);
return None; return None;
} }
} }
@ -1096,7 +1093,6 @@ fn resolve_link(
}; };
self.verify_disambiguator( self.verify_disambiguator(
path_str, path_str,
ori_link,
kind_for_dis, kind_for_dis,
id_for_dis, id_for_dis,
disambiguator, disambiguator,
@ -1118,7 +1114,6 @@ fn resolve_link(
fn verify_disambiguator( fn verify_disambiguator(
&self, &self,
path_str: &str, path_str: &str,
ori_link: &MarkdownLink,
kind: DefKind, kind: DefKind,
id: DefId, id: DefId,
disambiguator: Option<Disambiguator>, disambiguator: Option<Disambiguator>,
@ -1142,7 +1137,7 @@ fn verify_disambiguator(
=> {} => {}
(actual, Some(Disambiguator::Kind(expected))) if actual == expected => {} (actual, Some(Disambiguator::Kind(expected))) if actual == expected => {}
(_, Some(specified @ Disambiguator::Kind(_) | specified @ Disambiguator::Primitive)) => { (_, Some(specified @ Disambiguator::Kind(_) | specified @ Disambiguator::Primitive)) => {
self.report_disambiguator_mismatch(path_str,ori_link,specified, Res::Def(kind, id),diag_info); self.report_disambiguator_mismatch(path_str, specified, Res::Def(kind, id), diag_info);
return None; return None;
} }
} }
@ -1164,14 +1159,13 @@ fn verify_disambiguator(
fn report_disambiguator_mismatch( fn report_disambiguator_mismatch(
&self, &self,
path_str: &str, path_str: &str,
ori_link: &MarkdownLink,
specified: Disambiguator, specified: Disambiguator,
resolved: Res, resolved: Res,
diag_info: &DiagnosticInfo<'_>, diag_info: &DiagnosticInfo<'_>,
) { ) {
// The resolved item did not match the disambiguator; give a better error than 'not found' // The resolved item did not match the disambiguator; give a better error than 'not found'
let msg = format!("incompatible link kind for `{}`", path_str); let msg = format!("incompatible link kind for `{}`", path_str);
let callback = |diag: &mut Diagnostic, sp: Option<rustc_span::Span>| { let callback = |diag: &mut Diagnostic, sp: Option<rustc_span::Span>, link_range| {
let note = format!( let note = format!(
"this link resolved to {} {}, which is not {} {}", "this link resolved to {} {}, which is not {} {}",
resolved.article(), resolved.article(),
@ -1184,14 +1178,24 @@ fn report_disambiguator_mismatch(
} else { } else {
diag.note(note); diag.note(note);
} }
suggest_disambiguator(resolved, diag, path_str, &ori_link.link, sp); suggest_disambiguator(resolved, diag, path_str, link_range, sp, diag_info);
}; };
report_diagnostic(self.cx.tcx, BROKEN_INTRA_DOC_LINKS, msg, diag_info, callback); report_diagnostic(self.cx.tcx, BROKEN_INTRA_DOC_LINKS, msg, diag_info, callback);
} }
fn report_rawptr_assoc_feature_gate(&self, dox: &str, ori_link: &Range<usize>, item: &Item) { fn report_rawptr_assoc_feature_gate(
let span = super::source_span_for_markdown_range(self.cx.tcx, dox, ori_link, &item.attrs) &self,
.unwrap_or_else(|| item.attr_span(self.cx.tcx)); dox: &str,
ori_link: &MarkdownLinkRange,
item: &Item,
) {
let span = super::source_span_for_markdown_range(
self.cx.tcx,
dox,
ori_link.inner_range(),
&item.attrs,
)
.unwrap_or_else(|| item.attr_span(self.cx.tcx));
rustc_session::parse::feature_err( rustc_session::parse::feature_err(
&self.cx.tcx.sess.parse_sess, &self.cx.tcx.sess.parse_sess,
sym::intra_doc_pointers, sym::intra_doc_pointers,
@ -1371,16 +1375,23 @@ fn resolve_with_disambiguator(
/// [`Foo`] /// [`Foo`]
/// ^^^ /// ^^^
/// ``` /// ```
fn range_between_backticks(ori_link: &MarkdownLink) -> Range<usize> { ///
let after_first_backtick_group = ori_link.link.bytes().position(|b| b != b'`').unwrap_or(0); /// This function does nothing if `ori_link.range` is a `MarkdownLinkRange::WholeLink`.
let before_second_backtick_group = ori_link fn range_between_backticks(ori_link_range: &MarkdownLinkRange, dox: &str) -> MarkdownLinkRange {
.link let range = match ori_link_range {
mdlr @ MarkdownLinkRange::WholeLink(_) => return mdlr.clone(),
MarkdownLinkRange::Destination(inner) => inner.clone(),
};
let ori_link_text = &dox[range.clone()];
let after_first_backtick_group = ori_link_text.bytes().position(|b| b != b'`').unwrap_or(0);
let before_second_backtick_group = ori_link_text
.bytes() .bytes()
.skip(after_first_backtick_group) .skip(after_first_backtick_group)
.position(|b| b == b'`') .position(|b| b == b'`')
.unwrap_or(ori_link.link.len()); .unwrap_or(ori_link_text.len());
(ori_link.range.start + after_first_backtick_group) MarkdownLinkRange::Destination(
..(ori_link.range.start + before_second_backtick_group) (range.start + after_first_backtick_group)..(range.start + before_second_backtick_group),
)
} }
/// Returns true if we should ignore `link` due to it being unlikely /// Returns true if we should ignore `link` due to it being unlikely
@ -1530,14 +1541,23 @@ fn as_help_span(
sp: rustc_span::Span, sp: rustc_span::Span,
) -> Vec<(rustc_span::Span, String)> { ) -> Vec<(rustc_span::Span, String)> {
let inner_sp = match ori_link.find('(') { let inner_sp = match ori_link.find('(') {
Some(index) if index != 0 && ori_link.as_bytes()[index - 1] == b'\\' => {
sp.with_hi(sp.lo() + BytePos((index - 1) as _))
}
Some(index) => sp.with_hi(sp.lo() + BytePos(index as _)), Some(index) => sp.with_hi(sp.lo() + BytePos(index as _)),
None => sp, None => sp,
}; };
let inner_sp = match ori_link.find('!') { let inner_sp = match ori_link.find('!') {
Some(index) if index != 0 && ori_link.as_bytes()[index - 1] == b'\\' => {
sp.with_hi(sp.lo() + BytePos((index - 1) as _))
}
Some(index) => inner_sp.with_hi(inner_sp.lo() + BytePos(index as _)), Some(index) => inner_sp.with_hi(inner_sp.lo() + BytePos(index as _)),
None => inner_sp, None => inner_sp,
}; };
let inner_sp = match ori_link.find('@') { let inner_sp = match ori_link.find('@') {
Some(index) if index != 0 && ori_link.as_bytes()[index - 1] == b'\\' => {
sp.with_hi(sp.lo() + BytePos((index - 1) as _))
}
Some(index) => inner_sp.with_lo(inner_sp.lo() + BytePos(index as u32 + 1)), Some(index) => inner_sp.with_lo(inner_sp.lo() + BytePos(index as u32 + 1)),
None => inner_sp, None => inner_sp,
}; };
@ -1584,7 +1604,7 @@ fn report_diagnostic(
lint: &'static Lint, lint: &'static Lint,
msg: impl Into<DiagnosticMessage> + Display, msg: impl Into<DiagnosticMessage> + Display,
DiagnosticInfo { item, ori_link: _, dox, link_range }: &DiagnosticInfo<'_>, DiagnosticInfo { item, ori_link: _, dox, link_range }: &DiagnosticInfo<'_>,
decorate: impl FnOnce(&mut Diagnostic, Option<rustc_span::Span>), decorate: impl FnOnce(&mut Diagnostic, Option<rustc_span::Span>, MarkdownLinkRange),
) { ) {
let Some(hir_id) = DocContext::as_local_hir_id(tcx, item.item_id) let Some(hir_id) = DocContext::as_local_hir_id(tcx, item.item_id)
else { else {
@ -1596,16 +1616,32 @@ fn report_diagnostic(
let sp = item.attr_span(tcx); let sp = item.attr_span(tcx);
tcx.struct_span_lint_hir(lint, hir_id, sp, msg, |lint| { tcx.struct_span_lint_hir(lint, hir_id, sp, msg, |lint| {
let span = let (span, link_range) = match link_range {
super::source_span_for_markdown_range(tcx, dox, link_range, &item.attrs).map(|sp| { MarkdownLinkRange::Destination(md_range) => {
if dox.as_bytes().get(link_range.start) == Some(&b'`') let mut md_range = md_range.clone();
&& dox.as_bytes().get(link_range.end - 1) == Some(&b'`') let sp = super::source_span_for_markdown_range(tcx, dox, &md_range, &item.attrs)
{ .map(|mut sp| {
sp.with_lo(sp.lo() + BytePos(1)).with_hi(sp.hi() - BytePos(1)) while dox.as_bytes().get(md_range.start) == Some(&b' ')
} else { || dox.as_bytes().get(md_range.start) == Some(&b'`')
sp {
} md_range.start += 1;
}); sp = sp.with_lo(sp.lo() + BytePos(1));
}
while dox.as_bytes().get(md_range.end - 1) == Some(&b' ')
|| dox.as_bytes().get(md_range.end - 1) == Some(&b'`')
{
md_range.end -= 1;
sp = sp.with_hi(sp.hi() - BytePos(1));
}
sp
});
(sp, MarkdownLinkRange::Destination(md_range))
}
MarkdownLinkRange::WholeLink(md_range) => (
super::source_span_for_markdown_range(tcx, dox, &md_range, &item.attrs),
link_range.clone(),
),
};
if let Some(sp) = span { if let Some(sp) = span {
lint.set_span(sp); lint.set_span(sp);
@ -1614,21 +1650,22 @@ fn report_diagnostic(
// ^ ~~~~ // ^ ~~~~
// | link_range // | link_range
// last_new_line_offset // last_new_line_offset
let last_new_line_offset = dox[..link_range.start].rfind('\n').map_or(0, |n| n + 1); let md_range = link_range.inner_range().clone();
let last_new_line_offset = dox[..md_range.start].rfind('\n').map_or(0, |n| n + 1);
let line = dox[last_new_line_offset..].lines().next().unwrap_or(""); let line = dox[last_new_line_offset..].lines().next().unwrap_or("");
// Print the line containing the `link_range` and manually mark it with '^'s. // Print the line containing the `md_range` and manually mark it with '^'s.
lint.note(format!( lint.note(format!(
"the link appears in this line:\n\n{line}\n\ "the link appears in this line:\n\n{line}\n\
{indicator: <before$}{indicator:^<found$}", {indicator: <before$}{indicator:^<found$}",
line = line, line = line,
indicator = "", indicator = "",
before = link_range.start - last_new_line_offset, before = md_range.start - last_new_line_offset,
found = link_range.len(), found = md_range.len(),
)); ));
} }
decorate(lint, span); decorate(lint, span, link_range);
lint lint
}); });
@ -1652,7 +1689,7 @@ fn resolution_failure(
BROKEN_INTRA_DOC_LINKS, BROKEN_INTRA_DOC_LINKS,
format!("unresolved link to `{}`", path_str), format!("unresolved link to `{}`", path_str),
&diag_info, &diag_info,
|diag, sp| { |diag, sp, link_range| {
let item = |res: Res| format!("the {} `{}`", res.descr(), res.name(tcx),); let item = |res: Res| format!("the {} `{}`", res.descr(), res.name(tcx),);
let assoc_item_not_allowed = |res: Res| { let assoc_item_not_allowed = |res: Res| {
let name = res.name(tcx); let name = res.name(tcx);
@ -1845,7 +1882,14 @@ fn split(path: &str) -> Option<(&str, &str)> {
let note = match failure { let note = match failure {
ResolutionFailure::NotResolved { .. } => unreachable!("handled above"), ResolutionFailure::NotResolved { .. } => unreachable!("handled above"),
ResolutionFailure::WrongNamespace { res, expected_ns } => { ResolutionFailure::WrongNamespace { res, expected_ns } => {
suggest_disambiguator(res, diag, path_str, diag_info.ori_link, sp); suggest_disambiguator(
res,
diag,
path_str,
link_range.clone(),
sp,
&diag_info,
);
format!( format!(
"this link resolves to {}, which is not in the {} namespace", "this link resolves to {}, which is not in the {} namespace",
@ -1882,7 +1926,7 @@ fn anchor_failure(
msg: String, msg: String,
anchor_idx: usize, anchor_idx: usize,
) { ) {
report_diagnostic(cx.tcx, BROKEN_INTRA_DOC_LINKS, msg, &diag_info, |diag, sp| { report_diagnostic(cx.tcx, BROKEN_INTRA_DOC_LINKS, msg, &diag_info, |diag, sp, _link_range| {
if let Some(mut sp) = sp { if let Some(mut sp) = sp {
if let Some((fragment_offset, _)) = if let Some((fragment_offset, _)) =
diag_info.ori_link.char_indices().filter(|(_, x)| *x == '#').nth(anchor_idx) diag_info.ori_link.char_indices().filter(|(_, x)| *x == '#').nth(anchor_idx)
@ -1898,11 +1942,11 @@ fn anchor_failure(
fn disambiguator_error( fn disambiguator_error(
cx: &DocContext<'_>, cx: &DocContext<'_>,
mut diag_info: DiagnosticInfo<'_>, mut diag_info: DiagnosticInfo<'_>,
disambiguator_range: Range<usize>, disambiguator_range: MarkdownLinkRange,
msg: impl Into<DiagnosticMessage> + Display, msg: impl Into<DiagnosticMessage> + Display,
) { ) {
diag_info.link_range = disambiguator_range; diag_info.link_range = disambiguator_range;
report_diagnostic(cx.tcx, BROKEN_INTRA_DOC_LINKS, msg, &diag_info, |diag, _sp| { report_diagnostic(cx.tcx, BROKEN_INTRA_DOC_LINKS, msg, &diag_info, |diag, _sp, _link_range| {
let msg = format!( let msg = format!(
"see {}/rustdoc/write-documentation/linking-to-items-by-name.html#namespaces-and-disambiguators for more info about disambiguators", "see {}/rustdoc/write-documentation/linking-to-items-by-name.html#namespaces-and-disambiguators for more info about disambiguators",
crate::DOC_RUST_LANG_ORG_CHANNEL crate::DOC_RUST_LANG_ORG_CHANNEL
@ -1922,7 +1966,7 @@ fn report_malformed_generics(
BROKEN_INTRA_DOC_LINKS, BROKEN_INTRA_DOC_LINKS,
format!("unresolved link to `{}`", path_str), format!("unresolved link to `{}`", path_str),
&diag_info, &diag_info,
|diag, sp| { |diag, sp, _link_range| {
let note = match err { let note = match err {
MalformedGenerics::UnbalancedAngleBrackets => "unbalanced angle brackets", MalformedGenerics::UnbalancedAngleBrackets => "unbalanced angle brackets",
MalformedGenerics::MissingType => "missing type for generic parameters", MalformedGenerics::MissingType => "missing type for generic parameters",
@ -1995,7 +2039,7 @@ fn ambiguity_error(
} }
} }
report_diagnostic(cx.tcx, BROKEN_INTRA_DOC_LINKS, msg, diag_info, |diag, sp| { report_diagnostic(cx.tcx, BROKEN_INTRA_DOC_LINKS, msg, diag_info, |diag, sp, link_range| {
if let Some(sp) = sp { if let Some(sp) = sp {
diag.span_label(sp, "ambiguous link"); diag.span_label(sp, "ambiguous link");
} else { } else {
@ -2003,7 +2047,7 @@ fn ambiguity_error(
} }
for res in kinds { for res in kinds {
suggest_disambiguator(res, diag, path_str, diag_info.ori_link, sp); suggest_disambiguator(res, diag, path_str, link_range.clone(), sp, diag_info);
} }
}); });
true true
@ -2015,13 +2059,19 @@ fn suggest_disambiguator(
res: Res, res: Res,
diag: &mut Diagnostic, diag: &mut Diagnostic,
path_str: &str, path_str: &str,
ori_link: &str, link_range: MarkdownLinkRange,
sp: Option<rustc_span::Span>, sp: Option<rustc_span::Span>,
diag_info: &DiagnosticInfo<'_>,
) { ) {
let suggestion = res.disambiguator_suggestion(); let suggestion = res.disambiguator_suggestion();
let help = format!("to link to the {}, {}", res.descr(), suggestion.descr()); let help = format!("to link to the {}, {}", res.descr(), suggestion.descr());
if let Some(sp) = sp { let ori_link = match link_range {
MarkdownLinkRange::Destination(range) => Some(&diag_info.dox[range]),
MarkdownLinkRange::WholeLink(_) => None,
};
if let (Some(sp), Some(ori_link)) = (sp, ori_link) {
let mut spans = suggestion.as_help_span(path_str, ori_link, sp); let mut spans = suggestion.as_help_span(path_str, ori_link, sp);
if spans.len() > 1 { if spans.len() > 1 {
diag.multipart_suggestion(help, spans, Applicability::MaybeIncorrect); diag.multipart_suggestion(help, spans, Applicability::MaybeIncorrect);
@ -2047,7 +2097,7 @@ fn privacy_error(cx: &DocContext<'_>, diag_info: &DiagnosticInfo<'_>, path_str:
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.tcx, PRIVATE_INTRA_DOC_LINKS, msg, diag_info, |diag, sp| { report_diagnostic(cx.tcx, PRIVATE_INTRA_DOC_LINKS, msg, diag_info, |diag, sp, _link_range| {
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

@ -36,7 +36,7 @@ LL | //! [`Clone ()`].
help: to link to the trait, prefix with `trait@` help: to link to the trait, prefix with `trait@`
| |
LL - //! [`Clone ()`]. LL - //! [`Clone ()`].
LL + //! [`trait@Clone (`]. LL + //! [`trait@Clone `].
| |
error: incompatible link kind for `Clone` error: incompatible link kind for `Clone`
@ -47,8 +47,9 @@ LL | //! [`Clone !`].
| |
help: to link to the derive macro, prefix with `derive@` help: to link to the derive macro, prefix with `derive@`
| |
LL | //! [`derive@Clone !`]. LL - //! [`Clone !`].
| +++++++ LL + //! [`derive@Clone `].
|
error: aborting due to 4 previous errors error: aborting due to 4 previous errors

View File

@ -0,0 +1,140 @@
// Many examples are from
// https://github.com/rust-lang/rust/issues/110111#issuecomment-1517800781
#![deny(rustdoc::broken_intra_doc_links)]
//! This test case is closely linked to [raphlinus/pulldown-cmark#441], getting offsets of
//! link components. In particular, pulldown-cmark doesn't provide the offsets of the contents
//! of a link.
//!
//! To work around this, rustdoc parses parts of a link definition itself. This is basically a
//! test suite for that link syntax parser.
//!
//! [raphlinus/pulldown-cmark#441]: https://github.com/raphlinus/pulldown-cmark/issues/441
use std::clone::Clone;
// Basic version //
/// [`struct@Clone`] //~ERROR link
pub struct LinkToCloneWithBackquotes;
/// [```struct@Clone```] //~ERROR link
pub struct LinkToCloneWithMultipleBackquotes;
/// [ ` struct@Clone ` ] //~ERROR link
pub struct LinkToCloneWithSpacesAndBackquotes;
/// [ `Clone ()` ] //~ERROR link
pub struct LinkToCloneWithSpacesBackquotesAndParens;
/// [`Clone ()` ] //~ERROR link
pub struct LinkToCloneWithSpacesEndBackquotesAndParens;
/// [ `Clone ()`] //~ERROR link
pub struct LinkToCloneWithSpacesStartBackquotesAndParens;
/// [```Clone ()```] //~ERROR link
pub struct LinkToCloneWithMultipleBackquotesAndParens;
/// [```Clone \(\)```] // not URL-shaped enough
pub struct LinkToCloneWithMultipleBackquotesAndEscapedParens;
/// [ ``` Clone () ``` ] //~ERROR link
pub struct LinkToCloneWithSpacesMultipleBackquotesAndParens;
/// [ x \] ] // not URL-shaped enough
pub struct LinkWithEscapedCloseBrace;
/// [ x \[ ] // not URL-shaped enough
pub struct LinkWithEscapedOpenBrace;
/// [ x \( ] // not URL-shaped enough
pub struct LinkWithEscapedCloseParen;
/// [ x \) ] // not URL-shaped enough
pub struct LinkWithEscapedOpenParen;
/// [ Clone \(\) ] // not URL-shaped enough
pub struct LinkWithEscapedParens;
// [][] version //
/// [x][ struct@Clone] //~ERROR link
pub struct XLinkToCloneWithStartSpace;
/// [x][struct@Clone ] //~ERROR link
pub struct XLinkToCloneWithEndSpace;
/// [x][Clone\(\)] not URL-shaped enough
pub struct XLinkToCloneWithEscapedParens;
/// [x][`Clone`] not URL-shaped enough
pub struct XLinkToCloneWithBackquotes;
/// [x][Clone()] //~ERROR link
pub struct XLinkToCloneWithUnescapedParens;
/// [x][Clone ()] //~ERROR link
pub struct XLinkToCloneWithUnescapedParensAndDoubleSpace;
/// [x][Clone [] //~ERROR unresolved link to `x`
pub struct XLinkToCloneWithUnmatchedOpenParenAndDoubleSpace;
/// [x][Clone \[] // not URL-shaped enough
pub struct XLinkToCloneWithUnmatchedEscapedOpenParenAndDoubleSpace;
/// [x][Clone \]] // not URL-shaped enough
pub struct XLinkToCloneWithUnmatchedEscapedCloseParenAndDoubleSpace;
// []() version //
/// [w]( struct@Clone) //~ERROR link
pub struct WLinkToCloneWithStartSpace;
/// [w](struct@Clone ) //~ERROR link
pub struct WLinkToCloneWithEndSpace;
/// [w](Clone\(\)) //~ERROR link
pub struct WLinkToCloneWithEscapedParens;
/// [w](`Clone`) not URL-shaped enough
pub struct WLinkToCloneWithBackquotes;
/// [w](Clone()) //~ERROR link
pub struct WLinkToCloneWithUnescapedParens;
/// [w](Clone ()) not URL-shaped enough
pub struct WLinkToCloneWithUnescapedParensAndDoubleSpace;
/// [w](Clone () //~ERROR unresolved link to `w`
pub struct WLinkToCloneWithUnmatchedOpenParenAndDoubleSpace;
/// [w](Clone \() //~ERROR unresolved link to `w`
pub struct WLinkToCloneWithUnmatchedEscapedOpenParenAndDoubleSpace;
/// [w](Clone \)) //~ERROR unresolved link to `w`
pub struct WLinkToCloneWithUnmatchedEscapedCloseParenAndDoubleSpace;
// References
/// The [cln][] link here is going to be unresolved, because `Clone()` gets rejected //~ERROR link
/// in Markdown for not being URL-shaped enough.
///
/// [cln]: Clone() //~ERROR link
pub struct LinkToCloneWithParensInReference;
/// The [cln][] link here is going to be unresolved, because `struct@Clone` gets //~ERROR link
/// rejected in Markdown for not being URL-shaped enough.
///
/// [cln]: struct@Clone //~ERROR link
pub struct LinkToCloneWithWrongPrefix;
/// The [cln][] link here will produce a plain text suggestion //~ERROR link
///
/// [cln]: Clone\(\)
pub struct LinkToCloneWithEscapedParensInReference;
/// The [cln][] link here will produce a plain text suggestion //~ERROR link
///
/// [cln]: struct\@Clone
pub struct LinkToCloneWithEscapedAtsInReference;

View File

@ -0,0 +1,272 @@
error: incompatible link kind for `Clone`
--> $DIR/weird-syntax.rs:18:7
|
LL | /// [`struct@Clone`]
| ^^^^^^^^^^^^ this link resolved to a trait, which is not a struct
|
note: the lint level is defined here
--> $DIR/weird-syntax.rs:3:9
|
LL | #![deny(rustdoc::broken_intra_doc_links)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: to link to the trait, prefix with `trait@`
|
LL | /// [`trait@Clone`]
| ~~~~~~
error: incompatible link kind for `Clone`
--> $DIR/weird-syntax.rs:21:9
|
LL | /// [```struct@Clone```]
| ^^^^^^^^^^^^ this link resolved to a trait, which is not a struct
|
help: to link to the trait, prefix with `trait@`
|
LL | /// [```trait@Clone```]
| ~~~~~~
error: incompatible link kind for `Clone`
--> $DIR/weird-syntax.rs:24:11
|
LL | /// [ ` struct@Clone ` ]
| ^^^^^^^^^^^^ this link resolved to a trait, which is not a struct
|
help: to link to the trait, prefix with `trait@`
|
LL | /// [ ` trait@Clone ` ]
| ~~~~~~
error: unresolved link to `Clone`
--> $DIR/weird-syntax.rs:27:9
|
LL | /// [ `Clone ()` ]
| ^^^^^^^^ this link resolves to the trait `Clone`, which is not in the value namespace
|
help: to link to the trait, prefix with `trait@`
|
LL - /// [ `Clone ()` ]
LL + /// [ `trait@Clone ` ]
|
error: unresolved link to `Clone`
--> $DIR/weird-syntax.rs:30:7
|
LL | /// [`Clone ()` ]
| ^^^^^^^^ this link resolves to the trait `Clone`, which is not in the value namespace
|
help: to link to the trait, prefix with `trait@`
|
LL - /// [`Clone ()` ]
LL + /// [`trait@Clone ` ]
|
error: unresolved link to `Clone`
--> $DIR/weird-syntax.rs:33:9
|
LL | /// [ `Clone ()`]
| ^^^^^^^^ this link resolves to the trait `Clone`, which is not in the value namespace
|
help: to link to the trait, prefix with `trait@`
|
LL - /// [ `Clone ()`]
LL + /// [ `trait@Clone `]
|
error: unresolved link to `Clone`
--> $DIR/weird-syntax.rs:36:9
|
LL | /// [```Clone ()```]
| ^^^^^^^^ this link resolves to the trait `Clone`, which is not in the value namespace
|
help: to link to the trait, prefix with `trait@`
|
LL - /// [```Clone ()```]
LL + /// [```trait@Clone ```]
|
error: unresolved link to `Clone`
--> $DIR/weird-syntax.rs:42:13
|
LL | /// [ ``` Clone () ``` ]
| ^^^^^^^^ this link resolves to the trait `Clone`, which is not in the value namespace
|
help: to link to the trait, prefix with `trait@`
|
LL - /// [ ``` Clone () ``` ]
LL + /// [ ``` trait@Clone ``` ]
|
error: incompatible link kind for `Clone`
--> $DIR/weird-syntax.rs:62:10
|
LL | /// [x][ struct@Clone]
| ^^^^^^^^^^^^ this link resolved to a trait, which is not a struct
|
help: to link to the trait, prefix with `trait@`
|
LL | /// [x][ trait@Clone]
| ~~~~~~
error: incompatible link kind for `Clone`
--> $DIR/weird-syntax.rs:65:9
|
LL | /// [x][struct@Clone ]
| ^^^^^^^^^^^^ this link resolved to a trait, which is not a struct
|
help: to link to the trait, prefix with `trait@`
|
LL | /// [x][trait@Clone ]
| ~~~~~~
error: unresolved link to `Clone`
--> $DIR/weird-syntax.rs:74:9
|
LL | /// [x][Clone()]
| ^^^^^^^ this link resolves to the trait `Clone`, which is not in the value namespace
|
help: to link to the trait, prefix with `trait@`
|
LL - /// [x][Clone()]
LL + /// [x][trait@Clone]
|
error: unresolved link to `Clone`
--> $DIR/weird-syntax.rs:77:9
|
LL | /// [x][Clone ()]
| ^^^^^^^^^ this link resolves to the trait `Clone`, which is not in the value namespace
|
help: to link to the trait, prefix with `trait@`
|
LL - /// [x][Clone ()]
LL + /// [x][trait@Clone ]
|
error: unresolved link to `x`
--> $DIR/weird-syntax.rs:80:6
|
LL | /// [x][Clone []
| ^ no item named `x` in scope
|
= help: to escape `[` and `]` characters, add '\' before them like `\[` or `\]`
error: incompatible link kind for `Clone`
--> $DIR/weird-syntax.rs:91:10
|
LL | /// [w]( struct@Clone)
| ^^^^^^^^^^^^ this link resolved to a trait, which is not a struct
|
help: to link to the trait, prefix with `trait@`
|
LL | /// [w]( trait@Clone)
| ~~~~~~
error: incompatible link kind for `Clone`
--> $DIR/weird-syntax.rs:94:9
|
LL | /// [w](struct@Clone )
| ^^^^^^^^^^^^ this link resolved to a trait, which is not a struct
|
help: to link to the trait, prefix with `trait@`
|
LL | /// [w](trait@Clone )
| ~~~~~~
error: unresolved link to `Clone`
--> $DIR/weird-syntax.rs:97:9
|
LL | /// [w](Clone\(\))
| ^^^^^^^^^ this link resolves to the trait `Clone`, which is not in the value namespace
|
help: to link to the trait, prefix with `trait@`
|
LL - /// [w](Clone\(\))
LL + /// [w](trait@Clone)
|
error: unresolved link to `Clone`
--> $DIR/weird-syntax.rs:103:9
|
LL | /// [w](Clone())
| ^^^^^^^ this link resolves to the trait `Clone`, which is not in the value namespace
|
help: to link to the trait, prefix with `trait@`
|
LL - /// [w](Clone())
LL + /// [w](trait@Clone)
|
error: unresolved link to `w`
--> $DIR/weird-syntax.rs:109:6
|
LL | /// [w](Clone ()
| ^ no item named `w` in scope
|
= help: to escape `[` and `]` characters, add '\' before them like `\[` or `\]`
error: unresolved link to `w`
--> $DIR/weird-syntax.rs:112:6
|
LL | /// [w](Clone \()
| ^ no item named `w` in scope
|
= help: to escape `[` and `]` characters, add '\' before them like `\[` or `\]`
error: unresolved link to `w`
--> $DIR/weird-syntax.rs:115:6
|
LL | /// [w](Clone \))
| ^ no item named `w` in scope
|
= help: to escape `[` and `]` characters, add '\' before them like `\[` or `\]`
error: unresolved link to `cln`
--> $DIR/weird-syntax.rs:120:10
|
LL | /// The [cln][] link here is going to be unresolved, because `Clone()` gets rejected
| ^^^ no item named `cln` in scope
|
= help: to escape `[` and `]` characters, add '\' before them like `\[` or `\]`
error: unresolved link to `cln`
--> $DIR/weird-syntax.rs:123:6
|
LL | /// [cln]: Clone()
| ^^^ no item named `cln` in scope
|
= help: to escape `[` and `]` characters, add '\' before them like `\[` or `\]`
error: unresolved link to `cln`
--> $DIR/weird-syntax.rs:126:10
|
LL | /// The [cln][] link here is going to be unresolved, because `struct@Clone` gets
| ^^^ no item named `cln` in scope
|
= help: to escape `[` and `]` characters, add '\' before them like `\[` or `\]`
error: unresolved link to `cln`
--> $DIR/weird-syntax.rs:129:6
|
LL | /// [cln]: struct@Clone
| ^^^ no item named `cln` in scope
|
= help: to escape `[` and `]` characters, add '\' before them like `\[` or `\]`
error: unresolved link to `Clone`
--> $DIR/weird-syntax.rs:132:9
|
LL | /// The [cln][] link here will produce a plain text suggestion
| ^^^^^ this link resolves to the trait `Clone`, which is not in the value namespace
|
= help: to link to the trait, prefix with `trait@`: trait@Clone
error: incompatible link kind for `Clone`
--> $DIR/weird-syntax.rs:137:9
|
LL | /// The [cln][] link here will produce a plain text suggestion
| ^^^^^ this link resolved to a trait, which is not a struct
|
= help: to link to the trait, prefix with `trait@`: trait@Clone
error: aborting due to 26 previous errors