diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs
index 09e7ed293d4..9bb20022cfd 100644
--- a/src/librustdoc/html/markdown.rs
+++ b/src/librustdoc/html/markdown.rs
@@ -1237,7 +1237,27 @@ pub(crate) fn plain_text_summary(md: &str, link_names: &[RenderedLink]) -> Strin
pub(crate) struct MarkdownLink {
pub kind: LinkType,
pub link: String,
- pub range: Range,
+ pub range: MarkdownLinkRange,
+}
+
+#[derive(Clone, Debug)]
+pub(crate) enum MarkdownLinkRange {
+ /// Normally, markdown link warnings point only at the destination.
+ Destination(Range),
+ /// 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),
+}
+
+impl MarkdownLinkRange {
+ /// Extracts the inner range.
+ pub fn inner_range(&self) -> &Range {
+ match self {
+ MarkdownLinkRange::Destination(range) => range,
+ MarkdownLinkRange::WholeLink(range) => range,
+ }
+ }
}
pub(crate) fn markdown_links(
@@ -1257,9 +1277,9 @@ pub(crate) fn markdown_links(
if md_start <= s_start && s_end <= md_end {
let start = s_start.offset_from(md_start) as usize;
let end = s_end.offset_from(md_start) as usize;
- start..end
+ MarkdownLinkRange::Destination(start..end)
} else {
- fallback
+ MarkdownLinkRange::WholeLink(fallback)
}
};
@@ -1267,6 +1287,7 @@ pub(crate) fn markdown_links(
// 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
// is separate from the usage.
+
match link {
// `Borrowed` variant means the string (the link's destination) may come directly from
// the markdown text and we can locate the original link destination.
@@ -1275,10 +1296,82 @@ pub(crate) fn markdown_links(
CowStr::Borrowed(s) => locate(s, span),
// 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, 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, 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(
md,
main_body_opts(),
@@ -1287,11 +1380,20 @@ pub(crate) fn markdown_links(
.into_offset_iter()
.filter_map(|(event, span)| match event {
Event::Start(Tag::Link(link_type, dest, _)) if may_be_doc_link(link_type) => {
- preprocess_link(MarkdownLink {
- kind: link_type,
- range: span_for_link(&dest, span),
- link: dest.into_string(),
- })
+ let range = match link_type {
+ // Link is pulled from the link itself.
+ LinkType::ReferenceUnknown | LinkType::ShortcutUnknown => {
+ 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,
})
diff --git a/src/librustdoc/passes/collect_intra_doc_links.rs b/src/librustdoc/passes/collect_intra_doc_links.rs
index 9e6894a77df..417bdd58ad4 100644
--- a/src/librustdoc/passes/collect_intra_doc_links.rs
+++ b/src/librustdoc/passes/collect_intra_doc_links.rs
@@ -31,7 +31,7 @@ use std::ops::Range;
use crate::clean::{self, utils::find_nearest_parent_module};
use crate::clean::{Crate, Item, ItemLink, PrimitiveType};
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::passes::Pass;
use crate::visit::DocVisitor;
@@ -248,7 +248,7 @@ struct DiagnosticInfo<'a> {
item: &'a Item,
dox: &'a str,
ori_link: &'a str,
- link_range: Range,
+ link_range: MarkdownLinkRange,
}
struct LinkCollector<'a, 'tcx> {
@@ -833,7 +833,7 @@ impl<'a, 'tcx> DocVisitor for LinkCollector<'a, 'tcx> {
enum PreprocessingError {
/// User error: `[std#x#y]` is not valid
MultipleAnchors,
- Disambiguator(Range, String),
+ Disambiguator(MarkdownLinkRange, 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.
fn preprocess_link(
ori_link: &MarkdownLink,
+ dox: &str,
) -> Option> {
// [] is mostly likely not supposed to be a link
if ori_link.link.is_empty() {
@@ -906,9 +907,15 @@ fn preprocess_link(
Err((err_msg, relative_range)) => {
// Only report error if we would not have ignored this link. See issue #83859.
if !should_ignore_link_with_disambiguators(link) {
- let no_backticks_range = range_between_backticks(ori_link);
- let disambiguator_range = (no_backticks_range.start + relative_range.start)
- ..(no_backticks_range.start + relative_range.end);
+ let disambiguator_range = match range_between_backticks(&ori_link.range, dox) {
+ MarkdownLinkRange::Destination(no_backticks_range) => {
+ 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)));
} else {
return None;
@@ -947,7 +954,7 @@ fn preprocess_link(
fn preprocessed_markdown_links(s: &str) -> Vec {
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 @@ impl LinkCollector<'_, '_> {
// valid omission. See https://github.com/rust-lang/rust/pull/80660#discussion_r551585677
// for discussion on the matter.
let kind = self.cx.tcx.def_kind(id);
- self.verify_disambiguator(
- path_str,
- ori_link,
- kind,
- id,
- disambiguator,
- item,
- &diag_info,
- )?;
+ self.verify_disambiguator(path_str, kind, id, disambiguator, item, &diag_info)?;
} else {
match disambiguator {
Some(Disambiguator::Primitive | Disambiguator::Namespace(_)) | None => {}
Some(other) => {
- self.report_disambiguator_mismatch(
- path_str, ori_link, other, res, &diag_info,
- );
+ self.report_disambiguator_mismatch(path_str, other, res, &diag_info);
return None;
}
}
@@ -1096,7 +1093,6 @@ impl LinkCollector<'_, '_> {
};
self.verify_disambiguator(
path_str,
- ori_link,
kind_for_dis,
id_for_dis,
disambiguator,
@@ -1118,7 +1114,6 @@ impl LinkCollector<'_, '_> {
fn verify_disambiguator(
&self,
path_str: &str,
- ori_link: &MarkdownLink,
kind: DefKind,
id: DefId,
disambiguator: Option,
@@ -1142,7 +1137,7 @@ impl LinkCollector<'_, '_> {
=> {}
(actual, Some(Disambiguator::Kind(expected))) if actual == expected => {}
(_, 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;
}
}
@@ -1164,14 +1159,13 @@ impl LinkCollector<'_, '_> {
fn report_disambiguator_mismatch(
&self,
path_str: &str,
- ori_link: &MarkdownLink,
specified: Disambiguator,
resolved: Res,
diag_info: &DiagnosticInfo<'_>,
) {
// 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 callback = |diag: &mut Diagnostic, sp: Option| {
+ let callback = |diag: &mut Diagnostic, sp: Option, link_range| {
let note = format!(
"this link resolved to {} {}, which is not {} {}",
resolved.article(),
@@ -1184,14 +1178,24 @@ impl LinkCollector<'_, '_> {
} else {
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);
}
- fn report_rawptr_assoc_feature_gate(&self, dox: &str, ori_link: &Range, item: &Item) {
- let span = super::source_span_for_markdown_range(self.cx.tcx, dox, ori_link, &item.attrs)
- .unwrap_or_else(|| item.attr_span(self.cx.tcx));
+ fn report_rawptr_assoc_feature_gate(
+ &self,
+ 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(
&self.cx.tcx.sess.parse_sess,
sym::intra_doc_pointers,
@@ -1371,16 +1375,23 @@ impl LinkCollector<'_, '_> {
/// [`Foo`]
/// ^^^
/// ```
-fn range_between_backticks(ori_link: &MarkdownLink) -> Range {
- let after_first_backtick_group = ori_link.link.bytes().position(|b| b != b'`').unwrap_or(0);
- let before_second_backtick_group = ori_link
- .link
+///
+/// This function does nothing if `ori_link.range` is a `MarkdownLinkRange::WholeLink`.
+fn range_between_backticks(ori_link_range: &MarkdownLinkRange, dox: &str) -> MarkdownLinkRange {
+ 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()
.skip(after_first_backtick_group)
.position(|b| b == b'`')
- .unwrap_or(ori_link.link.len());
- (ori_link.range.start + after_first_backtick_group)
- ..(ori_link.range.start + before_second_backtick_group)
+ .unwrap_or(ori_link_text.len());
+ MarkdownLinkRange::Destination(
+ (range.start + after_first_backtick_group)..(range.start + before_second_backtick_group),
+ )
}
/// Returns true if we should ignore `link` due to it being unlikely
@@ -1530,14 +1541,23 @@ impl Suggestion {
sp: rustc_span::Span,
) -> Vec<(rustc_span::Span, String)> {
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 _)),
None => sp,
};
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 _)),
None => inner_sp,
};
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)),
None => inner_sp,
};
@@ -1584,7 +1604,7 @@ fn report_diagnostic(
lint: &'static Lint,
msg: impl Into + Display,
DiagnosticInfo { item, ori_link: _, dox, link_range }: &DiagnosticInfo<'_>,
- decorate: impl FnOnce(&mut Diagnostic, Option),
+ decorate: impl FnOnce(&mut Diagnostic, Option, MarkdownLinkRange),
) {
let Some(hir_id) = DocContext::as_local_hir_id(tcx, item.item_id)
else {
@@ -1596,16 +1616,32 @@ fn report_diagnostic(
let sp = item.attr_span(tcx);
tcx.struct_span_lint_hir(lint, hir_id, sp, msg, |lint| {
- let span =
- super::source_span_for_markdown_range(tcx, dox, link_range, &item.attrs).map(|sp| {
- if dox.as_bytes().get(link_range.start) == Some(&b'`')
- && dox.as_bytes().get(link_range.end - 1) == Some(&b'`')
- {
- sp.with_lo(sp.lo() + BytePos(1)).with_hi(sp.hi() - BytePos(1))
- } else {
- sp
- }
- });
+ let (span, link_range) = match link_range {
+ MarkdownLinkRange::Destination(md_range) => {
+ let mut md_range = md_range.clone();
+ let sp = super::source_span_for_markdown_range(tcx, dox, &md_range, &item.attrs)
+ .map(|mut sp| {
+ while dox.as_bytes().get(md_range.start) == Some(&b' ')
+ || dox.as_bytes().get(md_range.start) == Some(&b'`')
+ {
+ 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 {
lint.set_span(sp);
@@ -1614,21 +1650,22 @@ fn report_diagnostic(
// ^ ~~~~
// | link_range
// 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("");
- // 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!(
"the link appears in this line:\n\n{line}\n\
{indicator: unreachable!("handled above"),
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!(
"this link resolves to {}, which is not in the {} namespace",
@@ -1882,7 +1926,7 @@ fn anchor_failure(
msg: String,
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((fragment_offset, _)) =
diag_info.ori_link.char_indices().filter(|(_, x)| *x == '#').nth(anchor_idx)
@@ -1898,11 +1942,11 @@ fn anchor_failure(
fn disambiguator_error(
cx: &DocContext<'_>,
mut diag_info: DiagnosticInfo<'_>,
- disambiguator_range: Range,
+ disambiguator_range: MarkdownLinkRange,
msg: impl Into + Display,
) {
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!(
"see {}/rustdoc/write-documentation/linking-to-items-by-name.html#namespaces-and-disambiguators for more info about disambiguators",
crate::DOC_RUST_LANG_ORG_CHANNEL
@@ -1922,7 +1966,7 @@ fn report_malformed_generics(
BROKEN_INTRA_DOC_LINKS,
format!("unresolved link to `{}`", path_str),
&diag_info,
- |diag, sp| {
+ |diag, sp, _link_range| {
let note = match err {
MalformedGenerics::UnbalancedAngleBrackets => "unbalanced angle brackets",
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 {
diag.span_label(sp, "ambiguous link");
} else {
@@ -2003,7 +2047,7 @@ fn ambiguity_error(
}
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
@@ -2015,13 +2059,19 @@ fn suggest_disambiguator(
res: Res,
diag: &mut Diagnostic,
path_str: &str,
- ori_link: &str,
+ link_range: MarkdownLinkRange,
sp: Option,
+ diag_info: &DiagnosticInfo<'_>,
) {
let suggestion = res.disambiguator_suggestion();
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);
if spans.len() > 1 {
diag.multipart_suggestion(help, spans, Applicability::MaybeIncorrect);
@@ -2047,7 +2097,7 @@ fn privacy_error(cx: &DocContext<'_>, diag_info: &DiagnosticInfo<'_>, path_str:
let msg =
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 {
diag.span_label(sp, "this item is private");
}
diff --git a/tests/rustdoc-ui/intra-doc/issue-110495-suffix-with-space.stderr b/tests/rustdoc-ui/intra-doc/issue-110495-suffix-with-space.stderr
index 8669b0c2086..6c834fd0a1b 100644
--- a/tests/rustdoc-ui/intra-doc/issue-110495-suffix-with-space.stderr
+++ b/tests/rustdoc-ui/intra-doc/issue-110495-suffix-with-space.stderr
@@ -36,7 +36,7 @@ LL | //! [`Clone ()`].
help: to link to the trait, prefix with `trait@`
|
LL - //! [`Clone ()`].
-LL + //! [`trait@Clone (`].
+LL + //! [`trait@Clone `].
|
error: incompatible link kind for `Clone`
@@ -47,8 +47,9 @@ LL | //! [`Clone !`].
|
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
diff --git a/tests/rustdoc-ui/intra-doc/weird-syntax.rs b/tests/rustdoc-ui/intra-doc/weird-syntax.rs
new file mode 100644
index 00000000000..ca18842fb21
--- /dev/null
+++ b/tests/rustdoc-ui/intra-doc/weird-syntax.rs
@@ -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;
diff --git a/tests/rustdoc-ui/intra-doc/weird-syntax.stderr b/tests/rustdoc-ui/intra-doc/weird-syntax.stderr
new file mode 100644
index 00000000000..f50feb57fcc
--- /dev/null
+++ b/tests/rustdoc-ui/intra-doc/weird-syntax.stderr
@@ -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
+