Skip to content

Commit c01b87b

Browse files
committed
render intra-doc links in the #[deprectated] note
1 parent b1d3d3a commit c01b87b

11 files changed

Lines changed: 179 additions & 22 deletions

File tree

compiler/rustc_ast/src/attr/mod.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,34 @@ impl AttributeExt for Attribute {
213213
}
214214
}
215215

216+
fn deprecation_note(&self) -> Option<Symbol> {
217+
match &self.kind {
218+
AttrKind::Normal(normal) if normal.item.path == sym::deprecated => {
219+
let meta = &normal.item;
220+
221+
// #[deprecated = "..."]
222+
if let Some(s) = meta.value_str() {
223+
return Some(s);
224+
}
225+
226+
// #[deprecated(note = "...")]
227+
if let Some(list) = meta.meta_item_list() {
228+
for nested in list {
229+
if let Some(mi) = nested.meta_item()
230+
&& mi.path == sym::note
231+
&& let Some(s) = mi.value_str()
232+
{
233+
return Some(s);
234+
}
235+
}
236+
}
237+
238+
None
239+
}
240+
_ => None,
241+
}
242+
}
243+
216244
fn doc_resolution_scope(&self) -> Option<AttrStyle> {
217245
match &self.kind {
218246
AttrKind::DocComment(..) => Some(self.style),
@@ -255,6 +283,7 @@ impl Attribute {
255283

256284
pub fn may_have_doc_links(&self) -> bool {
257285
self.doc_str().is_some_and(|s| comments::may_have_doc_links(s.as_str()))
286+
|| self.deprecation_note().is_some_and(|s| comments::may_have_doc_links(s.as_str()))
258287
}
259288

260289
/// Extracts the MetaItem from inside this Attribute.
@@ -850,6 +879,11 @@ pub trait AttributeExt: Debug {
850879
/// * `#[doc(...)]` returns `None`.
851880
fn doc_str(&self) -> Option<Symbol>;
852881

882+
/// Returns the deprecation note if this is deprecation attribute.
883+
/// * `#[deprecated = "note"]` returns `Some("note")`.
884+
/// * `#[deprecated(note = "note", ...)]` returns `Some("note")`.
885+
fn deprecation_note(&self) -> Option<Symbol>;
886+
853887
fn is_proc_macro_attr(&self) -> bool {
854888
[sym::proc_macro, sym::proc_macro_attribute, sym::proc_macro_derive]
855889
.iter()

compiler/rustc_hir/src/hir.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1397,6 +1397,14 @@ impl AttributeExt for Attribute {
13971397
}
13981398
}
13991399

1400+
#[inline]
1401+
fn deprecation_note(&self) -> Option<Symbol> {
1402+
match &self {
1403+
Attribute::Parsed(AttributeKind::Deprecation { deprecation, .. }) => deprecation.note,
1404+
_ => None,
1405+
}
1406+
}
1407+
14001408
fn is_automatically_derived_attr(&self) -> bool {
14011409
matches!(self, Attribute::Parsed(AttributeKind::AutomaticallyDerived(..)))
14021410
}

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: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ pub(crate) struct MarkdownWithToc<'a> {
113113
/// and includes no paragraph tags.
114114
pub(crate) struct MarkdownItemInfo<'a> {
115115
pub(crate) content: &'a str,
116+
pub(crate) links: &'a [RenderedLink],
116117
pub(crate) ids: &'a mut IdMap,
117118
}
118119
/// A tuple struct like `Markdown` that renders only the first paragraph.
@@ -1463,18 +1464,27 @@ impl MarkdownWithToc<'_> {
14631464
}
14641465

14651466
impl<'a> MarkdownItemInfo<'a> {
1466-
pub(crate) fn new(content: &'a str, ids: &'a mut IdMap) -> Self {
1467-
Self { content, ids }
1467+
pub(crate) fn new(content: &'a str, links: &'a [RenderedLink], ids: &'a mut IdMap) -> Self {
1468+
Self { content, links, ids }
14681469
}
14691470

14701471
pub(crate) fn write_into(self, mut f: impl fmt::Write) -> fmt::Result {
1471-
let MarkdownItemInfo { content, ids } = self;
1472+
let MarkdownItemInfo { content: md, links, ids } = self;
14721473

14731474
// This is actually common enough to special-case
1474-
if content.is_empty() {
1475+
if md.is_empty() {
14751476
return Ok(());
14761477
}
1477-
let p = Parser::new_ext(content, 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();
14781488

14791489
// Treat inline HTML as plain text.
14801490
let p = p.map(|event| match event.0 {
@@ -1484,6 +1494,7 @@ impl<'a> MarkdownItemInfo<'a> {
14841494

14851495
ids.handle_footnotes(|ids, existing_footnotes| {
14861496
let p = HeadingLinks::new(p, None, ids, HeadingOffset::H1);
1497+
let p = SpannedLinkReplacer::new(p, links);
14871498
let p = footnotes::Footnotes::new(p, existing_footnotes);
14881499
let p = TableWrapper::new(p.map(|(ev, _)| ev));
14891500
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::new(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::new(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: 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;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
error: unresolved link to `TypeAlias::hoge`
2+
--> $DIR/deprecated.rs:3:1
3+
|
4+
LL | #[deprecated = "[broken cross-reference](TypeAlias::hoge)"]
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
6+
|
7+
= note: the link appears in this line:
8+
9+
[broken cross-reference](TypeAlias::hoge)
10+
^^^^^^^^^^^^^^^
11+
= note: no item named `TypeAlias` in scope
12+
note: the lint level is defined here
13+
--> $DIR/deprecated.rs:1:9
14+
|
15+
LL | #![deny(rustdoc::broken_intra_doc_links)]
16+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
17+
18+
error: unresolved link to `TypeAlias::hoge`
19+
--> $DIR/deprecated.rs:6:1
20+
|
21+
LL | #[deprecated(since = "0.0.0", note = "[broken cross-reference](TypeAlias::hoge)")]
22+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
23+
|
24+
= note: the link appears in this line:
25+
26+
[broken cross-reference](TypeAlias::hoge)
27+
^^^^^^^^^^^^^^^
28+
= note: no item named `TypeAlias` in scope
29+
30+
error: unresolved link to `TypeAlias::hoge`
31+
--> $DIR/deprecated.rs:9:1
32+
|
33+
LL | #[deprecated(note = "[broken cross-reference](TypeAlias::hoge)", since = "0.0.0")]
34+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
35+
|
36+
= note: the link appears in this line:
37+
38+
[broken cross-reference](TypeAlias::hoge)
39+
^^^^^^^^^^^^^^^
40+
= note: no item named `TypeAlias` in scope
41+
42+
error: aborting due to 3 previous errors
43+

0 commit comments

Comments
 (0)