diff --git a/compiler/rustc_ast/src/attr/mod.rs b/compiler/rustc_ast/src/attr/mod.rs index 6ecba865c8156..0a2a34d932f61 100644 --- a/compiler/rustc_ast/src/attr/mod.rs +++ b/compiler/rustc_ast/src/attr/mod.rs @@ -235,6 +235,34 @@ impl AttributeExt for Attribute { } } + fn deprecation_note(&self) -> Option { + match &self.kind { + AttrKind::Normal(normal) if normal.item.path == sym::deprecated => { + let meta = &normal.item; + + // #[deprecated = "..."] + if let Some(s) = meta.value_str() { + return Some(s); + } + + // #[deprecated(note = "...")] + if let Some(list) = meta.meta_item_list() { + for nested in list { + if let Some(mi) = nested.meta_item() + && mi.path == sym::note + && let Some(s) = mi.value_str() + { + return Some(s); + } + } + } + + None + } + _ => None, + } + } + fn doc_resolution_scope(&self) -> Option { match &self.kind { AttrKind::DocComment(..) => Some(self.style), @@ -277,6 +305,7 @@ impl Attribute { pub fn may_have_doc_links(&self) -> bool { self.doc_str().is_some_and(|s| comments::may_have_doc_links(s.as_str())) + || self.deprecation_note().is_some_and(|s| comments::may_have_doc_links(s.as_str())) } /// Extracts the MetaItem from inside this Attribute. @@ -873,6 +902,11 @@ pub trait AttributeExt: Debug { /// * `#[doc(...)]` returns `None`. fn doc_str(&self) -> Option; + /// Returns the deprecation note if this is deprecation attribute. + /// * `#[deprecated = "note"]` returns `Some("note")`. + /// * `#[deprecated(note = "note", ...)]` returns `Some("note")`. + fn deprecation_note(&self) -> Option; + fn is_proc_macro_attr(&self) -> bool { [sym::proc_macro, sym::proc_macro_attribute, sym::proc_macro_derive] .iter() diff --git a/compiler/rustc_hir/src/hir.rs b/compiler/rustc_hir/src/hir.rs index aacd6324bb031..883ba23ca214f 100644 --- a/compiler/rustc_hir/src/hir.rs +++ b/compiler/rustc_hir/src/hir.rs @@ -1400,6 +1400,14 @@ impl AttributeExt for Attribute { } } + #[inline] + fn deprecation_note(&self) -> Option { + match &self { + Attribute::Parsed(AttributeKind::Deprecation { deprecation, .. }) => deprecation.note, + _ => None, + } + } + fn is_automatically_derived_attr(&self) -> bool { matches!(self, Attribute::Parsed(AttributeKind::AutomaticallyDerived(..))) } diff --git a/compiler/rustc_resolve/src/rustdoc.rs b/compiler/rustc_resolve/src/rustdoc.rs index 7f7c423acb40a..9f74a7801d2ee 100644 --- a/compiler/rustc_resolve/src/rustdoc.rs +++ b/compiler/rustc_resolve/src/rustdoc.rs @@ -410,8 +410,17 @@ pub fn may_be_doc_link(link_type: LinkType) -> bool { /// Simplified version of `preprocessed_markdown_links` from rustdoc. /// Must return at least the same links as it, but may add some more links on top of that. pub(crate) fn attrs_to_preprocessed_links(attrs: &[A]) -> Vec> { - let (doc_fragments, _) = attrs_to_doc_fragments(attrs.iter().map(|attr| (attr, None)), true); - let doc = prepare_to_doc_link_resolution(&doc_fragments).into_values().next().unwrap(); + let (doc_fragments, other_attrs) = + attrs_to_doc_fragments(attrs.iter().map(|attr| (attr, None)), false); + let mut doc = + prepare_to_doc_link_resolution(&doc_fragments).into_values().next().unwrap_or_default(); + + for attr in other_attrs { + if let Some(note) = attr.deprecation_note() { + doc += note.as_str(); + doc += "\n"; + } + } parse_links(&doc) } diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs index a390a03ff1144..c3bafd3db13ac 100644 --- a/src/librustdoc/clean/types.rs +++ b/src/librustdoc/clean/types.rs @@ -7,6 +7,7 @@ use std::{fmt, iter}; use arrayvec::ArrayVec; use itertools::Either; use rustc_abi::{ExternAbi, VariantIdx}; +use rustc_ast::attr::AttributeExt; use rustc_data_structures::fx::{FxHashSet, FxIndexMap, FxIndexSet}; use rustc_data_structures::thin_vec::ThinVec; use rustc_hir::attrs::{AttributeKind, DeprecatedSince, Deprecation, DocAttribute}; @@ -450,7 +451,16 @@ impl Item { } pub(crate) fn attr_span(&self, tcx: TyCtxt<'_>) -> rustc_span::Span { + let deprecation_notes = self + .attrs + .other_attrs + .iter() + .filter_map(|attr| attr.deprecation_note().map(|_| attr.span())); + span_of_fragments(&self.attrs.doc_strings) + .into_iter() + .chain(deprecation_notes) + .reduce(|a, b| a.to(b)) .unwrap_or_else(|| self.span(tcx).map_or(DUMMY_SP, |span| span.inner())) } diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index a4d377432c914..c472c20a7dc71 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -111,7 +111,11 @@ pub(crate) struct MarkdownWithToc<'a> { } /// A tuple struct like `Markdown` that renders the markdown escaping HTML tags /// and includes no paragraph tags. -pub(crate) struct MarkdownItemInfo<'a>(pub(crate) &'a str, pub(crate) &'a mut IdMap); +pub(crate) struct MarkdownItemInfo<'a> { + pub(crate) content: &'a str, + pub(crate) links: &'a [RenderedLink], + pub(crate) ids: &'a mut IdMap, +} /// A tuple struct like `Markdown` that renders only the first paragraph. pub(crate) struct MarkdownSummaryLine<'a>(pub &'a str, pub &'a [RenderedLink]); @@ -1459,15 +1463,28 @@ impl MarkdownWithToc<'_> { } } -impl MarkdownItemInfo<'_> { +impl<'a> MarkdownItemInfo<'a> { + pub(crate) fn new(content: &'a str, links: &'a [RenderedLink], ids: &'a mut IdMap) -> Self { + Self { content, links, ids } + } + pub(crate) fn write_into(self, mut f: impl fmt::Write) -> fmt::Result { - let MarkdownItemInfo(md, ids) = self; + let MarkdownItemInfo { content: md, links, ids } = self; // This is actually common enough to special-case if md.is_empty() { return Ok(()); } - let p = Parser::new_ext(md, main_body_opts()).into_offset_iter(); + + let replacer = move |broken_link: BrokenLink<'_>| { + links + .iter() + .find(|link| *link.original_text == *broken_link.reference) + .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into())) + }; + + let p = Parser::new_with_broken_link_callback(md, main_body_opts(), Some(replacer)); + let p = p.into_offset_iter(); // Treat inline HTML as plain text. let p = p.map(|event| match event.0 { @@ -1477,6 +1494,7 @@ impl MarkdownItemInfo<'_> { ids.handle_footnotes(|ids, existing_footnotes| { let p = HeadingLinks::new(p, None, ids, HeadingOffset::H1); + let p = SpannedLinkReplacer::new(p, links); let p = footnotes::Footnotes::new(p, existing_footnotes); let p = TableWrapper::new(p.map(|(ev, _)| ev)); let p = p.filter(|event| { diff --git a/src/librustdoc/html/markdown/tests.rs b/src/librustdoc/html/markdown/tests.rs index 61fd428746332..1c99ccc5228b1 100644 --- a/src/librustdoc/html/markdown/tests.rs +++ b/src/librustdoc/html/markdown/tests.rs @@ -471,7 +471,7 @@ fn test_markdown_html_escape() { fn t(input: &str, expect: &str) { let mut idmap = IdMap::new(); let mut output = String::new(); - MarkdownItemInfo(input, &mut idmap).write_into(&mut output).unwrap(); + MarkdownItemInfo::new(input, &[], &mut idmap).write_into(&mut output).unwrap(); assert_eq!(output, expect, "original: {}", input); } diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index 4529f5a8c0163..63de870f07f45 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -877,7 +877,8 @@ fn short_item_info( if let Some(note) = note { let note = note.as_str(); let mut id_map = cx.id_map.borrow_mut(); - let html = MarkdownItemInfo(note, &mut id_map); + let links = item.links(cx); + let html = MarkdownItemInfo::new(note, &links, &mut id_map); message.push_str(": "); html.write_into(&mut message).unwrap(); } diff --git a/src/librustdoc/passes/collect_intra_doc_links.rs b/src/librustdoc/passes/collect_intra_doc_links.rs index 3abf0fee3959a..07d6efaa97e15 100644 --- a/src/librustdoc/passes/collect_intra_doc_links.rs +++ b/src/librustdoc/passes/collect_intra_doc_links.rs @@ -7,6 +7,7 @@ use std::fmt::Display; use std::mem; use std::ops::Range; +use rustc_ast::attr::AttributeExt; use rustc_ast::util::comments::may_have_doc_links; use rustc_data_structures::fx::{FxHashMap, FxHashSet, FxIndexMap, FxIndexSet}; use rustc_data_structures::intern::Interned; @@ -1047,18 +1048,7 @@ impl LinkCollector<'_, '_> { return; } - // We want to resolve in the lexical scope of the documentation. - // In the presence of re-exports, this is not the same as the module of the item. - // Rather than merging all documentation into one, resolve it one attribute at a time - // so we know which module it came from. - for (item_id, doc) in prepare_to_doc_link_resolution(&item.attrs.doc_strings) { - if !may_have_doc_links(&doc) { - continue; - } - debug!("combined_docs={doc}"); - // NOTE: if there are links that start in one crate and end in another, this will not resolve them. - // This is a degenerate case and it's not supported by rustdoc. - let item_id = item_id.unwrap_or_else(|| item.item_id.expect_def_id()); + let mut insert_links = |item_id, doc: &str| { let module_id = match self.cx.tcx.def_kind(item_id) { DefKind::Mod if item.inner_docs(self.cx.tcx) => item_id, _ => find_nearest_parent_module(self.cx.tcx, item_id).unwrap(), @@ -1074,6 +1064,35 @@ impl LinkCollector<'_, '_> { .insert(link); } } + }; + + // We want to resolve in the lexical scope of the documentation. + // In the presence of re-exports, this is not the same as the module of the item. + // Rather than merging all documentation into one, resolve it one attribute at a time + // so we know which module it came from. + for (item_id, doc) in prepare_to_doc_link_resolution(&item.attrs.doc_strings) { + if !may_have_doc_links(&doc) { + continue; + } + + debug!("combined_docs={doc}"); + // NOTE: if there are links that start in one crate and end in another, this will not resolve them. + // This is a degenerate case and it's not supported by rustdoc. + let item_id = item_id.unwrap_or_else(|| item.item_id.expect_def_id()); + insert_links(item_id, &doc) + } + + // Also resolve links in the note text of `#[deprecated]`. + for attr in &item.attrs.other_attrs { + let Some(note_sym) = attr.deprecation_note() else { continue }; + let note = note_sym.as_str(); + + if !may_have_doc_links(note) { + continue; + } + + debug!("deprecated_note={note}"); + insert_links(item.item_id.expect_def_id(), note) } } @@ -1086,7 +1105,7 @@ impl LinkCollector<'_, '_> { /// FIXME(jynelson): this is way too many arguments fn resolve_link( &mut self, - dox: &String, + dox: &str, item: &Item, item_id: DefId, module_id: DefId, diff --git a/tests/rustdoc-html/intra-doc/deprecated.rs b/tests/rustdoc-html/intra-doc/deprecated.rs new file mode 100644 index 0000000000000..6f8639593a2d4 --- /dev/null +++ b/tests/rustdoc-html/intra-doc/deprecated.rs @@ -0,0 +1,12 @@ +//@ has deprecated/struct.A.html '//a[@href="{{channel}}/core/ops/range/struct.Range.html#structfield.start"]' 'start' +//@ has deprecated/struct.B1.html '//a[@href="{{channel}}/std/io/error/enum.ErrorKind.html#variant.NotFound"]' 'not_found' +//@ has deprecated/struct.B2.html '//a[@href="{{channel}}/std/io/error/enum.ErrorKind.html#variant.NotFound"]' 'not_found' + +#[deprecated = "[start][std::ops::Range::start]"] +pub struct A; + +#[deprecated(since = "0.0.0", note = "[not_found][std::io::ErrorKind::NotFound]")] +pub struct B1; + +#[deprecated(note = "[not_found][std::io::ErrorKind::NotFound]", since = "0.0.0")] +pub struct B2; diff --git a/tests/rustdoc-ui/intra-doc/deprecated.rs b/tests/rustdoc-ui/intra-doc/deprecated.rs new file mode 100644 index 0000000000000..37c27dcde598a --- /dev/null +++ b/tests/rustdoc-ui/intra-doc/deprecated.rs @@ -0,0 +1,10 @@ +#![deny(rustdoc::broken_intra_doc_links)] + +#[deprecated = "[broken cross-reference](TypeAlias::hoge)"] //~ ERROR +pub struct A; + +#[deprecated(since = "0.0.0", note = "[broken cross-reference](TypeAlias::hoge)")] //~ ERROR +pub struct B1; + +#[deprecated(note = "[broken cross-reference](TypeAlias::hoge)", since = "0.0.0")] //~ ERROR +pub struct B2; diff --git a/tests/rustdoc-ui/intra-doc/deprecated.stderr b/tests/rustdoc-ui/intra-doc/deprecated.stderr new file mode 100644 index 0000000000000..9bd64544eef82 --- /dev/null +++ b/tests/rustdoc-ui/intra-doc/deprecated.stderr @@ -0,0 +1,43 @@ +error: unresolved link to `TypeAlias::hoge` + --> $DIR/deprecated.rs:3:1 + | +LL | #[deprecated = "[broken cross-reference](TypeAlias::hoge)"] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: the link appears in this line: + + [broken cross-reference](TypeAlias::hoge) + ^^^^^^^^^^^^^^^ + = note: no item named `TypeAlias` in scope +note: the lint level is defined here + --> $DIR/deprecated.rs:1:9 + | +LL | #![deny(rustdoc::broken_intra_doc_links)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: unresolved link to `TypeAlias::hoge` + --> $DIR/deprecated.rs:6:1 + | +LL | #[deprecated(since = "0.0.0", note = "[broken cross-reference](TypeAlias::hoge)")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: the link appears in this line: + + [broken cross-reference](TypeAlias::hoge) + ^^^^^^^^^^^^^^^ + = note: no item named `TypeAlias` in scope + +error: unresolved link to `TypeAlias::hoge` + --> $DIR/deprecated.rs:9:1 + | +LL | #[deprecated(note = "[broken cross-reference](TypeAlias::hoge)", since = "0.0.0")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: the link appears in this line: + + [broken cross-reference](TypeAlias::hoge) + ^^^^^^^^^^^^^^^ + = note: no item named `TypeAlias` in scope + +error: aborting due to 3 previous errors +