Skip to content

Commit 05dbcc1

Browse files
committed
doc_markdown: add check for text[adjacent] style links
This is the lint described at rust-lang/rust#136308 (comment) that recommends using HTML to nest links inside code.
1 parent f51e18d commit 05dbcc1

File tree

4 files changed

+267
-0
lines changed

4 files changed

+267
-0
lines changed

clippy_lints/src/doc/mod.rs

+39
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,14 @@ enum Container {
844844
List(usize),
845845
}
846846

847+
#[derive(Clone, Copy, Eq, PartialEq)]
848+
enum CodeCluster {
849+
// true means already in a link, so only needs to be followed by code
850+
// false means we've hit code, and need to find a link
851+
First(usize, bool),
852+
Nth(usize, usize),
853+
}
854+
847855
/// Checks parsed documentation.
848856
/// This walks the "events" (think sections of markdown) produced by `pulldown_cmark`,
849857
/// so lints here will generally access that information.
@@ -875,9 +883,40 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
875883

876884
let mut containers = Vec::new();
877885

886+
let mut code_cluster = None;
887+
878888
let mut events = events.peekable();
879889

880890
while let Some((event, range)) = events.next() {
891+
code_cluster = match (code_cluster, &event) {
892+
(None, Code(_)) if in_link.is_some() && doc.as_bytes().get(range.start.wrapping_sub(1)) == Some(&b'[') => {
893+
Some(CodeCluster::First(range.start - 1, true))
894+
},
895+
(None, Code(_)) => Some(CodeCluster::First(range.start, false)),
896+
(Some(CodeCluster::First(pos, _)), Start(Link { .. })) | (Some(CodeCluster::First(pos, true)), Code(_)) => {
897+
Some(CodeCluster::Nth(pos, range.end))
898+
},
899+
(Some(CodeCluster::Nth(start, end)), Code(_) | Start(Link { .. })) => {
900+
Some(CodeCluster::Nth(start, range.end.max(end)))
901+
},
902+
(code_cluster @ Some(_), Code(_) | End(TagEnd::Link)) => code_cluster,
903+
(Some(CodeCluster::First(_, _)) | None, _) => None,
904+
(Some(CodeCluster::Nth(start, end)), _) => {
905+
if let Some(span) = fragments.span(cx, start..end) {
906+
span_lint_and_then(cx, DOC_MARKDOWN, span, "code link adjacent to code text", |diag| {
907+
let sugg = format!("<code>{}</code>", doc[start..end].replace('`', ""));
908+
diag.span_suggestion_verbose(
909+
span,
910+
"wrap the entire group in `<code>` tags",
911+
sugg,
912+
Applicability::MaybeIncorrect,
913+
);
914+
diag.help("separate code snippets will be shown with a gap");
915+
});
916+
}
917+
None
918+
},
919+
};
881920
match event {
882921
Html(tag) | InlineHtml(tag) => {
883922
if tag.starts_with("<code") {

tests/ui/doc/link_adjacent.fixed

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#![warn(clippy::doc_markdown)]
2+
3+
//! Test case for code links that are adjacent to code text.
4+
//!
5+
//! This is not an example: `first``second`
6+
//!
7+
//! Neither is this: [`first`](x)
8+
//!
9+
//! Neither is this: [`first`](x) `second`
10+
//!
11+
//! Neither is this: [first](x)`second`
12+
//!
13+
//! This is: <code>[first](x)second</code>
14+
//~^ ERROR: adjacent
15+
//!
16+
//! So is this <code>first[second](x)</code>
17+
//~^ ERROR: adjacent
18+
//!
19+
//! So is this <code>[first](x)[second](x)</code>
20+
//~^ ERROR: adjacent
21+
//!
22+
//! So is this <code>[first](x)[second](x)[third](x)</code>
23+
//~^ ERROR: adjacent
24+
//!
25+
//! So is this <code>[first](x)second[third](x)</code>
26+
//~^ ERROR: adjacent
27+
28+
/// Test case for code links that are adjacent to code text.
29+
///
30+
/// This is not an example: `first``second` arst
31+
///
32+
/// Neither is this: [`first`](x) arst
33+
///
34+
/// Neither is this: [`first`](x) `second` arst
35+
///
36+
/// Neither is this: [first](x)`second` arst
37+
///
38+
/// This is: <code>[first](x)second</code> arst
39+
//~^ ERROR: adjacent
40+
///
41+
/// So is this <code>first[second](x)</code> arst
42+
//~^ ERROR: adjacent
43+
///
44+
/// So is this <code>[first](x)[second](x)</code> arst
45+
//~^ ERROR: adjacent
46+
///
47+
/// So is this <code>[first](x)[second](x)[third](x)</code> arst
48+
//~^ ERROR: adjacent
49+
///
50+
/// So is this <code>[first](x)second[third](x)</code> arst
51+
//~^ ERROR: adjacent
52+
pub struct WithTrailing;

tests/ui/doc/link_adjacent.rs

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#![warn(clippy::doc_markdown)]
2+
3+
//! Test case for code links that are adjacent to code text.
4+
//!
5+
//! This is not an example: `first``second`
6+
//!
7+
//! Neither is this: [`first`](x)
8+
//!
9+
//! Neither is this: [`first`](x) `second`
10+
//!
11+
//! Neither is this: [first](x)`second`
12+
//!
13+
//! This is: [`first`](x)`second`
14+
//~^ ERROR: adjacent
15+
//!
16+
//! So is this `first`[`second`](x)
17+
//~^ ERROR: adjacent
18+
//!
19+
//! So is this [`first`](x)[`second`](x)
20+
//~^ ERROR: adjacent
21+
//!
22+
//! So is this [`first`](x)[`second`](x)[`third`](x)
23+
//~^ ERROR: adjacent
24+
//!
25+
//! So is this [`first`](x)`second`[`third`](x)
26+
//~^ ERROR: adjacent
27+
28+
/// Test case for code links that are adjacent to code text.
29+
///
30+
/// This is not an example: `first``second` arst
31+
///
32+
/// Neither is this: [`first`](x) arst
33+
///
34+
/// Neither is this: [`first`](x) `second` arst
35+
///
36+
/// Neither is this: [first](x)`second` arst
37+
///
38+
/// This is: [`first`](x)`second` arst
39+
//~^ ERROR: adjacent
40+
///
41+
/// So is this `first`[`second`](x) arst
42+
//~^ ERROR: adjacent
43+
///
44+
/// So is this [`first`](x)[`second`](x) arst
45+
//~^ ERROR: adjacent
46+
///
47+
/// So is this [`first`](x)[`second`](x)[`third`](x) arst
48+
//~^ ERROR: adjacent
49+
///
50+
/// So is this [`first`](x)`second`[`third`](x) arst
51+
//~^ ERROR: adjacent
52+
pub struct WithTrailing;

tests/ui/doc/link_adjacent.stderr

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
error: code link adjacent to code text
2+
--> tests/ui/doc/link_adjacent.rs:13:14
3+
|
4+
LL | //! This is: [`first`](x)`second`
5+
| ^^^^^^^^^^^^^^^^^^^^
6+
|
7+
= help: separate code snippets will be shown with a gap
8+
= note: `-D clippy::doc-markdown` implied by `-D warnings`
9+
= help: to override `-D warnings` add `#[allow(clippy::doc_markdown)]`
10+
help: wrap the entire group in `<code>` tags
11+
|
12+
LL | //! This is: <code>[first](x)second</code>
13+
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
14+
15+
error: code link adjacent to code text
16+
--> tests/ui/doc/link_adjacent.rs:16:16
17+
|
18+
LL | //! So is this `first`[`second`](x)
19+
| ^^^^^^^^^^^^^^^^^^^^
20+
|
21+
= help: separate code snippets will be shown with a gap
22+
help: wrap the entire group in `<code>` tags
23+
|
24+
LL | //! So is this <code>first[second](x)</code>
25+
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
26+
27+
error: code link adjacent to code text
28+
--> tests/ui/doc/link_adjacent.rs:19:16
29+
|
30+
LL | //! So is this [`first`](x)[`second`](x)
31+
| ^^^^^^^^^^^^^^^^^^^^^^^^^
32+
|
33+
= help: separate code snippets will be shown with a gap
34+
help: wrap the entire group in `<code>` tags
35+
|
36+
LL | //! So is this <code>[first](x)[second](x)</code>
37+
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
38+
39+
error: code link adjacent to code text
40+
--> tests/ui/doc/link_adjacent.rs:22:16
41+
|
42+
LL | //! So is this [`first`](x)[`second`](x)[`third`](x)
43+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
44+
|
45+
= help: separate code snippets will be shown with a gap
46+
help: wrap the entire group in `<code>` tags
47+
|
48+
LL | //! So is this <code>[first](x)[second](x)[third](x)</code>
49+
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
50+
51+
error: code link adjacent to code text
52+
--> tests/ui/doc/link_adjacent.rs:25:16
53+
|
54+
LL | //! So is this [`first`](x)`second`[`third`](x)
55+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
56+
|
57+
= help: separate code snippets will be shown with a gap
58+
help: wrap the entire group in `<code>` tags
59+
|
60+
LL | //! So is this <code>[first](x)second[third](x)</code>
61+
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
62+
63+
error: code link adjacent to code text
64+
--> tests/ui/doc/link_adjacent.rs:38:14
65+
|
66+
LL | /// This is: [`first`](x)`second` arst
67+
| ^^^^^^^^^^^^^^^^^^^^
68+
|
69+
= help: separate code snippets will be shown with a gap
70+
help: wrap the entire group in `<code>` tags
71+
|
72+
LL | /// This is: <code>[first](x)second</code> arst
73+
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
74+
75+
error: code link adjacent to code text
76+
--> tests/ui/doc/link_adjacent.rs:41:16
77+
|
78+
LL | /// So is this `first`[`second`](x) arst
79+
| ^^^^^^^^^^^^^^^^^^^^
80+
|
81+
= help: separate code snippets will be shown with a gap
82+
help: wrap the entire group in `<code>` tags
83+
|
84+
LL | /// So is this <code>first[second](x)</code> arst
85+
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
86+
87+
error: code link adjacent to code text
88+
--> tests/ui/doc/link_adjacent.rs:44:16
89+
|
90+
LL | /// So is this [`first`](x)[`second`](x) arst
91+
| ^^^^^^^^^^^^^^^^^^^^^^^^^
92+
|
93+
= help: separate code snippets will be shown with a gap
94+
help: wrap the entire group in `<code>` tags
95+
|
96+
LL | /// So is this <code>[first](x)[second](x)</code> arst
97+
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
98+
99+
error: code link adjacent to code text
100+
--> tests/ui/doc/link_adjacent.rs:47:16
101+
|
102+
LL | /// So is this [`first`](x)[`second`](x)[`third`](x) arst
103+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
104+
|
105+
= help: separate code snippets will be shown with a gap
106+
help: wrap the entire group in `<code>` tags
107+
|
108+
LL | /// So is this <code>[first](x)[second](x)[third](x)</code> arst
109+
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
110+
111+
error: code link adjacent to code text
112+
--> tests/ui/doc/link_adjacent.rs:50:16
113+
|
114+
LL | /// So is this [`first`](x)`second`[`third`](x) arst
115+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
116+
|
117+
= help: separate code snippets will be shown with a gap
118+
help: wrap the entire group in `<code>` tags
119+
|
120+
LL | /// So is this <code>[first](x)second[third](x)</code> arst
121+
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
122+
123+
error: aborting due to 10 previous errors
124+

0 commit comments

Comments
 (0)