Skip to content
Closed
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
18 changes: 18 additions & 0 deletions compiler/rustc_resolve/src/rustdoc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,24 @@ pub fn span_of_fragments(fragments: &[DocFragment]) -> Option<Span> {
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<DefId> {
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,
Expand Down
10 changes: 9 additions & 1 deletion src/librustdoc/lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<&'static Lint>> = Lazy::new(|| {
vec![
BROKEN_INTRA_DOC_LINKS,
Expand All @@ -236,6 +244,7 @@ pub(crate) static RUSTDOC_LINTS: Lazy<Vec<&'static Lint>> = Lazy::new(|| {
BROKEN_FOOTNOTE,
UNUSED_FOOTNOTE_DEFINITION,
INVALID_MARKDOWN_TABLE,
UNPORTABLE_MARKDOWN,
]
});

Expand All @@ -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");
}
4 changes: 4 additions & 0 deletions src/librustdoc/passes/lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
105 changes: 105 additions & 0 deletions src/librustdoc/passes/lint/unportable_markdown.rs
Original file line number Diff line number Diff line change
@@ -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}`"));
}
}
1 change: 1 addition & 0 deletions tests/rustdoc-html/attributes-inlining-108281.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
1 change: 1 addition & 0 deletions tests/rustdoc-html/intra-doc/nested-use.rs
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
1 change: 1 addition & 0 deletions tests/rustdoc-html/multiple-import-levels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// account.

#![crate_name = "foo"]
#![allow(rustdoc::unportable_markdown)]

mod a {
/// 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions tests/rustdoc-html/reexport/doc-hidden-reexports-109449.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// <https://github.com/rust-lang/rust/issues/109449>.

#![crate_name = "foo"]
#![allow(rustdoc::unportable_markdown)]

mod private_module {
#[doc(hidden)]
Expand Down
1 change: 1 addition & 0 deletions tests/rustdoc-html/reexport/extern-135092.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// <https://github.com/rust-lang/rust/issues/135092>

#![crate_name = "foo"]
#![allow(rustdoc::unportable_markdown)]

mod native {
extern "C" {
Expand Down
1 change: 1 addition & 0 deletions tests/rustdoc-html/reexport/local-reexport-doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"]' \
Expand Down
1 change: 1 addition & 0 deletions tests/rustdoc-html/reexport/merge-glob-and-non-glob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions tests/rustdoc-ui/intra-doc/import-inline-merge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

//@ check-pass

#![allow(rustdoc::unportable_markdown)]
#![allow(rustdoc::private_intra_doc_links)]

mod m {
Expand Down
17 changes: 17 additions & 0 deletions tests/rustdoc-ui/lints/unportable-markdown-item-id.rs
Original file line number Diff line number Diff line change
@@ -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;
}
41 changes: 41 additions & 0 deletions tests/rustdoc-ui/lints/unportable-markdown-item-id.stderr
Original file line number Diff line number Diff line change
@@ -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

Loading