From f82d251c798d7a2d67763b109844cc10af0e1427 Mon Sep 17 00:00:00 2001 From: binarycat Date: Mon, 21 Sep 2026 15:48:34 -0400 Subject: [PATCH 1/3] rustdoc: fix ICE caused by evaluating an intra-doc link in the wrong scope --- compiler/rustc_resolve/src/late.rs | 9 ++++-- compiler/rustc_resolve/src/rustdoc.rs | 9 ++++-- src/librustdoc/clean/types.rs | 15 +++++++--- .../passes/collect_intra_doc_links.rs | 29 ++++--------------- .../ice-intra-doc-link-in-depr-note-157210.rs | 12 ++++++++ 5 files changed, 42 insertions(+), 32 deletions(-) create mode 100644 tests/rustdoc-ui/ice-intra-doc-link-in-depr-note-157210.rs diff --git a/compiler/rustc_resolve/src/late.rs b/compiler/rustc_resolve/src/late.rs index fe46287373dee..c0c17bc556fb2 100644 --- a/compiler/rustc_resolve/src/late.rs +++ b/compiler/rustc_resolve/src/late.rs @@ -697,7 +697,7 @@ impl PathSource<'_, '_, '_> { /// At this point for most items we can answer whether that item is exported or not, /// but some items like impls require type information to determine exported-ness, so we make a /// conservative estimate for them (e.g. based on nominal visibility). -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] enum MaybeExported<'a> { Ok(NodeId), Impl(Option), @@ -5503,9 +5503,10 @@ impl<'a, 'ast, 'ra, 'tcx> LateResolutionVisitor<'a, 'ast, 'ra, 'tcx> { // FIXME: This caching may be incorrect in case of multiple `macro_rules` // items with the same name in the same module. // Also hygiene is not considered. + let mod_id = self.parent_scope.module.nearest_parent_mod().expect_local(); let mut doc_link_resolutions = std::mem::take(&mut self.r.doc_link_resolutions); let res = *doc_link_resolutions - .entry(self.parent_scope.module.nearest_parent_mod().expect_local()) + .entry(mod_id) .or_default() .entry((Symbol::intern(path_str), ns)) .or_insert_with_key(|(path, ns)| { @@ -5518,6 +5519,7 @@ impl<'a, 'ast, 'ra, 'tcx> LateResolutionVisitor<'a, 'ast, 'ra, 'tcx> { // because it will only store proc macros for it. return None; } + debug!("cached resolution of {mod_id:?} {path_str:?} as {res:?} in ns {ns:?}"); res }); self.r.doc_link_resolutions = doc_link_resolutions; @@ -5535,6 +5537,7 @@ impl<'a, 'ast, 'ra, 'tcx> LateResolutionVisitor<'a, 'ast, 'ra, 'tcx> { } fn resolve_doc_links(&mut self, attrs: &[Attribute], maybe_exported: MaybeExported<'_>) { + debug!(?attrs, ?maybe_exported, ?self.r.tcx.sess.opts.resolve_doc_links); match self.r.tcx.sess.opts.resolve_doc_links { ResolveDocLinks::None => return, ResolveDocLinks::ExportedMetadata @@ -5560,6 +5563,7 @@ impl<'a, 'ast, 'ra, 'tcx> LateResolutionVisitor<'a, 'ast, 'ra, 'tcx> { let mut need_traits_in_scope = false; for path_str in rustdoc::attrs_to_preprocessed_links(attrs) { + debug!("resolving link {path_str:?}"); // Resolve all namespaces due to no disambiguator or for diagnostics. let mut any_resolved = false; let mut need_assoc = false; @@ -5574,6 +5578,7 @@ impl<'a, 'ast, 'ra, 'tcx> LateResolutionVisitor<'a, 'ast, 'ra, 'tcx> { } // Resolve all prefixes for type-relative resolution or for diagnostics. + debug!(?any_resolved, ?need_assoc); if need_assoc || !any_resolved { let mut path = &path_str[..]; while let Some(idx) = path.rfind("::") { diff --git a/compiler/rustc_resolve/src/rustdoc.rs b/compiler/rustc_resolve/src/rustdoc.rs index ddf5600f76659..54946e1d59f4a 100644 --- a/compiler/rustc_resolve/src/rustdoc.rs +++ b/compiler/rustc_resolve/src/rustdoc.rs @@ -200,11 +200,12 @@ pub fn add_doc_fragment(out: &mut String, frag: &DocFragment) { pub fn attrs_to_doc_fragments<'a, A: AttributeExt + Clone + 'a>( attrs: impl Iterator)>, doc_only: bool, -) -> (Vec, ThinVec) { +) -> (Vec, ThinVec, ThinVec>) { let (min_size, max_size) = attrs.size_hint(); let size_hint = max_size.unwrap_or(min_size); let mut doc_fragments = Vec::with_capacity(size_hint); let mut other_attrs = ThinVec::::with_capacity(if doc_only { 0 } else { size_hint }); + let mut other_attrs_src = ThinVec::with_capacity(if doc_only { 0 } else { size_hint }); for (attr, item_id) in attrs { if let Some((doc_str, fragment_kind)) = attr.doc_str_and_fragment_kind() { let doc = beautify_doc_string(doc_str, fragment_kind.comment_kind()); @@ -220,15 +221,17 @@ pub fn attrs_to_doc_fragments<'a, A: AttributeExt + Clone + 'a>( doc_fragments.push(fragment); } else if !doc_only { other_attrs.push(attr.clone()); + other_attrs_src.push(item_id); } } doc_fragments.shrink_to_fit(); other_attrs.shrink_to_fit(); + other_attrs_src.shrink_to_fit(); unindent_doc_fragments(&mut doc_fragments); - (doc_fragments, other_attrs) + (doc_fragments, other_attrs, other_attrs_src) } /// Return the doc-comments on this item, grouped by the module they came from. @@ -410,7 +413,7 @@ 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: &[ast::Attribute]) -> Vec> { - let (doc_fragments, other_attrs) = + let (doc_fragments, other_attrs, _) = attrs_to_doc_fragments(attrs.iter().map(|attr| (attr, None)), false); let doc = prepare_to_doc_link_resolution(&doc_fragments).into_values().next(); let mut links = doc.as_deref().map(parse_links).unwrap_or_default(); diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs index 1b5968e20e4b4..731a44bf87946 100644 --- a/src/librustdoc/clean/types.rs +++ b/src/librustdoc/clean/types.rs @@ -1062,6 +1062,11 @@ pub struct RenderedLink { pub(crate) struct Attributes { pub(crate) doc_strings: Vec, pub(crate) other_attrs: ThinVec, + /// The `DefId` the attribute is from, either the actual item or a re-exporting use item. + /// + /// We need this to properly resolve intra-doc links in deprecation notes on a reexport. + // FIXME: currently deprecation of a reexport does not show in the docs + pub(crate) other_attrs_src: ThinVec>, } impl Attributes { @@ -1091,8 +1096,8 @@ impl Attributes { attrs: impl Iterator)>, doc_only: bool, ) -> Attributes { - let (doc_strings, other_attrs) = attrs_to_doc_fragments(attrs, doc_only); - Attributes { doc_strings, other_attrs } + let (doc_strings, other_attrs, other_attrs_src) = attrs_to_doc_fragments(attrs, doc_only); + Attributes { doc_strings, other_attrs, other_attrs_src } } /// Combine all doc strings into a single value handling indentation and newlines as needed. @@ -1128,9 +1133,11 @@ impl Attributes { } pub(crate) fn merge_with(&mut self, other: Self) { - let Self { doc_strings, other_attrs } = other; + let Self { doc_strings, other_attrs, other_attrs_src } = other; self.doc_strings.extend(doc_strings); self.other_attrs.extend(other_attrs); + self.other_attrs_src.extend(other_attrs_src); + assert_eq!(self.other_attrs.len(), self.other_attrs_src.len()); } } @@ -2518,7 +2525,7 @@ mod size_asserts { static_assert_size!(GenericParamDef, 40); static_assert_size!(Generics, 16); static_assert_size!(Item, 8); - static_assert_size!(ItemInner, 144); + static_assert_size!(ItemInner, 152); static_assert_size!(ItemKind, 48); static_assert_size!(PathSegment, 32); static_assert_size!(Type, 32); diff --git a/src/librustdoc/passes/collect_intra_doc_links.rs b/src/librustdoc/passes/collect_intra_doc_links.rs index 1ae23d58448a3..dd3e0c14ee5c2 100644 --- a/src/librustdoc/passes/collect_intra_doc_links.rs +++ b/src/librustdoc/passes/collect_intra_doc_links.rs @@ -15,7 +15,7 @@ use rustc_hir::attrs::AttributeKind; use rustc_hir::def::Namespace::*; use rustc_hir::def::{DefKind, MacroKinds, Namespace, PerNS}; use rustc_hir::def_id::{CRATE_DEF_ID, DefId, LOCAL_CRATE}; -use rustc_hir::{Attribute, Mutability, Safety, find_attr}; +use rustc_hir::{Attribute, Mutability, Safety}; use rustc_lint::Lint; use rustc_middle::ty; use rustc_middle::ty::{Ty, TyCtxt}; @@ -33,7 +33,7 @@ use smallvec::{SmallVec, smallvec}; use tracing::{debug, info, instrument, trace}; use crate::clean::utils::find_nearest_parent_module; -use crate::clean::{self, Crate, Item, ItemId, ItemLink, PrimitiveType, reexport_chain}; +use crate::clean::{self, Crate, Item, ItemId, ItemLink, PrimitiveType}; use crate::core::DocContext; use crate::html::markdown::{MarkdownLink, MarkdownLinkRange, markdown_links}; use crate::lint::{BROKEN_INTRA_DOC_LINKS, PRIVATE_INTRA_DOC_LINKS}; @@ -1129,10 +1129,9 @@ impl LinkCollector<'_, '_> { } // Also resolve links in the note text of `#[deprecated]`. - for attr in &item.attrs.other_attrs { - let Attribute::Parsed(AttributeKind::Deprecated { span: depr_span, deprecation }) = - attr - else { + for (attr, item_id) in item.attrs.other_attrs.iter().zip(item.attrs.other_attrs_src.iter()) + { + let Attribute::Parsed(AttributeKind::Deprecated { span: _, deprecation }) = attr else { continue; }; let Some(note_sym) = deprecation.note else { continue }; @@ -1146,23 +1145,7 @@ impl LinkCollector<'_, '_> { // When resolving an intra-doc link inside a deprecation note that is on an inlined // `use` statement, we need to use the `def_id` of the `use` statement, not the // inlined item. - // - let item_id = if let Some(inline_stmt_id) = item.inline_stmt_id { - let target_def_id = item.item_id.expect_def_id(); - reexport_chain(tcx, inline_stmt_id, target_def_id) - .iter() - .flat_map(|reexport| reexport.id()) - .find(|&reexport_def_id| { - find_attr!( - tcx, - reexport_def_id, - Deprecated { span, .. } if span == depr_span - ) - }) - .unwrap_or(target_def_id) - } else { - item.item_id.expect_def_id() - }; + let item_id = item_id.unwrap_or(item.item_id.expect_def_id()); try_insert_links(item_id, note) } } diff --git a/tests/rustdoc-ui/ice-intra-doc-link-in-depr-note-157210.rs b/tests/rustdoc-ui/ice-intra-doc-link-in-depr-note-157210.rs new file mode 100644 index 0000000000000..a25be7c0d98dc --- /dev/null +++ b/tests/rustdoc-ui/ice-intra-doc-link-in-depr-note-157210.rs @@ -0,0 +1,12 @@ +//@ check-pass + +pub mod inner { + // [123](a::b::c) + //#[deprecated = "[broken link](TypeAlias::x)"] + #[doc(no_inline)] + #[deprecated = "bar"] + pub use std::vec::Vec; + //#[deprecated = "[working link](std::range)"] + #[deprecated = "foo"] + pub use std::ops::Range; +} From 23cbcffd3bdce8fe77f5981ca47f9c837e4ad6b2 Mon Sep 17 00:00:00 2001 From: binarycat Date: Tue, 22 Sep 2026 13:49:35 -0400 Subject: [PATCH 2/3] clippy: update to account for new attrs_to_doc_fragments signature --- src/tools/clippy/clippy_lints/src/doc/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/clippy/clippy_lints/src/doc/mod.rs b/src/tools/clippy/clippy_lints/src/doc/mod.rs index 8d2eb01e8743f..fbd2d505ca0a0 100644 --- a/src/tools/clippy/clippy_lints/src/doc/mod.rs +++ b/src/tools/clippy/clippy_lints/src/doc/mod.rs @@ -876,7 +876,7 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet, attrs: &[ return None; } - let (fragments, _) = attrs_to_doc_fragments( + let (fragments, _, _) = attrs_to_doc_fragments( attrs.iter().filter_map(|attr| { if attr.doc_str_and_fragment_kind().is_none() || attr.span().in_external_macro(cx.sess().source_map()) { None From 7b3e93b30e5485a9413d151d50df6e21524c9e52 Mon Sep 17 00:00:00 2001 From: binarycat Date: Tue, 22 Sep 2026 14:52:42 -0400 Subject: [PATCH 3/3] update internal rustdoc unit test to account for new struct field --- src/librustdoc/clean/types/tests.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/librustdoc/clean/types/tests.rs b/src/librustdoc/clean/types/tests.rs index a0fc623c20c48..cebe0e30c9b68 100644 --- a/src/librustdoc/clean/types/tests.rs +++ b/src/librustdoc/clean/types/tests.rs @@ -20,7 +20,11 @@ fn run_test(input: &str, expected: &str) { create_default_session_globals_then(|| { let mut s = create_doc_fragment(input); unindent_doc_fragments(&mut s); - let attrs = Attributes { doc_strings: s, other_attrs: Default::default() }; + let attrs = Attributes { + doc_strings: s, + other_attrs: Default::default(), + other_attrs_src: Default::default(), + }; assert_eq!(attrs.doc_value(), expected); }); }