From 1740c931380b61ce16d52e69086a1c9ddc9d8c0e Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 11 Mar 2025 14:54:18 +0100 Subject: [PATCH 1/5] Add new rustdoc `broken_footnote` lint --- src/librustdoc/lint.rs | 8 +++ src/librustdoc/passes/lint.rs | 2 + src/librustdoc/passes/lint/footnotes.rs | 66 +++++++++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 src/librustdoc/passes/lint/footnotes.rs diff --git a/src/librustdoc/lint.rs b/src/librustdoc/lint.rs index b09ea05688595..1e27bd03456e0 100644 --- a/src/librustdoc/lint.rs +++ b/src/librustdoc/lint.rs @@ -196,6 +196,13 @@ declare_rustdoc_lint! { "detects redundant explicit links in doc comments" } +declare_rustdoc_lint! { + /// This lint checks for uses of footnote references without definition. + BROKEN_FOOTNOTE, + Warn, + "footnote reference with no associated definition" +} + pub(crate) static RUSTDOC_LINTS: Lazy> = Lazy::new(|| { vec![ BROKEN_INTRA_DOC_LINKS, @@ -209,6 +216,7 @@ pub(crate) static RUSTDOC_LINTS: Lazy> = Lazy::new(|| { MISSING_CRATE_LEVEL_DOCS, UNESCAPED_BACKTICKS, REDUNDANT_EXPLICIT_LINKS, + BROKEN_FOOTNOTE, ] }); diff --git a/src/librustdoc/passes/lint.rs b/src/librustdoc/passes/lint.rs index 7740d14148bf0..bb952b32393cf 100644 --- a/src/librustdoc/passes/lint.rs +++ b/src/librustdoc/passes/lint.rs @@ -3,6 +3,7 @@ mod bare_urls; mod check_code_block_syntax; +mod footnotes; mod html_tags; mod redundant_explicit_links; mod unescaped_backticks; @@ -41,6 +42,7 @@ impl DocVisitor<'_> for Linter<'_, '_> { if may_have_link { bare_urls::visit_item(self.cx, item, hir_id, &dox); redundant_explicit_links::visit_item(self.cx, item, hir_id); + footnotes::visit_item(self.cx, item, hir_id, &dox); } if may_have_code { check_code_block_syntax::visit_item(self.cx, item, &dox); diff --git a/src/librustdoc/passes/lint/footnotes.rs b/src/librustdoc/passes/lint/footnotes.rs new file mode 100644 index 0000000000000..dd15b6bdfbe3c --- /dev/null +++ b/src/librustdoc/passes/lint/footnotes.rs @@ -0,0 +1,66 @@ +//! Detects specific markdown syntax that's different between pulldown-cmark +//! 0.9 and 0.11. +//! +//! This is a mitigation for old parser bugs that affected some +//! real crates' docs. The old parser claimed to comply with CommonMark, +//! but it did not. These warnings will eventually be removed, +//! though some of them may become Clippy lints. +//! +//! +//! +//! + +use std::ops::Range; + +use pulldown_cmark::{Event, Options, Parser}; +use rustc_data_structures::fx::FxHashSet; +use rustc_hir::HirId; +use rustc_lint_defs::Applicability; +use rustc_resolve::rustdoc::source_span_for_markdown_range; + +use crate::clean::Item; +use crate::core::DocContext; + +pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: &str) { + let tcx = cx.tcx; + + let mut missing_footnote_references = FxHashSet::default(); + + let options = Options::ENABLE_FOOTNOTES; + let mut parser = Parser::new_ext(dox, options).into_offset_iter().peekable(); + while let Some((event, span)) = parser.next() { + match event { + Event::Text(text) + if &*text == "[" + && let Some((Event::Text(text), _)) = parser.peek() + && text.trim_start().starts_with('^') + && parser.next().is_some() + && let Some((Event::Text(text), end_span)) = parser.peek() + && &**text == "]" => + { + missing_footnote_references.insert(Range { start: span.start, end: end_span.end }); + } + _ => {} + } + } + + #[allow(rustc::potential_query_instability)] + for span in missing_footnote_references { + let (ref_span, precise) = + source_span_for_markdown_range(tcx, dox, &span, &item.attrs.doc_strings) + .map(|span| (span, true)) + .unwrap_or_else(|| (item.attr_span(tcx), false)); + + if precise { + tcx.node_span_lint(crate::lint::BROKEN_FOOTNOTE, hir_id, ref_span, |lint| { + lint.primary_message("no footnote definition matching this footnote"); + lint.span_suggestion( + ref_span.shrink_to_lo(), + "if it should not be a footnote, escape it", + "\\", + Applicability::MaybeIncorrect, + ); + }); + } + } +} From 66dc579bf201e55ec0a5786abebab9fcfd19538b Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 11 Mar 2025 14:58:06 +0100 Subject: [PATCH 2/5] Add ui test for rustdoc `broken_footnote` lint --- tests/rustdoc-ui/lints/broken-footnote.rs | 8 +++++++ tests/rustdoc-ui/lints/broken-footnote.stderr | 24 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 tests/rustdoc-ui/lints/broken-footnote.rs create mode 100644 tests/rustdoc-ui/lints/broken-footnote.stderr diff --git a/tests/rustdoc-ui/lints/broken-footnote.rs b/tests/rustdoc-ui/lints/broken-footnote.rs new file mode 100644 index 0000000000000..b32c9f3db9481 --- /dev/null +++ b/tests/rustdoc-ui/lints/broken-footnote.rs @@ -0,0 +1,8 @@ +#![deny(rustdoc::broken_footnote)] +#![allow(rustdoc::unportable_markdown)] + +//! Footnote referenced [^1]. And [^2]. And [^bla]. +//! +//! [^1]: footnote defined +//~^^^ ERROR: no footnote definition matching this footnote +//~| ERROR: no footnote definition matching this footnote diff --git a/tests/rustdoc-ui/lints/broken-footnote.stderr b/tests/rustdoc-ui/lints/broken-footnote.stderr new file mode 100644 index 0000000000000..a039135aef669 --- /dev/null +++ b/tests/rustdoc-ui/lints/broken-footnote.stderr @@ -0,0 +1,24 @@ +error: no footnote definition matching this footnote + --> $DIR/broken-footnote.rs:4:45 + | +LL | //! Footnote referenced [^1]. And [^2]. And [^bla]. + | -^^^^^ + | | + | help: if it should not be a footnote, escape it: `\` + | +note: the lint level is defined here + --> $DIR/broken-footnote.rs:1:9 + | +LL | #![deny(rustdoc::broken_footnote)] + | ^^^^^^^^^^^^^^^^^^^^^^^^ + +error: no footnote definition matching this footnote + --> $DIR/broken-footnote.rs:4:35 + | +LL | //! Footnote referenced [^1]. And [^2]. And [^bla]. + | -^^^ + | | + | help: if it should not be a footnote, escape it: `\` + +error: aborting due to 2 previous errors + From e0d8136a87bdd983082787b8da51878f3dae55e7 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 11 Mar 2025 15:10:40 +0100 Subject: [PATCH 3/5] Add new `unused_footnote_definition` rustdoc lint --- src/librustdoc/lint.rs | 8 +++++++ src/librustdoc/passes/lint/footnotes.rs | 30 ++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/librustdoc/lint.rs b/src/librustdoc/lint.rs index 1e27bd03456e0..f2b9ede415c84 100644 --- a/src/librustdoc/lint.rs +++ b/src/librustdoc/lint.rs @@ -203,6 +203,13 @@ declare_rustdoc_lint! { "footnote reference with no associated definition" } +declare_rustdoc_lint! { + /// This lint checks if all footnote definitions are used. + UNUSED_FOOTNOTE_DEFINITION, + Warn, + "unused footnote definition" +} + pub(crate) static RUSTDOC_LINTS: Lazy> = Lazy::new(|| { vec![ BROKEN_INTRA_DOC_LINKS, @@ -217,6 +224,7 @@ pub(crate) static RUSTDOC_LINTS: Lazy> = Lazy::new(|| { UNESCAPED_BACKTICKS, REDUNDANT_EXPLICIT_LINKS, BROKEN_FOOTNOTE, + UNUSED_FOOTNOTE_DEFINITION, ] }); diff --git a/src/librustdoc/passes/lint/footnotes.rs b/src/librustdoc/passes/lint/footnotes.rs index dd15b6bdfbe3c..b76487b6aa7da 100644 --- a/src/librustdoc/passes/lint/footnotes.rs +++ b/src/librustdoc/passes/lint/footnotes.rs @@ -12,10 +12,10 @@ use std::ops::Range; -use pulldown_cmark::{Event, Options, Parser}; -use rustc_data_structures::fx::FxHashSet; +use rustc_data_structures::fx::{FxHashMap, FxHashSet}; use rustc_hir::HirId; use rustc_lint_defs::Applicability; +use rustc_resolve::rustdoc::pulldown_cmark::{Event, Options, Parser, Tag}; use rustc_resolve::rustdoc::source_span_for_markdown_range; use crate::clean::Item; @@ -25,6 +25,8 @@ pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: & let tcx = cx.tcx; let mut missing_footnote_references = FxHashSet::default(); + let mut footnote_references = FxHashSet::default(); + let mut footnote_definitions = FxHashMap::default(); let options = Options::ENABLE_FOOTNOTES; let mut parser = Parser::new_ext(dox, options).into_offset_iter().peekable(); @@ -40,15 +42,37 @@ pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: & { missing_footnote_references.insert(Range { start: span.start, end: end_span.end }); } + Event::FootnoteReference(label) => { + footnote_references.insert(label); + } + Event::Start(Tag::FootnoteDefinition(label)) => { + footnote_definitions.insert(label, span.start + 1); + } _ => {} } } + #[allow(rustc::potential_query_instability)] + for (footnote, span) in footnote_definitions { + if !footnote_references.contains(&footnote) { + let (span, _) = source_span_for_markdown_range( + tcx, + dox, + &(span..span + 1), + &item.attrs.doc_strings, + ) + .unwrap_or_else(|| (item.attr_span(tcx), false)); + + tcx.node_span_lint(crate::lint::UNUSED_FOOTNOTE_DEFINITION, hir_id, span, |lint| { + lint.primary_message("unused footnote definition"); + }); + } + } + #[allow(rustc::potential_query_instability)] for span in missing_footnote_references { let (ref_span, precise) = source_span_for_markdown_range(tcx, dox, &span, &item.attrs.doc_strings) - .map(|span| (span, true)) .unwrap_or_else(|| (item.attr_span(tcx), false)); if precise { From 340653335ac6699155c840118af58f62cf002ada Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Tue, 11 Mar 2025 15:10:54 +0100 Subject: [PATCH 4/5] Add ui test for new `unused_footnote_definition` rustdoc lint --- tests/rustdoc-ui/lints/unused-footnote.rs | 9 +++++++++ tests/rustdoc-ui/lints/unused-footnote.stderr | 14 ++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 tests/rustdoc-ui/lints/unused-footnote.rs create mode 100644 tests/rustdoc-ui/lints/unused-footnote.stderr diff --git a/tests/rustdoc-ui/lints/unused-footnote.rs b/tests/rustdoc-ui/lints/unused-footnote.rs new file mode 100644 index 0000000000000..059be7da402de --- /dev/null +++ b/tests/rustdoc-ui/lints/unused-footnote.rs @@ -0,0 +1,9 @@ +// This test ensures that the rustdoc `unused_footnote` is working as expected. + +#![deny(rustdoc::unused_footnote_definition)] + +//! Footnote referenced. [^2] +//! +//! [^1]: footnote defined +//! [^2]: footnote defined +//~^^ unused_footnote_definition diff --git a/tests/rustdoc-ui/lints/unused-footnote.stderr b/tests/rustdoc-ui/lints/unused-footnote.stderr new file mode 100644 index 0000000000000..d227cef181df3 --- /dev/null +++ b/tests/rustdoc-ui/lints/unused-footnote.stderr @@ -0,0 +1,14 @@ +error: unused footnote definition + --> $DIR/unused-footnote.rs:7:6 + | +LL | //! [^1]: footnote defined + | ^ + | +note: the lint level is defined here + --> $DIR/unused-footnote.rs:3:9 + | +LL | #![deny(rustdoc::unused_footnote_definition)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: aborting due to 1 previous error + From 2cdc54b89d5c75ed0342c874589df91e43bddab0 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 26 Jan 2026 12:05:56 +0100 Subject: [PATCH 5/5] Improve description of new rustdoc lints --- src/librustdoc/lint.rs | 4 ++-- src/librustdoc/passes/lint/footnotes.rs | 22 +++++++++---------- tests/rustdoc-ui/lints/broken-footnote.rs | 1 - tests/rustdoc-ui/lints/broken-footnote.stderr | 4 ++-- tests/rustdoc-ui/lints/unused-footnote.rs | 4 ++-- 5 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/librustdoc/lint.rs b/src/librustdoc/lint.rs index f2b9ede415c84..c1e6d067b1977 100644 --- a/src/librustdoc/lint.rs +++ b/src/librustdoc/lint.rs @@ -200,14 +200,14 @@ declare_rustdoc_lint! { /// This lint checks for uses of footnote references without definition. BROKEN_FOOTNOTE, Warn, - "footnote reference with no associated definition" + "detects footnote references with no associated definition" } declare_rustdoc_lint! { /// This lint checks if all footnote definitions are used. UNUSED_FOOTNOTE_DEFINITION, Warn, - "unused footnote definition" + "detects unused footnote definitions" } pub(crate) static RUSTDOC_LINTS: Lazy> = Lazy::new(|| { diff --git a/src/librustdoc/passes/lint/footnotes.rs b/src/librustdoc/passes/lint/footnotes.rs index b76487b6aa7da..c09740e2ef325 100644 --- a/src/librustdoc/passes/lint/footnotes.rs +++ b/src/librustdoc/passes/lint/footnotes.rs @@ -71,20 +71,18 @@ pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: & #[allow(rustc::potential_query_instability)] for span in missing_footnote_references { - let (ref_span, precise) = + let (ref_span, _) = source_span_for_markdown_range(tcx, dox, &span, &item.attrs.doc_strings) .unwrap_or_else(|| (item.attr_span(tcx), false)); - if precise { - tcx.node_span_lint(crate::lint::BROKEN_FOOTNOTE, hir_id, ref_span, |lint| { - lint.primary_message("no footnote definition matching this footnote"); - lint.span_suggestion( - ref_span.shrink_to_lo(), - "if it should not be a footnote, escape it", - "\\", - Applicability::MaybeIncorrect, - ); - }); - } + tcx.node_span_lint(crate::lint::BROKEN_FOOTNOTE, hir_id, ref_span, |lint| { + lint.primary_message("no footnote definition matching this footnote"); + lint.span_suggestion( + ref_span.shrink_to_lo(), + "if it should not be a footnote, escape it", + "\\", + Applicability::MaybeIncorrect, + ); + }); } } diff --git a/tests/rustdoc-ui/lints/broken-footnote.rs b/tests/rustdoc-ui/lints/broken-footnote.rs index b32c9f3db9481..ef030d0e14999 100644 --- a/tests/rustdoc-ui/lints/broken-footnote.rs +++ b/tests/rustdoc-ui/lints/broken-footnote.rs @@ -1,5 +1,4 @@ #![deny(rustdoc::broken_footnote)] -#![allow(rustdoc::unportable_markdown)] //! Footnote referenced [^1]. And [^2]. And [^bla]. //! diff --git a/tests/rustdoc-ui/lints/broken-footnote.stderr b/tests/rustdoc-ui/lints/broken-footnote.stderr index a039135aef669..0d63ab8f01513 100644 --- a/tests/rustdoc-ui/lints/broken-footnote.stderr +++ b/tests/rustdoc-ui/lints/broken-footnote.stderr @@ -1,5 +1,5 @@ error: no footnote definition matching this footnote - --> $DIR/broken-footnote.rs:4:45 + --> $DIR/broken-footnote.rs:3:45 | LL | //! Footnote referenced [^1]. And [^2]. And [^bla]. | -^^^^^ @@ -13,7 +13,7 @@ LL | #![deny(rustdoc::broken_footnote)] | ^^^^^^^^^^^^^^^^^^^^^^^^ error: no footnote definition matching this footnote - --> $DIR/broken-footnote.rs:4:35 + --> $DIR/broken-footnote.rs:3:35 | LL | //! Footnote referenced [^1]. And [^2]. And [^bla]. | -^^^ diff --git a/tests/rustdoc-ui/lints/unused-footnote.rs b/tests/rustdoc-ui/lints/unused-footnote.rs index 059be7da402de..368902df8c526 100644 --- a/tests/rustdoc-ui/lints/unused-footnote.rs +++ b/tests/rustdoc-ui/lints/unused-footnote.rs @@ -1,4 +1,4 @@ -// This test ensures that the rustdoc `unused_footnote` is working as expected. +// This test ensures that the `rustdoc::unused_footnote` lint is working as expected. #![deny(rustdoc::unused_footnote_definition)] @@ -6,4 +6,4 @@ //! //! [^1]: footnote defined //! [^2]: footnote defined -//~^^ unused_footnote_definition +//~^^ ERROR: unused footnote definition