render intra-doc links in the #[deprectated] note
This commit is contained in:
parent
64c78f6e74
commit
3be74a7441
11 changed files with 179 additions and 22 deletions
|
|
@ -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),
|
||||
|
|
@ -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<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()
|
||||
|
|
|
|||
|
|
@ -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(..)))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ pub(crate) struct MarkdownWithToc<'a> {
|
|||
/// and includes no paragraph tags.
|
||||
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.
|
||||
|
|
@ -1463,18 +1464,27 @@ impl MarkdownWithToc<'_> {
|
|||
}
|
||||
|
||||
impl<'a> MarkdownItemInfo<'a> {
|
||||
pub(crate) fn new(content: &'a str, ids: &'a mut IdMap) -> Self {
|
||||
Self { content, ids }
|
||||
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 { content, ids } = self;
|
||||
let MarkdownItemInfo { content: md, links, ids } = self;
|
||||
|
||||
// This is actually common enough to special-case
|
||||
if content.is_empty() {
|
||||
if md.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let p = Parser::new_ext(content, 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 {
|
||||
|
|
@ -1484,6 +1494,7 @@ impl<'a> MarkdownItemInfo<'a> {
|
|||
|
||||
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| {
|
||||
|
|
|
|||
|
|
@ -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::new(input, &mut idmap).write_into(&mut output).unwrap();
|
||||
MarkdownItemInfo::new(input, &[], &mut idmap).write_into(&mut output).unwrap();
|
||||
assert_eq!(output, expect, "original: {}", input);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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::new(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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
12
tests/rustdoc-html/intra-doc/deprecated.rs
Normal file
12
tests/rustdoc-html/intra-doc/deprecated.rs
Normal file
|
|
@ -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
tests/rustdoc-ui/intra-doc/deprecated.rs
Normal file
10
tests/rustdoc-ui/intra-doc/deprecated.rs
Normal file
|
|
@ -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
tests/rustdoc-ui/intra-doc/deprecated.stderr
Normal file
43
tests/rustdoc-ui/intra-doc/deprecated.stderr
Normal file
|
|
@ -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…
Add table
Add a link
Reference in a new issue