Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 60 additions & 3 deletions src/librustdoc/html/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,8 +462,31 @@ impl<'a> LinkReplacerInner<'a> {
}
// If this is a link, but not a shortcut link,
// replace the URL, since the broken_link_callback was not called.
Event::Start(Tag::Link { dest_url, title, .. }) => {
if let Some(link) =
Event::Start(Tag::Link { dest_url, title, link_type, id, .. }) => {
if matches!(
link_type,
LinkType::Reference | LinkType::Collapsed | LinkType::Shortcut
) && let Some(link) = self.links.iter().find(|&link| *link.original_text == **id)
{
// Markdown link references are case insensitive, so if you have:
//
// ```
// - [`Foo`]
// - [`foo`]
//
// [`foo`]: Bar::foo
// ```
//
// It'll consider that both `Foo` and `foo` are defined by "[`foo`]: Bar::foo".
// So we start by checking if an intra-doc link has the same ID, and if so,
// use it instead of the `original_text` which would be `Bar::foo` here.
assert!(self.shortcut_link.is_none(), "shortcut links cannot be nested");
self.shortcut_link = Some(link);
if title.is_empty() && !link.tooltip.is_empty() {
*title = CowStr::Borrowed(link.tooltip.as_ref());
}
*dest_url = CowStr::Borrowed(link.href.as_ref());
} else if let Some(link) =
self.links.iter().find(|&link| *link.original_text == **dest_url)
{
*dest_url = CowStr::Borrowed(link.href.as_ref());
Expand Down Expand Up @@ -1767,6 +1790,9 @@ pub(crate) struct MarkdownLink {
pub kind: LinkType,
pub link: String,
pub range: MarkdownLinkRange,
// If this is a link where a case-insensitive link reference matches, we don't want to emit
// warnings in case the intra-doc link doesn't find anything.
pub emit_error: bool,
}

#[derive(Clone, Debug)]
Expand Down Expand Up @@ -1961,6 +1987,7 @@ pub(crate) fn markdown_links<'md, R>(
Event::Start(Tag::Link { link_type, dest_url, id, .. })
if may_be_doc_link(link_type) =>
{
let mut alternative_span = None;
let range = match link_type {
// Link is pulled from the link itself.
LinkType::ReferenceUnknown | LinkType::ShortcutUnknown => {
Expand All @@ -1974,16 +2001,45 @@ pub(crate) fn markdown_links<'md, R>(
*is_used = true;
span_for_refdef(&CowStr::from(&dest_url[..]), span.clone())
} else {
// Markdown link references are case insensitive, so if you have:
//
// ```
// - [`Foo`]
// - [`foo`]
//
// [`foo`]: Bar::foo
// ```
//
// It'll consider that both `Foo` and `foo` are defined by
// "[`foo`]: Bar::foo". So if no key in `refdefs` has the exact same
// name, we consider that it might be a case-sensitivity conflict
// between a defined link and this one. If so, we don't want to use
// its `dest_url` (which is `Bar::foo` in the case above) and instead
// use its original link "id" (which is `Foo`).
alternative_span =
Some(span_for_offset_backward(span.clone(), b'[', b']'));
span_for_link(&dest_url, span)
}
}
LinkType::Autolink | LinkType::Email => unreachable!(),
};

if let Some(range) = alternative_span
&& let Some(link) = preprocess_link(MarkdownLink {
kind: link_type,
link: id.into_string(),
range,
// This is `false` because in case this is supposed to match the link def
// with case insensitivity, we don't want to emit warning.
emit_error: false,
})
{
links.push(link);
}
if let Some(link) = preprocess_link(MarkdownLink {
kind: link_type,
link: dest_url.into_string(),
range,
emit_error: true,
}) {
links.push(link);
}
Expand All @@ -1998,6 +2054,7 @@ pub(crate) fn markdown_links<'md, R>(
kind: LinkType::Reference,
range: span_for_refdef(&CowStr::from(&dest_url[..]), span),
link: dest_url,
emit_error: true,
})
{
links.push(link);
Expand Down
78 changes: 49 additions & 29 deletions src/librustdoc/passes/collect_intra_doc_links.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,13 +221,15 @@ pub(crate) struct DiagnosticInfo<'a> {
dox: &'a str,
ori_link: &'a str,
link_range: MarkdownLinkRange,
emit_error: bool,
}

pub(crate) struct OwnedDiagnosticInfo {
item: Item,
dox: String,
ori_link: String,
link_range: MarkdownLinkRange,
emit_error: bool,
}

impl From<DiagnosticInfo<'_>> for OwnedDiagnosticInfo {
Expand All @@ -237,6 +239,7 @@ impl From<DiagnosticInfo<'_>> for OwnedDiagnosticInfo {
dox: f.dox.to_string(),
ori_link: f.ori_link.to_string(),
link_range: f.link_range.clone(),
emit_error: f.emit_error,
}
}
}
Expand All @@ -248,6 +251,7 @@ impl OwnedDiagnosticInfo {
ori_link: &self.ori_link,
dox: &self.dox,
link_range: self.link_range.clone(),
emit_error: self.emit_error,
}
}
}
Expand Down Expand Up @@ -1189,9 +1193,16 @@ impl LinkCollector<'_, '_> {
dox,
ori_link: &ori_link.link,
link_range: ori_link.range.clone(),
emit_error: ori_link.emit_error,
};
let PreprocessingInfo { path_str, disambiguator, extra_fragment, link_text } =
pp_link.as_ref().map_err(|err| err.report(self.cx, diag_info.clone())).ok()?;
let PreprocessingInfo { path_str, disambiguator, extra_fragment, link_text } = pp_link
.as_ref()
.map_err(|err| {
if ori_link.emit_error {
err.report(self.cx, diag_info.clone())
}
})
.ok()?;
let disambiguator = *disambiguator;

let mut resolved = self.resolve_with_disambiguator_cached(
Expand Down Expand Up @@ -1297,34 +1308,40 @@ impl LinkCollector<'_, '_> {
}
}
0 => {
report_diagnostic(
self.cx.tcx,
BROKEN_INTRA_DOC_LINKS,
format!("all items matching `{path_str}` are private or doc(hidden)"),
&diag_info,
|diag, sp, _| {
if let Some(sp) = sp {
diag.span_label(sp, "unresolved link");
} else {
diag.note("unresolved link");
}
},
);
if diag_info.emit_error {
report_diagnostic(
self.cx.tcx,
BROKEN_INTRA_DOC_LINKS,
format!(
"all items matching `{path_str}` are private or doc(hidden)"
),
&diag_info,
|diag, sp, _| {
if let Some(sp) = sp {
diag.span_label(sp, "unresolved link");
} else {
diag.note("unresolved link");
}
},
);
}
}
_ => {
let candidates = info
.resolved
.iter()
.map(|(res, fragment)| {
let def_id = if let Some(UrlFragment::Item(def_id)) = fragment {
Some(*def_id)
} else {
None
};
(*res, def_id)
})
.collect::<Vec<_>>();
ambiguity_error(self.cx, &diag_info, path_str, &candidates, true);
if diag_info.emit_error {
let candidates = info
.resolved
.iter()
.map(|(res, fragment)| {
let def_id = if let Some(UrlFragment::Item(def_id)) = fragment {
Some(*def_id)
} else {
None
};
(*res, def_id)
})
.collect::<Vec<_>>();
ambiguity_error(self.cx, &diag_info, path_str, &candidates, true);
}
}
}
}
Expand Down Expand Up @@ -1929,9 +1946,12 @@ fn report_diagnostic(
tcx: TyCtxt<'_>,
lint: &'static Lint,
msg: impl Into<DiagMessage> + Display,
DiagnosticInfo { item, ori_link: _, dox, link_range }: &DiagnosticInfo<'_>,
DiagnosticInfo { item, ori_link: _, dox, link_range, emit_error }: &DiagnosticInfo<'_>,
decorate: impl FnOnce(&mut Diag<'_, ()>, Option<rustc_span::Span>, MarkdownLinkRange),
) {
if !*emit_error {
return;
}
let Some(hir_id) = DocContext::as_local_hir_id(tcx, item.item_id) else {
// If non-local, no need to check anything.
info!("ignoring warning from parent crate: {msg}");
Expand Down
25 changes: 25 additions & 0 deletions tests/rustdoc-html/intra-doc/case-sensitivity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// This test ensures that links are case sensitive and that intra-doc links don't get overriden
// by a link definition below which the same when case insensitive.
// Regression test for <https://github.com/rust-lang/rust/issues/80882>.

#![crate_name = "foo"]

//@ has 'foo/index.html'
//@ has - '//*[@class="item-table"]/dd/a[@href="trait.Foo.html"]' 'Foo'
//@ has - '//*[@class="item-table"]/dd/a[@href="trait.Bar.html#tymethod.foo"]' 'foo'

/// [`Foo`] and [`foo`]
///
/// [`foo`]: Bar::foo
pub trait Bar {
fn foo();
}

// We want to ensure that the link is correctly resolved in case the link def doesn't
// case-sensitively match.
//@ has - '//*[@class="item-table"]/dd/a[@href="https://cookie.land"]' 'Flower'

/// [`Flower`]
///
/// [`flower`]: https://cookie.land
pub trait Foo {}
13 changes: 13 additions & 0 deletions tests/rustdoc-ui/intra-doc/case-sensitivity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Regression test for <https://github.com/rust-lang/rust/issues/80882>.

//@ check-pass

#![deny(rustdoc::broken_intra_doc_links)]

// We want to ensure that there is no warning emitted in case we first check that
// `Flower` is not an intra-doc link.

/// [`Flower`]
///
/// [`flower`]: https://cookie.land
pub trait Foo {}
Loading