diff --git a/compiler/rustc_resolve/src/rustdoc.rs b/compiler/rustc_resolve/src/rustdoc.rs index ddf5600f76659..724097ef5540c 100644 --- a/compiler/rustc_resolve/src/rustdoc.rs +++ b/compiler/rustc_resolve/src/rustdoc.rs @@ -520,6 +520,24 @@ pub fn span_of_fragments(fragments: &[DocFragment]) -> Option { Some(first_fragment.span.to(last_fragment.span)) } +/// Matches a range of bytes from parsed markdown to the item it comes from. +/// +/// Returns `None` if the DocFragment itself has no attached `item_id`, +/// and, if that happens, the ID of the Item itself should be used. +pub fn item_defid_for_markdown_position( + mut md_pos: usize, + fragments: &[DocFragment], +) -> Option { + for frag in fragments { + let s = frag.doc.as_str(); + if md_pos <= s.len() { + return frag.item_id; + } + md_pos -= s.len() + } + None +} + /// Attempts to match a range of bytes from parsed markdown to a `Span` in the source code. /// /// This method does not always work, because markdown bytes don't necessarily match source bytes, diff --git a/src/librustdoc/lint.rs b/src/librustdoc/lint.rs index 5d8675aecb86a..4991a6c6830ea 100644 --- a/src/librustdoc/lint.rs +++ b/src/librustdoc/lint.rs @@ -220,6 +220,14 @@ declare_rustdoc_lint! { "detects unescaped pipe in table rows in doc comments" } +declare_rustdoc_lint! { + /// Detects if re-export doc comments are used in a way that might break. + /// This is a compatibility lint, and will eventually be removed. + UNPORTABLE_MARKDOWN, + Deny, + "detects Markdown that may parse differently in a new version" +} + pub(crate) static RUSTDOC_LINTS: Lazy> = Lazy::new(|| { vec![ BROKEN_INTRA_DOC_LINKS, @@ -236,6 +244,7 @@ pub(crate) static RUSTDOC_LINTS: Lazy> = Lazy::new(|| { BROKEN_FOOTNOTE, UNUSED_FOOTNOTE_DEFINITION, INVALID_MARKDOWN_TABLE, + UNPORTABLE_MARKDOWN, ] }); @@ -259,5 +268,4 @@ pub(crate) fn register_lints(_sess: &Session, lint_store: &mut LintStore) { .register_renamed("intra_doc_link_resolution_failure", "rustdoc::broken_intra_doc_links"); lint_store.register_renamed("non_autolinks", "rustdoc::bare_urls"); lint_store.register_renamed("rustdoc::non_autolinks", "rustdoc::bare_urls"); - lint_store.register_removed("rustdoc::unportable_markdown", "old parser removed"); } diff --git a/src/librustdoc/passes/lint.rs b/src/librustdoc/passes/lint.rs index 9988aa683d6d6..b825b6c271797 100644 --- a/src/librustdoc/passes/lint.rs +++ b/src/librustdoc/passes/lint.rs @@ -7,6 +7,7 @@ mod html_tags; mod invalid_markdown_table; mod redundant_explicit_links; mod unescaped_backticks; +mod unportable_markdown; use crate::clean::*; use crate::core::DocContext; @@ -51,6 +52,9 @@ impl DocVisitor<'_> for Linter<'_, '_> { if may_have_table { invalid_markdown_table::visit_item(self.cx, item, hir_id, &dox); } + if item.inner.attrs.doc_strings.iter().any(|frag| frag.item_id.is_some()) { + unportable_markdown::visit_item(self.cx, item, hir_id, &dox); + } } self.visit_item_recur(item) diff --git a/src/librustdoc/passes/lint/unportable_markdown.rs b/src/librustdoc/passes/lint/unportable_markdown.rs new file mode 100644 index 0000000000000..ee77614de3e4e --- /dev/null +++ b/src/librustdoc/passes/lint/unportable_markdown.rs @@ -0,0 +1,105 @@ +use rustc_errors::{Diag, DiagDecorator}; +use rustc_hir::HirId; +use rustc_resolve::rustdoc::pulldown_cmark::{Event, LinkType, Parser, Tag}; +use rustc_resolve::rustdoc::{item_defid_for_markdown_position, source_span_for_markdown_range}; + +use crate::clean::Item; +use crate::core::DocContext; +use crate::html::markdown::main_body_opts; + +pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: &str) { + let Some(span) = item.span(cx.tcx) else { return }; + let mut p = Parser::new_ext(dox, main_body_opts()).into_offset_iter(); + while let Some((event, range)) = p.next() { + let span = source_span_for_markdown_range( + cx.tcx, + &dox, + &(range.start..range.start + 1), + &item.attrs.doc_strings, + ) + .map(|(span, _)| span) + .unwrap_or(span.inner()); + let item_id_start = item_defid_for_markdown_position(range.start, &item.attrs.doc_strings); + let item_id_end = item_defid_for_markdown_position(range.end - 1, &item.attrs.doc_strings); + if item_id_start != item_id_end { + cx.tcx.emit_node_span_lint( + crate::lint::UNPORTABLE_MARKDOWN, + hir_id, + span, + DiagDecorator(|lint: &mut Diag<'_, ()>| { + lint.primary_message("markdown element starts on one item and ends on another"); + lint.help("the way this is parsed might change in the future"); + report_idx(cx, item, dox, range.start, "starts", lint); + report_idx(cx, item, dox, range.end - 1, "ends", lint); + }), + ); + } else if let Event::Start(Tag::Link { + link_type: + LinkType::Reference + | LinkType::ReferenceUnknown + | LinkType::Collapsed + | LinkType::CollapsedUnknown + | LinkType::Shortcut + | LinkType::ShortcutUnknown, + id, + .. + }) = event + { + if let Some(refdef) = p.reference_definitions().get(&id[..]) { + let item_id_refdef_start = + item_defid_for_markdown_position(refdef.span.start, &item.attrs.doc_strings); + let item_id_refdef_end = + item_defid_for_markdown_position(refdef.span.end - 1, &item.attrs.doc_strings); + if item_id_refdef_start != item_id_start { + cx.tcx.emit_node_span_lint( + crate::lint::UNPORTABLE_MARKDOWN, + hir_id, + span, + DiagDecorator(|lint: &mut Diag<'_, ()>| { + lint.primary_message( + "markdown link and refdef are defined on different items", + ); + lint.help("the way this is parsed might change in the future"); + report_idx(cx, item, dox, refdef.span.start, "refdef starts", lint); + report_idx(cx, item, dox, range.start, "item starts", lint); + }), + ); + } else if item_id_refdef_end != item_id_start { + cx.tcx.emit_node_span_lint( + crate::lint::UNPORTABLE_MARKDOWN, + hir_id, + span, + DiagDecorator(|lint: &mut Diag<'_, ()>| { + lint.primary_message( + "markdown link and refdef are defined on different items", + ); + lint.help("the way this is parsed might change in the future"); + report_idx(cx, item, dox, refdef.span.end - 1, "refdef ends", lint); + report_idx(cx, item, dox, range.start, "item starts", lint); + }), + ); + } + } + } + } +} + +fn report_idx( + cx: &DocContext<'_>, + item: &Item, + dox: &str, + idx: usize, + verb: &'static str, + lint: &mut Diag<'_, ()>, +) { + if let Some((span, _)) = + source_span_for_markdown_range(cx.tcx, &dox, &(idx..idx + 1), &item.attrs.doc_strings) + { + lint.span_label(span, format!("{verb} here")); + } else { + let line_start = dox[..idx].rfind('\n').map_or(0, |i| i + 1); + let line_end = dox[idx..].find('\n').map_or(dox.len(), |i| i + idx); + let line = &dox[line_start..line_end]; + lint.help(format!("{verb} near `{line}`")); + } +} diff --git a/tests/rustdoc-html/attributes-inlining-108281.rs b/tests/rustdoc-html/attributes-inlining-108281.rs index 9dfaf1a6846f3..112f662f54355 100644 --- a/tests/rustdoc-html/attributes-inlining-108281.rs +++ b/tests/rustdoc-html/attributes-inlining-108281.rs @@ -2,6 +2,7 @@ // It ensures that the attributes on the first reexport are not duplicated. #![crate_name = "foo"] +#![allow(rustdoc::unportable_markdown)] //@ has 'foo/index.html' diff --git a/tests/rustdoc-html/intra-doc/nested-use.rs b/tests/rustdoc-html/intra-doc/nested-use.rs index 2aaaed196fd12..88a1bc7f2a52f 100644 --- a/tests/rustdoc-html/intra-doc/nested-use.rs +++ b/tests/rustdoc-html/intra-doc/nested-use.rs @@ -1,6 +1,7 @@ // Regression test for issue #113896: Intra-doc links on nested use items. #![crate_name = "foo"] +#![allow(rustdoc::unportable_markdown)] //@ has foo/struct.Foo.html //@ has - '//a[@href="struct.Foo.html"]' 'Foo' diff --git a/tests/rustdoc-html/multiple-import-levels.rs b/tests/rustdoc-html/multiple-import-levels.rs index ac00f1fbdfc24..2fb0596814883 100644 --- a/tests/rustdoc-html/multiple-import-levels.rs +++ b/tests/rustdoc-html/multiple-import-levels.rs @@ -2,6 +2,7 @@ // account. #![crate_name = "foo"] +#![allow(rustdoc::unportable_markdown)] mod a { /// 1 diff --git a/tests/rustdoc-html/private/inline-private-with-intermediate-doc-hidden.rs b/tests/rustdoc-html/private/inline-private-with-intermediate-doc-hidden.rs index d27ecbad1690e..f3ef9e2d440d8 100644 --- a/tests/rustdoc-html/private/inline-private-with-intermediate-doc-hidden.rs +++ b/tests/rustdoc-html/private/inline-private-with-intermediate-doc-hidden.rs @@ -3,6 +3,7 @@ // from the doc hidden re-export. #![crate_name = "foo"] +#![allow(rustdoc::unportable_markdown)] //@ has 'foo/index.html' // There should only be one struct displayed. diff --git a/tests/rustdoc-html/reexport/doc-hidden-reexports-109449.rs b/tests/rustdoc-html/reexport/doc-hidden-reexports-109449.rs index 78b9214300a95..d5b0504f4b05d 100644 --- a/tests/rustdoc-html/reexport/doc-hidden-reexports-109449.rs +++ b/tests/rustdoc-html/reexport/doc-hidden-reexports-109449.rs @@ -2,6 +2,7 @@ // . #![crate_name = "foo"] +#![allow(rustdoc::unportable_markdown)] mod private_module { #[doc(hidden)] diff --git a/tests/rustdoc-html/reexport/extern-135092.rs b/tests/rustdoc-html/reexport/extern-135092.rs index fb5c71d56d595..b61a6a6ffa266 100644 --- a/tests/rustdoc-html/reexport/extern-135092.rs +++ b/tests/rustdoc-html/reexport/extern-135092.rs @@ -2,6 +2,7 @@ // #![crate_name = "foo"] +#![allow(rustdoc::unportable_markdown)] mod native { extern "C" { diff --git a/tests/rustdoc-html/reexport/local-reexport-doc.rs b/tests/rustdoc-html/reexport/local-reexport-doc.rs index 6db8f999d29f6..2e53aacc17e31 100644 --- a/tests/rustdoc-html/reexport/local-reexport-doc.rs +++ b/tests/rustdoc-html/reexport/local-reexport-doc.rs @@ -2,6 +2,7 @@ // the reexport. #![crate_name = "foo"] +#![allow(rustdoc::unportable_markdown)] //@ has 'foo/fn.g.html' //@ has - '//*[@class="toggle top-doc"]/*[@class="docblock"]' \ diff --git a/tests/rustdoc-html/reexport/merge-glob-and-non-glob.rs b/tests/rustdoc-html/reexport/merge-glob-and-non-glob.rs index ff67859cb39af..6056fe70181f3 100644 --- a/tests/rustdoc-html/reexport/merge-glob-and-non-glob.rs +++ b/tests/rustdoc-html/reexport/merge-glob-and-non-glob.rs @@ -6,6 +6,7 @@ #![no_core] #![no_std] #![crate_name = "foo"] +#![allow(rustdoc::unportable_markdown)] // First we ensure we only have two items. //@ has 'foo/index.html' diff --git a/tests/rustdoc-ui/intra-doc/import-inline-merge.rs b/tests/rustdoc-ui/intra-doc/import-inline-merge.rs index 9307ec23b1e65..a15b835e7d6f5 100644 --- a/tests/rustdoc-ui/intra-doc/import-inline-merge.rs +++ b/tests/rustdoc-ui/intra-doc/import-inline-merge.rs @@ -3,6 +3,7 @@ //@ check-pass +#![allow(rustdoc::unportable_markdown)] #![allow(rustdoc::private_intra_doc_links)] mod m { diff --git a/tests/rustdoc-ui/lints/unportable-markdown-item-id.rs b/tests/rustdoc-ui/lints/unportable-markdown-item-id.rs new file mode 100644 index 0000000000000..11eca86563b5c --- /dev/null +++ b/tests/rustdoc-ui/lints/unportable-markdown-item-id.rs @@ -0,0 +1,17 @@ +#![deny(rustdoc::unportable_markdown)] +// invalid_html is buggy in this case +#![allow(rustdoc::invalid_html_tags)] + +/// [doc.example]: https://example.com +/// +/// - bar +//~^ ERROR +//~| ERROR +#[doc(inline)] +pub use foo::First; + +pub mod foo { +/// - My [doc.example] +//~^ ERROR +pub struct First; +} diff --git a/tests/rustdoc-ui/lints/unportable-markdown-item-id.stderr b/tests/rustdoc-ui/lints/unportable-markdown-item-id.stderr new file mode 100644 index 0000000000000..973e3c2809be6 --- /dev/null +++ b/tests/rustdoc-ui/lints/unportable-markdown-item-id.stderr @@ -0,0 +1,41 @@ +error: markdown element starts on one item and ends on another + --> $DIR/unportable-markdown-item-id.rs:7:5 + | +LL | /// - bar + | ^ starts here +... +LL | /// - My [doc.example] + | - ends here + | + = help: the way this is parsed might change in the future +note: the lint level is defined here + --> $DIR/unportable-markdown-item-id.rs:1:9 + | +LL | #![deny(rustdoc::unportable_markdown)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: markdown link and refdef are defined on different items + --> $DIR/unportable-markdown-item-id.rs:14:10 + | +LL | /// [doc.example]: https://example.com + | - refdef starts here +... +LL | /// - My [doc.example] + | ^ item starts here + | + = help: the way this is parsed might change in the future + +error: markdown element starts on one item and ends on another + --> $DIR/unportable-markdown-item-id.rs:7:5 + | +LL | /// - bar + | ^ starts here +... +LL | /// - My [doc.example] + | - ends here + | + = help: the way this is parsed might change in the future + = note: duplicate diagnostic emitted due to `-Z deduplicate-diagnostics=no` + +error: aborting due to 3 previous errors +