-
-
Notifications
You must be signed in to change notification settings - Fork 15.3k
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
fb1ff14
378cc53
176b1fa
35dd2a5
2a6588e
44afd9b
9529bcd
6a2cf7f
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,117 @@ | ||
| 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 == "[" | ||
| && (span.start == 0 || dox.as_bytes().get(span.start - 1) != Some(&b'\\')) | ||
| && let Some(len) = scan_footnote_ref(&dox[span.start..]) => | ||
| { | ||
| missing_footnote_references | ||
| .insert(Range { start: span.start, end: span.start + len }); | ||
| } | ||
|
Comment on lines
+24
to
+31
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, | ||
| ); | ||
| }), | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| fn scan_footnote_ref(dox: &str) -> Option<usize> { | ||
| let dox = dox.as_bytes(); | ||
| let mut i = 0; | ||
| if dox.get(i) != Some(&b'[') { | ||
| return None; | ||
| } | ||
| i += 1; | ||
| if dox.get(i) != Some(&b'^') { | ||
| return None; | ||
| } | ||
| i += 1; | ||
| while let Some(&c) = dox.get(i) { | ||
| if c == b']' { | ||
| i += 1; | ||
| return Some(i); | ||
| } | ||
| if c == b'\r' || c == b'\n' || c == b'[' { | ||
| // Can't nest things like this. | ||
| break; | ||
| } | ||
| if c == b'\\' { | ||
| i += 1; | ||
| } | ||
| if dox.get(i) == Some(&b'\r') || dox.get(i) == Some(&b'\n') { | ||
| // Can't have line breaks in footnote refs | ||
| break; | ||
| } | ||
| i += 1; | ||
| } | ||
| None | ||
| } | ||
|
GuillaumeGomez marked this conversation as resolved.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| #![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 | ||
| //! | ||
| //! ``` | ||
|
|
||
| // Edge cases from https://pulldown-cmark.github.io/pulldown-cmark/specs/footnotes.html | ||
| /// The following are not footnote references: | ||
| /// | ||
| /// \[^a] | ||
| /// | ||
| /// [\^b] | ||
| /// | ||
| /// [^c\] | ||
| /// | ||
| /// [^d | ||
| /// e] | ||
| /// | ||
| /// [^f\ | ||
| /// g] | ||
| pub struct NotReferences; | ||
|
|
||
| /// The following are not footnote references: | ||
| /// | ||
| /// [^a b] | ||
| //~^ ERROR: no footnote definition matching this footnote | ||
| /// | ||
| /// [^1\.2] | ||
| //~^ ERROR: no footnote definition matching this footnote | ||
| /// | ||
| /// [^*] | ||
| //~^ ERROR: no footnote definition matching this footnote | ||
| pub struct EdgeCases; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| 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: no footnote definition matching this footnote | ||
| --> $DIR/broken-footnote.rs:39:5 | ||
| | | ||
| LL | /// [^1\.2] | ||
| | -^^^^^^ | ||
| | | | ||
| | help: if it should not be a footnote, escape it: `\` | ||
|
|
||
| error: no footnote definition matching this footnote | ||
| --> $DIR/broken-footnote.rs:42:5 | ||
| | | ||
| LL | /// [^*] | ||
| | -^^^ | ||
| | | | ||
| | help: if it should not be a footnote, escape it: `\` | ||
|
|
||
| error: no footnote definition matching this footnote | ||
| --> $DIR/broken-footnote.rs:36:5 | ||
| | | ||
| LL | /// [^a b] | ||
| | -^^^^^ | ||
| | | | ||
| | help: if it should not be a footnote, escape it: `\` | ||
|
|
||
| error: aborting due to 5 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?