Skip to content
Merged
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
34 changes: 34 additions & 0 deletions compiler/rustc_ast/src/attr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,34 @@ impl AttributeExt for Attribute {
}
}

fn deprecation_note(&self) -> Option<Symbol> {
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<AttrStyle> {
match &self.kind {
AttrKind::DocComment(..) => Some(self.style),
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -873,6 +902,11 @@ pub trait AttributeExt: Debug {
/// * `#[doc(...)]` returns `None`.
fn doc_str(&self) -> Option<Symbol>;

/// 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<Symbol>;

fn is_proc_macro_attr(&self) -> bool {
[sym::proc_macro, sym::proc_macro_attribute, sym::proc_macro_derive]
.iter()
Expand Down
8 changes: 8 additions & 0 deletions compiler/rustc_hir/src/hir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1400,6 +1400,14 @@ impl AttributeExt for Attribute {
}
}

#[inline]
fn deprecation_note(&self) -> Option<Symbol> {
match &self {
Attribute::Parsed(AttributeKind::Deprecation { deprecation, .. }) => deprecation.note,
_ => None,
}
}

fn is_automatically_derived_attr(&self) -> bool {
matches!(self, Attribute::Parsed(AttributeKind::AutomaticallyDerived(..)))
}
Expand Down
13 changes: 11 additions & 2 deletions compiler/rustc_resolve/src/rustdoc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<A: AttributeExt + Clone>(attrs: &[A]) -> Vec<Box<str>> {
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);
Comment on lines +413 to +414
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the final argument switched from true to false here, which keeps the other attributes. That probably has perf consequences, but I did not want to just make up a doc fragment for the deprecation note.

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)
}
Expand Down
10 changes: 10 additions & 0 deletions src/librustdoc/clean/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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()))
}

Expand Down
26 changes: 22 additions & 4 deletions src/librustdoc/html/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]);

Expand Down Expand Up @@ -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 {
Expand All @@ -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| {
Expand Down
2 changes: 1 addition & 1 deletion src/librustdoc/html/markdown/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
3 changes: 2 additions & 1 deletion src/librustdoc/html/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
45 changes: 32 additions & 13 deletions src/librustdoc/passes/collect_intra_doc_links.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(),
Expand All @@ -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)
}
}

Expand All @@ -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,
Expand Down
12 changes: 12 additions & 0 deletions tests/rustdoc-html/intra-doc/deprecated.rs
Original file line number Diff line number Diff line change
@@ -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;
10 changes: 10 additions & 0 deletions tests/rustdoc-ui/intra-doc/deprecated.rs
Original file line number Diff line number Diff line change
@@ -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;
43 changes: 43 additions & 0 deletions tests/rustdoc-ui/intra-doc/deprecated.stderr
Original file line number Diff line number Diff line change
@@ -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

Loading