-
-
Notifications
You must be signed in to change notification settings - Fork 14.8k
Add new unused_footnote_definition rustdoc lint
#137858
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
963746f
e7b4bc8
6b500aa
621d984
4ddd48c
5deff4b
d33610f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| use std::ops::Range; | ||
|
|
||
| use rustc_data_structures::fx::{FxHashMap, FxHashSet}; | ||
| use rustc_errors::DiagDecorator; | ||
| 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; | ||
| 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 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(); | ||
| 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 == "]" => | ||
|
Comment on lines
+25
to
+30
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This parsing logic doesn't fully account for backslashes or special characters. This test case fails (it's a false positive, because it produces a warning when it shouldn't): /// Backslash escaped footnotes should not be recognized:
///
/// [\^4]
///
/// [^5\]
pub struct BackslashEscape;And so does this one (it's a false negative, since it's supposed to produce a warning, but it doesn't): /// Footnotes can contain asterisks, underscores, and other specials:
///
/// [^*]
//~^ ERROR: no footnote definition matching this footnote
///
/// [^_]
//~^ ERROR: no footnote definition matching this footnote
///
/// [^<inside></inside>]
//~^ ERROR: no footnote definition matching this footnote
pub struct Specials;To do this correctly, you need to parse the source text, not the returned event stream. Mostly copy scan_link_label, but strip out everything unrelated to footnotes. |
||
| { | ||
| missing_footnote_references.insert(Range { start: span.start, end: end_span.end }); | ||
| } | ||
|
Comment on lines
+24
to
+33
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is quite odd that pulldown_cmark isn't emmitting some form of FootnoteReference here despite the docs saying they might not map to an actual definition. In any case, I don't think this implementation is correct, since One way to handle this is to track the type of the last
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The docs are outdated. FootnoteReference is only emitted if the footnote definition exists.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. glad to see the docs getting fixed, but i still believe this code handles code blocks incorrectly.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could be easily answered by some tests that exercise the case of broken markdown inside a code block, as well as a footnote definition outside the code block referenced by markdown inside the block, and the other way around. |
||
| 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.emit_node_span_lint( | ||
| crate::lint::UNUSED_FOOTNOTE_DEFINITION, | ||
| hir_id, | ||
| span, | ||
| DiagDecorator(|lint| { | ||
| lint.primary_message("unused footnote definition"); | ||
| }), | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| #[allow(rustc::potential_query_instability)] | ||
| for span in missing_footnote_references { | ||
| let ref_span = source_span_for_markdown_range(tcx, dox, &span, &item.attrs.doc_strings) | ||
| .map(|(span, _)| span) | ||
| .unwrap_or_else(|| item.attr_span(tcx)); | ||
|
|
||
| tcx.emit_node_span_lint( | ||
| crate::lint::BROKEN_FOOTNOTE, | ||
| hir_id, | ||
| ref_span, | ||
| DiagDecorator(|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, | ||
| ); | ||
| }), | ||
| ); | ||
| } | ||
| } | ||
|
GuillaumeGomez marked this conversation as resolved.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| #![deny(rustdoc::broken_footnote)] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A selection of many good corner cases to test this lint against can be found here: https://pulldown-cmark.github.io/pulldown-cmark/specs/footnotes.html |
||
|
|
||
| //! Footnote referenced [^1]. And [^2]. And [^bla]. | ||
| //! | ||
| //! [^1]: footnote defined | ||
| //~^^^ ERROR: no footnote definition matching this footnote | ||
| //~| ERROR: no footnote definition matching this footnote | ||
|
|
||
| // Should not lint. | ||
| //! foo[^1] | ||
| //! | ||
| //! ``` | ||
| //! | ||
| //! [^1]: bar | ||
| //! | ||
| //! ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| error: no footnote definition matching this footnote | ||
| --> $DIR/broken-footnote.rs:3: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:3: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 | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| // This test ensures that the `rustdoc::unused_footnote` lint is working as expected. | ||
|
|
||
| #![deny(rustdoc::unused_footnote_definition)] | ||
|
|
||
| //! Footnote referenced. [^2] | ||
| //! | ||
| //! [^1]: footnote defined | ||
| //! [^2]: footnote defined | ||
| //~^^ ERROR: unused_footnote_definition |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we be making sure the lint is enabled before invoking the parser? I know the other lints don't do this, but maybe they should?