Skip to content

Commit 9f74df2

Browse files
committed
doc_suspicious_footnotes: lint text that looks like a footnote
1 parent 549107d commit 9f74df2

7 files changed

+138
-2
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5636,6 +5636,7 @@ Released 2018-09-13
56365636
[`doc_markdown`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown
56375637
[`doc_nested_refdefs`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_nested_refdefs
56385638
[`doc_overindented_list_items`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_overindented_list_items
5639+
[`doc_suspicious_footnotes`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_suspicious_footnotes
56395640
[`double_comparisons`]: https://rust-lang.github.io/rust-clippy/master/index.html#double_comparisons
56405641
[`double_ended_iterator_last`]: https://rust-lang.github.io/rust-clippy/master/index.html#double_ended_iterator_last
56415642
[`double_must_use`]: https://rust-lang.github.io/rust-clippy/master/index.html#double_must_use

clippy_lints/src/declared_lints.rs

+1
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
117117
crate::doc::DOC_MARKDOWN_INFO,
118118
crate::doc::DOC_NESTED_REFDEFS_INFO,
119119
crate::doc::DOC_OVERINDENTED_LIST_ITEMS_INFO,
120+
crate::doc::DOC_SUSPICIOUS_FOOTNOTES_INFO,
120121
crate::doc::EMPTY_DOCS_INFO,
121122
crate::doc::MISSING_ERRORS_DOC_INFO,
122123
crate::doc::MISSING_PANICS_DOC_INFO,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
use clippy_utils::diagnostics::span_lint_and_then;
2+
use clippy_utils::source::snippet_with_applicability;
3+
use rustc_errors::Applicability;
4+
use rustc_lint::LateContext;
5+
6+
use std::ops::Range;
7+
8+
use super::{DOC_SUSPICIOUS_FOOTNOTES, Fragments};
9+
10+
pub fn check(cx: &LateContext<'_>, doc: &str, range: Range<usize>, fragments: &Fragments<'_>) {
11+
for i in doc[range.clone()]
12+
.bytes()
13+
.enumerate()
14+
.filter_map(|(i, c)| if c == b'[' { Some(i) } else { None })
15+
{
16+
let start = i + range.start;
17+
if doc.as_bytes().get(start + 1) == Some(&b'^') {
18+
if let Some(end) = all_numbers_upto_brace(doc, start + 2)
19+
&& doc.as_bytes().get(end) != Some(&b':')
20+
&& doc.as_bytes().get(start - 1) != Some(&b'\\')
21+
&& let Some(span) = fragments.span(cx, start..end)
22+
{
23+
span_lint_and_then(
24+
cx,
25+
DOC_SUSPICIOUS_FOOTNOTES,
26+
span,
27+
"looks like a footnote ref, but no matching footnote",
28+
|diag| {
29+
let mut applicability = Applicability::MachineApplicable;
30+
let snippet = snippet_with_applicability(cx, span, "..", &mut applicability);
31+
diag.span_suggestion_verbose(span, "try", format!("`{snippet}`"), applicability);
32+
},
33+
);
34+
}
35+
}
36+
}
37+
}
38+
39+
fn all_numbers_upto_brace(text: &str, i: usize) -> Option<usize> {
40+
for (j, c) in text.as_bytes()[i..].iter().copied().enumerate().take(64) {
41+
if c == b']' && j != 0 {
42+
return Some(i + j + 1);
43+
}
44+
if !c.is_ascii_digit() || j >= 64 {
45+
break;
46+
}
47+
}
48+
None
49+
}

clippy_lints/src/doc/mod.rs

+33-2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use std::ops::Range;
2626
use url::Url;
2727

2828
mod doc_comment_double_space_linebreaks;
29+
mod doc_suspicious_footnotes;
2930
mod include_in_doc_without_cfg;
3031
mod lazy_continuation;
3132
mod link_with_quotes;
@@ -608,6 +609,34 @@ declare_clippy_lint! {
608609
"double-space used for doc comment linebreak instead of `\\`"
609610
}
610611

612+
declare_clippy_lint! {
613+
/// ### What it does
614+
/// Detects syntax that looks like a footnote reference,
615+
/// because it matches the regexp `\[\^[0-9]+\]`,
616+
/// but has no referent.
617+
///
618+
/// ### Why is this bad?
619+
/// This probably means that a definition was meant to exist,
620+
/// but was not written.
621+
///
622+
/// ### Example
623+
/// ```no_run
624+
/// /// This is not a footnote[^1], because no definition exists.
625+
/// fn my_fn() {}
626+
/// ```
627+
/// Use instead:
628+
/// ```no_run
629+
/// /// This is a footnote[^1].
630+
/// ///
631+
/// /// [^1]: defined here
632+
/// fn my_fn() {}
633+
/// ```
634+
#[clippy::version = "1.88.0"]
635+
pub DOC_SUSPICIOUS_FOOTNOTES,
636+
suspicious,
637+
"looks like a link or footnote ref, but with no definition"
638+
}
639+
611640
pub struct Documentation {
612641
valid_idents: FxHashSet<String>,
613642
check_private_items: bool,
@@ -639,7 +668,8 @@ impl_lint_pass!(Documentation => [
639668
DOC_OVERINDENTED_LIST_ITEMS,
640669
TOO_LONG_FIRST_DOC_PARAGRAPH,
641670
DOC_INCLUDE_WITHOUT_CFG,
642-
DOC_COMMENT_DOUBLE_SPACE_LINEBREAKS
671+
DOC_COMMENT_DOUBLE_SPACE_LINEBREAKS,
672+
DOC_SUSPICIOUS_FOOTNOTES,
643673
]);
644674

645675
impl EarlyLintPass for Documentation {
@@ -1147,7 +1177,8 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
11471177
// Don't check the text associated with external URLs
11481178
continue;
11491179
}
1150-
text_to_check.push((text, range, code_level));
1180+
text_to_check.push((text, range.clone(), code_level));
1181+
doc_suspicious_footnotes::check(cx, doc, range, &fragments);
11511182
}
11521183
}
11531184
FootnoteReference(_) => {}
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#![warn(clippy::doc_suspicious_footnotes)]
2+
3+
/// This is not a footnote`[^1]`.
4+
//~^ doc_suspicious_footnotes
5+
///
6+
/// This is not a footnote[^either], but it doesn't warn.
7+
///
8+
/// This is not a footnote\[^1], but it also doesn't warn.
9+
///
10+
/// This is not a footnote[^1\], but it also doesn't warn.
11+
///
12+
/// This is not a `footnote[^1]`, but it also doesn't warn.
13+
///
14+
/// This is a footnote[^2].
15+
///
16+
/// [^2]: hello world
17+
pub fn footnotes() {
18+
// test code goes here
19+
}

tests/ui/doc_suspicious_footnotes.rs

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#![warn(clippy::doc_suspicious_footnotes)]
2+
3+
/// This is not a footnote[^1].
4+
//~^ doc_suspicious_footnotes
5+
///
6+
/// This is not a footnote[^either], but it doesn't warn.
7+
///
8+
/// This is not a footnote\[^1], but it also doesn't warn.
9+
///
10+
/// This is not a footnote[^1\], but it also doesn't warn.
11+
///
12+
/// This is not a `footnote[^1]`, but it also doesn't warn.
13+
///
14+
/// This is a footnote[^2].
15+
///
16+
/// [^2]: hello world
17+
pub fn footnotes() {
18+
// test code goes here
19+
}
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
error: looks like a footnote ref, but no matching footnote
2+
--> tests/ui/doc_suspicious_footnotes.rs:3:27
3+
|
4+
LL | /// This is not a footnote[^1].
5+
| ^^^^
6+
|
7+
= note: `-D clippy::doc-suspicious-footnotes` implied by `-D warnings`
8+
= help: to override `-D warnings` add `#[allow(clippy::doc_suspicious_footnotes)]`
9+
help: try
10+
|
11+
LL - /// This is not a footnote[^1].
12+
LL + /// This is not a footnote`[^1]`.
13+
|
14+
15+
error: aborting due to 1 previous error
16+

0 commit comments

Comments
 (0)