Skip to content

Commit d763ffa

Browse files
authored
Rollup merge of #150721 - deprecated-doc-intra-link, r=GuillaumeGomez
Deprecated doc intra link fixes #98342 r? @GuillaumeGomez Renders intra-doc links in the note text of the `#[deprecated]` attribute. It is quite natural to suggest some other function to use there. So e.g. ```rust #[deprecated(since = "0.0.0", note = "use [`std::mem::size_of`] instead")] ``` renders as <img width="431" height="74" alt="Screenshot from 2026-01-06 12-08-21" src="https://github.com/user-attachments/assets/8f608f08-13ee-4bbf-a631-6008058a51e2" />
2 parents cb3b2d8 + 3be74a7 commit d763ffa

11 files changed

Lines changed: 185 additions & 21 deletions

File tree

compiler/rustc_ast/src/attr/mod.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,34 @@ impl AttributeExt for Attribute {
235235
}
236236
}
237237

238+
fn deprecation_note(&self) -> Option<Symbol> {
239+
match &self.kind {
240+
AttrKind::Normal(normal) if normal.item.path == sym::deprecated => {
241+
let meta = &normal.item;
242+
243+
// #[deprecated = "..."]
244+
if let Some(s) = meta.value_str() {
245+
return Some(s);
246+
}
247+
248+
// #[deprecated(note = "...")]
249+
if let Some(list) = meta.meta_item_list() {
250+
for nested in list {
251+
if let Some(mi) = nested.meta_item()
252+
&& mi.path == sym::note
253+
&& let Some(s) = mi.value_str()
254+
{
255+
return Some(s);
256+
}
257+
}
258+
}
259+
260+
None
261+
}
262+
_ => None,
263+
}
264+
}
265+
238266
fn doc_resolution_scope(&self) -> Option<AttrStyle> {
239267
match &self.kind {
240268
AttrKind::DocComment(..) => Some(self.style),
@@ -277,6 +305,7 @@ impl Attribute {
277305

278306
pub fn may_have_doc_links(&self) -> bool {
279307
self.doc_str().is_some_and(|s| comments::may_have_doc_links(s.as_str()))
308+
|| self.deprecation_note().is_some_and(|s| comments::may_have_doc_links(s.as_str()))
280309
}
281310

282311
/// Extracts the MetaItem from inside this Attribute.
@@ -873,6 +902,11 @@ pub trait AttributeExt: Debug {
873902
/// * `#[doc(...)]` returns `None`.
874903
fn doc_str(&self) -> Option<Symbol>;
875904

905+
/// Returns the deprecation note if this is deprecation attribute.
906+
/// * `#[deprecated = "note"]` returns `Some("note")`.
907+
/// * `#[deprecated(note = "note", ...)]` returns `Some("note")`.
908+
fn deprecation_note(&self) -> Option<Symbol>;
909+
876910
fn is_proc_macro_attr(&self) -> bool {
877911
[sym::proc_macro, sym::proc_macro_attribute, sym::proc_macro_derive]
878912
.iter()

compiler/rustc_hir/src/hir.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1401,6 +1401,14 @@ impl AttributeExt for Attribute {
14011401
}
14021402
}
14031403

1404+
#[inline]
1405+
fn deprecation_note(&self) -> Option<Symbol> {
1406+
match &self {
1407+
Attribute::Parsed(AttributeKind::Deprecation { deprecation, .. }) => deprecation.note,
1408+
_ => None,
1409+
}
1410+
}
1411+
14041412
fn is_automatically_derived_attr(&self) -> bool {
14051413
matches!(self, Attribute::Parsed(AttributeKind::AutomaticallyDerived(..)))
14061414
}

compiler/rustc_resolve/src/rustdoc.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -410,8 +410,17 @@ pub fn may_be_doc_link(link_type: LinkType) -> bool {
410410
/// Simplified version of `preprocessed_markdown_links` from rustdoc.
411411
/// Must return at least the same links as it, but may add some more links on top of that.
412412
pub(crate) fn attrs_to_preprocessed_links<A: AttributeExt + Clone>(attrs: &[A]) -> Vec<Box<str>> {
413-
let (doc_fragments, _) = attrs_to_doc_fragments(attrs.iter().map(|attr| (attr, None)), true);
414-
let doc = prepare_to_doc_link_resolution(&doc_fragments).into_values().next().unwrap();
413+
let (doc_fragments, other_attrs) =
414+
attrs_to_doc_fragments(attrs.iter().map(|attr| (attr, None)), false);
415+
let mut doc =
416+
prepare_to_doc_link_resolution(&doc_fragments).into_values().next().unwrap_or_default();
417+
418+
for attr in other_attrs {
419+
if let Some(note) = attr.deprecation_note() {
420+
doc += note.as_str();
421+
doc += "\n";
422+
}
423+
}
415424

416425
parse_links(&doc)
417426
}

src/librustdoc/clean/types.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::{fmt, iter};
77
use arrayvec::ArrayVec;
88
use itertools::Either;
99
use rustc_abi::{ExternAbi, VariantIdx};
10+
use rustc_ast::attr::AttributeExt;
1011
use rustc_data_structures::fx::{FxHashSet, FxIndexMap, FxIndexSet};
1112
use rustc_data_structures::thin_vec::ThinVec;
1213
use rustc_hir::attrs::{AttributeKind, DeprecatedSince, Deprecation, DocAttribute};
@@ -450,7 +451,16 @@ impl Item {
450451
}
451452

452453
pub(crate) fn attr_span(&self, tcx: TyCtxt<'_>) -> rustc_span::Span {
454+
let deprecation_notes = self
455+
.attrs
456+
.other_attrs
457+
.iter()
458+
.filter_map(|attr| attr.deprecation_note().map(|_| attr.span()));
459+
453460
span_of_fragments(&self.attrs.doc_strings)
461+
.into_iter()
462+
.chain(deprecation_notes)
463+
.reduce(|a, b| a.to(b))
454464
.unwrap_or_else(|| self.span(tcx).map_or(DUMMY_SP, |span| span.inner()))
455465
}
456466

src/librustdoc/html/markdown.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,11 @@ pub(crate) struct MarkdownWithToc<'a> {
111111
}
112112
/// A tuple struct like `Markdown` that renders the markdown escaping HTML tags
113113
/// and includes no paragraph tags.
114-
pub(crate) struct MarkdownItemInfo<'a>(pub(crate) &'a str, pub(crate) &'a mut IdMap);
114+
pub(crate) struct MarkdownItemInfo<'a> {
115+
pub(crate) content: &'a str,
116+
pub(crate) links: &'a [RenderedLink],
117+
pub(crate) ids: &'a mut IdMap,
118+
}
115119
/// A tuple struct like `Markdown` that renders only the first paragraph.
116120
pub(crate) struct MarkdownSummaryLine<'a>(pub &'a str, pub &'a [RenderedLink]);
117121

@@ -1459,15 +1463,28 @@ impl MarkdownWithToc<'_> {
14591463
}
14601464
}
14611465

1462-
impl MarkdownItemInfo<'_> {
1466+
impl<'a> MarkdownItemInfo<'a> {
1467+
pub(crate) fn new(content: &'a str, links: &'a [RenderedLink], ids: &'a mut IdMap) -> Self {
1468+
Self { content, links, ids }
1469+
}
1470+
14631471
pub(crate) fn write_into(self, mut f: impl fmt::Write) -> fmt::Result {
1464-
let MarkdownItemInfo(md, ids) = self;
1472+
let MarkdownItemInfo { content: md, links, ids } = self;
14651473

14661474
// This is actually common enough to special-case
14671475
if md.is_empty() {
14681476
return Ok(());
14691477
}
1470-
let p = Parser::new_ext(md, main_body_opts()).into_offset_iter();
1478+
1479+
let replacer = move |broken_link: BrokenLink<'_>| {
1480+
links
1481+
.iter()
1482+
.find(|link| *link.original_text == *broken_link.reference)
1483+
.map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
1484+
};
1485+
1486+
let p = Parser::new_with_broken_link_callback(md, main_body_opts(), Some(replacer));
1487+
let p = p.into_offset_iter();
14711488

14721489
// Treat inline HTML as plain text.
14731490
let p = p.map(|event| match event.0 {
@@ -1477,6 +1494,7 @@ impl MarkdownItemInfo<'_> {
14771494

14781495
ids.handle_footnotes(|ids, existing_footnotes| {
14791496
let p = HeadingLinks::new(p, None, ids, HeadingOffset::H1);
1497+
let p = SpannedLinkReplacer::new(p, links);
14801498
let p = footnotes::Footnotes::new(p, existing_footnotes);
14811499
let p = TableWrapper::new(p.map(|(ev, _)| ev));
14821500
let p = p.filter(|event| {

src/librustdoc/html/markdown/tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,7 @@ fn test_markdown_html_escape() {
471471
fn t(input: &str, expect: &str) {
472472
let mut idmap = IdMap::new();
473473
let mut output = String::new();
474-
MarkdownItemInfo(input, &mut idmap).write_into(&mut output).unwrap();
474+
MarkdownItemInfo::new(input, &[], &mut idmap).write_into(&mut output).unwrap();
475475
assert_eq!(output, expect, "original: {}", input);
476476
}
477477

src/librustdoc/html/render/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -877,7 +877,8 @@ fn short_item_info(
877877
if let Some(note) = note {
878878
let note = note.as_str();
879879
let mut id_map = cx.id_map.borrow_mut();
880-
let html = MarkdownItemInfo(note, &mut id_map);
880+
let links = item.links(cx);
881+
let html = MarkdownItemInfo::new(note, &links, &mut id_map);
881882
message.push_str(": ");
882883
html.write_into(&mut message).unwrap();
883884
}

src/librustdoc/passes/collect_intra_doc_links.rs

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::fmt::Display;
77
use std::mem;
88
use std::ops::Range;
99

10+
use rustc_ast::attr::AttributeExt;
1011
use rustc_ast::util::comments::may_have_doc_links;
1112
use rustc_data_structures::fx::{FxHashMap, FxHashSet, FxIndexMap, FxIndexSet};
1213
use rustc_data_structures::intern::Interned;
@@ -1047,18 +1048,7 @@ impl LinkCollector<'_, '_> {
10471048
return;
10481049
}
10491050

1050-
// We want to resolve in the lexical scope of the documentation.
1051-
// In the presence of re-exports, this is not the same as the module of the item.
1052-
// Rather than merging all documentation into one, resolve it one attribute at a time
1053-
// so we know which module it came from.
1054-
for (item_id, doc) in prepare_to_doc_link_resolution(&item.attrs.doc_strings) {
1055-
if !may_have_doc_links(&doc) {
1056-
continue;
1057-
}
1058-
debug!("combined_docs={doc}");
1059-
// NOTE: if there are links that start in one crate and end in another, this will not resolve them.
1060-
// This is a degenerate case and it's not supported by rustdoc.
1061-
let item_id = item_id.unwrap_or_else(|| item.item_id.expect_def_id());
1051+
let mut insert_links = |item_id, doc: &str| {
10621052
let module_id = match self.cx.tcx.def_kind(item_id) {
10631053
DefKind::Mod if item.inner_docs(self.cx.tcx) => item_id,
10641054
_ => find_nearest_parent_module(self.cx.tcx, item_id).unwrap(),
@@ -1074,6 +1064,35 @@ impl LinkCollector<'_, '_> {
10741064
.insert(link);
10751065
}
10761066
}
1067+
};
1068+
1069+
// We want to resolve in the lexical scope of the documentation.
1070+
// In the presence of re-exports, this is not the same as the module of the item.
1071+
// Rather than merging all documentation into one, resolve it one attribute at a time
1072+
// so we know which module it came from.
1073+
for (item_id, doc) in prepare_to_doc_link_resolution(&item.attrs.doc_strings) {
1074+
if !may_have_doc_links(&doc) {
1075+
continue;
1076+
}
1077+
1078+
debug!("combined_docs={doc}");
1079+
// NOTE: if there are links that start in one crate and end in another, this will not resolve them.
1080+
// This is a degenerate case and it's not supported by rustdoc.
1081+
let item_id = item_id.unwrap_or_else(|| item.item_id.expect_def_id());
1082+
insert_links(item_id, &doc)
1083+
}
1084+
1085+
// Also resolve links in the note text of `#[deprecated]`.
1086+
for attr in &item.attrs.other_attrs {
1087+
let Some(note_sym) = attr.deprecation_note() else { continue };
1088+
let note = note_sym.as_str();
1089+
1090+
if !may_have_doc_links(note) {
1091+
continue;
1092+
}
1093+
1094+
debug!("deprecated_note={note}");
1095+
insert_links(item.item_id.expect_def_id(), note)
10771096
}
10781097
}
10791098

@@ -1086,7 +1105,7 @@ impl LinkCollector<'_, '_> {
10861105
/// FIXME(jynelson): this is way too many arguments
10871106
fn resolve_link(
10881107
&mut self,
1089-
dox: &String,
1108+
dox: &str,
10901109
item: &Item,
10911110
item_id: DefId,
10921111
module_id: DefId,
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//@ has deprecated/struct.A.html '//a[@href="{{channel}}/core/ops/range/struct.Range.html#structfield.start"]' 'start'
2+
//@ has deprecated/struct.B1.html '//a[@href="{{channel}}/std/io/error/enum.ErrorKind.html#variant.NotFound"]' 'not_found'
3+
//@ has deprecated/struct.B2.html '//a[@href="{{channel}}/std/io/error/enum.ErrorKind.html#variant.NotFound"]' 'not_found'
4+
5+
#[deprecated = "[start][std::ops::Range::start]"]
6+
pub struct A;
7+
8+
#[deprecated(since = "0.0.0", note = "[not_found][std::io::ErrorKind::NotFound]")]
9+
pub struct B1;
10+
11+
#[deprecated(note = "[not_found][std::io::ErrorKind::NotFound]", since = "0.0.0")]
12+
pub struct B2;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#![deny(rustdoc::broken_intra_doc_links)]
2+
3+
#[deprecated = "[broken cross-reference](TypeAlias::hoge)"] //~ ERROR
4+
pub struct A;
5+
6+
#[deprecated(since = "0.0.0", note = "[broken cross-reference](TypeAlias::hoge)")] //~ ERROR
7+
pub struct B1;
8+
9+
#[deprecated(note = "[broken cross-reference](TypeAlias::hoge)", since = "0.0.0")] //~ ERROR
10+
pub struct B2;

0 commit comments

Comments
 (0)