Skip to content

Commit 99b16ae

Browse files
committed
Improve suggestions and add more tests
1 parent 1356e76 commit 99b16ae

8 files changed

+390
-15
lines changed

clippy_lints/src/doc/doc_suspicious_footnotes.rs

+66-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use clippy_utils::diagnostics::span_lint_and_then;
2-
use clippy_utils::source::snippet_with_applicability;
32
use rustc_errors::Applicability;
4-
use rustc_lint::LateContext;
3+
use rustc_lint::{LateContext, LintContext};
54

65
use std::ops::Range;
76

@@ -14,21 +13,81 @@ pub fn check(cx: &LateContext<'_>, doc: &str, range: Range<usize>, fragments: &F
1413
.filter_map(|(i, c)| if c == b'[' { Some(i) } else { None })
1514
{
1615
let start = i + range.start;
16+
let mut this_fragment_start = start;
1717
if doc.as_bytes().get(start + 1) == Some(&b'^')
1818
&& let Some(end) = all_numbers_upto_brace(doc, start + 2)
1919
&& doc.as_bytes().get(end) != Some(&b':')
2020
&& doc.as_bytes().get(start - 1) != Some(&b'\\')
21-
&& let Some(span) = fragments.span(cx, start..end)
21+
&& let Some(this_fragment) = fragments
22+
.fragments
23+
.iter()
24+
.find(|frag| {
25+
let found = this_fragment_start < frag.doc.as_str().len();
26+
if !found {
27+
this_fragment_start -= frag.doc.as_str().len();
28+
}
29+
found
30+
})
31+
.or(fragments.fragments.last())
2232
{
33+
let span = fragments.span(cx, start..end).unwrap_or(this_fragment.span);
2334
span_lint_and_then(
2435
cx,
2536
DOC_SUSPICIOUS_FOOTNOTES,
2637
span,
27-
"looks like a footnote ref, but no matching footnote",
38+
"looks like a footnote ref, but has no matching footnote",
2839
|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);
40+
let applicability = Applicability::HasPlaceholders;
41+
let start_of_md_line = doc.as_bytes()[..start]
42+
.iter()
43+
.rposition(|&c| c == b'\n' || c == b'\r')
44+
.unwrap_or(0);
45+
let end_of_md_line = doc.as_bytes()[start..]
46+
.iter()
47+
.position(|&c| c == b'\n' || c == b'\r')
48+
.unwrap_or(doc.len() - start)
49+
+ start;
50+
let span_md_line = fragments
51+
.span(cx, start_of_md_line..end_of_md_line)
52+
.unwrap_or(this_fragment.span);
53+
let span_whole_line = cx.sess().source_map().span_extend_to_line(span_md_line);
54+
if let Ok(mut pfx) = cx
55+
.sess()
56+
.source_map()
57+
.span_to_snippet(span_whole_line.until(span_md_line))
58+
&& let Ok(mut sfx) = cx
59+
.sess()
60+
.source_map()
61+
.span_to_snippet(span_md_line.shrink_to_hi().until(span_whole_line.shrink_to_hi()))
62+
{
63+
let mut insert_before = String::new();
64+
let mut insert_after = String::new();
65+
let span = if this_fragment.kind == rustc_resolve::rustdoc::DocFragmentKind::RawDoc
66+
&& (!pfx.is_empty() || !sfx.is_empty())
67+
{
68+
if (pfx.trim() == "#[doc=" || pfx.trim() == "#![doc=") && sfx.trim() == "]" {
69+
// try to use per-line doc fragments if that's what the author did
70+
pfx.push('"');
71+
sfx.insert(0, '"');
72+
span_whole_line.shrink_to_hi()
73+
} else {
74+
// otherwise, replace the whole line with the result
75+
pfx = String::new();
76+
sfx = String::new();
77+
insert_before = format!(r#"r###"{}"#, this_fragment.doc);
78+
r####""###"####.clone_into(&mut insert_after);
79+
span_md_line
80+
}
81+
} else {
82+
span_whole_line.shrink_to_hi()
83+
};
84+
diag.span_suggestion_verbose(
85+
span,
86+
"add footnote definition",
87+
format!("{insert_before}\n{pfx}{sfx}\n{pfx}{label}: <!-- description -->{sfx}\n{pfx}{sfx}{insert_after}", label = &doc[start..end]),
88+
applicability,
89+
);
90+
}
3291
},
3392
);
3493
}

clippy_lints/src/doc/mod.rs

+10-1
Original file line numberDiff line numberDiff line change
@@ -615,8 +615,17 @@ declare_clippy_lint! {
615615
/// because it matches the regexp `\[\^[0-9]+\]`,
616616
/// but has no referent.
617617
///
618+
/// Rustdoc footnotes are compatible with GitHub-Flavored Markdown (GFM).
619+
/// They are not parsed as footnotes unless a definition also exists,
620+
/// so they usually "do what you mean" if you want to write the text
621+
/// literally—usually in a regular expression.
622+
///
623+
/// However, footnote references are usually numbers, and regex
624+
/// negative character classes usually contain other characters, so this
625+
/// lint can make a practical guess for which is meant.
626+
///
618627
/// ### Why is this bad?
619-
/// This probably means that a definition was meant to exist,
628+
/// This probably means that a footnote was meant to exist,
620629
/// but was not written.
621630
///
622631
/// ### Example

tests/ui/doc_suspicious_footnotes.fixed

+101-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,27 @@
11
#![warn(clippy::doc_suspicious_footnotes)]
2+
#![allow(clippy::needless_raw_string_hashes)]
3+
//! This is not a footnote[^1].
4+
//!
5+
//! [^1]: <!-- description -->
6+
//!
7+
//~^ doc_suspicious_footnotes
8+
//!
9+
//! This is not a footnote[^either], but it doesn't warn.
10+
//!
11+
//! This is not a footnote\[^1], but it also doesn't warn.
12+
//!
13+
//! This is not a footnote[^1\], but it also doesn't warn.
14+
//!
15+
//! This is not a `footnote[^1]`, but it also doesn't warn.
16+
//!
17+
//! This is a footnote[^2].
18+
//!
19+
//! [^2]: hello world
220

3-
/// This is not a footnote`[^1]`.
21+
/// This is not a footnote[^1].
22+
///
23+
/// [^1]: <!-- description -->
24+
///
425
//~^ doc_suspicious_footnotes
526
///
627
/// This is not a footnote[^either], but it doesn't warn.
@@ -17,3 +38,82 @@
1738
pub fn footnotes() {
1839
// test code goes here
1940
}
41+
42+
pub struct Foo;
43+
impl Foo {
44+
#[doc = r###"This is not a footnote[^1].
45+
46+
[^1]: <!-- description -->
47+
"###]
48+
//~^ doc_suspicious_footnotes
49+
#[doc = r#""#]
50+
#[doc = r#"This is not a footnote[^either], but it doesn't warn."#]
51+
#[doc = r#""#]
52+
#[doc = r#"This is not a footnote\[^1], but it also doesn't warn."#]
53+
#[doc = r#""#]
54+
#[doc = r#"This is not a footnote[^1\], but it also doesn't warn."#]
55+
#[doc = r#""#]
56+
#[doc = r#"This is not a `footnote[^1]`, but it also doesn't warn."#]
57+
#[doc = r#""#]
58+
#[doc = r#"This is a footnote[^2]."#]
59+
#[doc = r#""#]
60+
#[doc = r#"[^2]: hello world"#]
61+
pub fn footnotes() {
62+
// test code goes here
63+
}
64+
#[doc = r###"This is not a footnote[^1].
65+
66+
This is not a footnote[^either], but it doesn't warn.
67+
68+
This is not a footnote\[^1], but it also doesn't warn.
69+
70+
This is not a footnote[^1\], but it also doesn't warn.
71+
72+
This is not a `footnote[^1]`, but it also doesn't warn.
73+
74+
This is a footnote[^2].
75+
76+
[^2]: hello world
77+
78+
79+
[^1]: <!-- description -->
80+
"###]
81+
//~^^^^^^^^^^^^^^ doc_suspicious_footnotes
82+
pub fn footnotes2() {
83+
// test code goes here
84+
}
85+
#[cfg_attr(
86+
not(FALSE),
87+
doc = r###"This is not a footnote[^1].
88+
89+
This is not a footnote[^either], but it doesn't warn.
90+
91+
[^1]: <!-- description -->
92+
"###
93+
//~^ doc_suspicious_footnotes
94+
)]
95+
pub fn footnotes3() {
96+
// test code goes here
97+
}
98+
}
99+
100+
#[doc = r###"This is not a footnote[^1].
101+
102+
[^1]: <!-- description -->
103+
"###]
104+
//~^ doc_suspicious_footnotes
105+
#[doc = r""]
106+
#[doc = r"This is not a footnote[^either], but it doesn't warn."]
107+
#[doc = r""]
108+
#[doc = r"This is not a footnote\[^1], but it also doesn't warn."]
109+
#[doc = r""]
110+
#[doc = r"This is not a footnote[^1\], but it also doesn't warn."]
111+
#[doc = r""]
112+
#[doc = r"This is not a `footnote[^1]`, but it also doesn't warn."]
113+
#[doc = r""]
114+
#[doc = r"This is a footnote[^2]."]
115+
#[doc = r""]
116+
#[doc = r"[^2]: hello world"]
117+
pub fn footnotes_attrs() {
118+
// test code goes here
119+
}

tests/ui/doc_suspicious_footnotes.rs

+80
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,19 @@
11
#![warn(clippy::doc_suspicious_footnotes)]
2+
#![allow(clippy::needless_raw_string_hashes)]
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
217
318
/// This is not a footnote[^1].
419
//~^ doc_suspicious_footnotes
@@ -17,3 +32,68 @@
1732
pub fn footnotes() {
1833
// test code goes here
1934
}
35+
36+
pub struct Foo;
37+
impl Foo {
38+
#[doc = r#"This is not a footnote[^1]."#]
39+
//~^ doc_suspicious_footnotes
40+
#[doc = r#""#]
41+
#[doc = r#"This is not a footnote[^either], but it doesn't warn."#]
42+
#[doc = r#""#]
43+
#[doc = r#"This is not a footnote\[^1], but it also doesn't warn."#]
44+
#[doc = r#""#]
45+
#[doc = r#"This is not a footnote[^1\], but it also doesn't warn."#]
46+
#[doc = r#""#]
47+
#[doc = r#"This is not a `footnote[^1]`, but it also doesn't warn."#]
48+
#[doc = r#""#]
49+
#[doc = r#"This is a footnote[^2]."#]
50+
#[doc = r#""#]
51+
#[doc = r#"[^2]: hello world"#]
52+
pub fn footnotes() {
53+
// test code goes here
54+
}
55+
#[doc = "This is not a footnote[^1].
56+
57+
This is not a footnote[^either], but it doesn't warn.
58+
59+
This is not a footnote\\[^1], but it also doesn't warn.
60+
61+
This is not a footnote[^1\\], but it also doesn't warn.
62+
63+
This is not a `footnote[^1]`, but it also doesn't warn.
64+
65+
This is a footnote[^2].
66+
67+
[^2]: hello world
68+
"]
69+
//~^^^^^^^^^^^^^^ doc_suspicious_footnotes
70+
pub fn footnotes2() {
71+
// test code goes here
72+
}
73+
#[cfg_attr(
74+
not(FALSE),
75+
doc = "This is not a footnote[^1].\n\nThis is not a footnote[^either], but it doesn't warn."
76+
//~^ doc_suspicious_footnotes
77+
)]
78+
pub fn footnotes3() {
79+
// test code goes here
80+
}
81+
}
82+
83+
#[doc = r"This is not a footnote[^1]."]
84+
//~^ doc_suspicious_footnotes
85+
#[doc = r""]
86+
#[doc = r"This is not a footnote[^either], but it doesn't warn."]
87+
#[doc = r""]
88+
#[doc = r"This is not a footnote\[^1], but it also doesn't warn."]
89+
#[doc = r""]
90+
#[doc = r"This is not a footnote[^1\], but it also doesn't warn."]
91+
#[doc = r""]
92+
#[doc = r"This is not a `footnote[^1]`, but it also doesn't warn."]
93+
#[doc = r""]
94+
#[doc = r"This is a footnote[^2]."]
95+
#[doc = r""]
96+
#[doc = r"[^2]: hello world"]
97+
pub fn footnotes_attrs() {
98+
// test code goes here
99+
}

0 commit comments

Comments
 (0)