Skip to content

Commit cc64f45

Browse files
committed
doc_link_code: 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 75e3a2e commit cc64f45

File tree

6 files changed

+292
-0
lines changed

6 files changed

+292
-0
lines changed

Diff for: CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5530,6 +5530,7 @@ Released 2018-09-13
55305530
[`diverging_sub_expression`]: https://rust-lang.github.io/rust-clippy/master/index.html#diverging_sub_expression
55315531
[`doc_include_without_cfg`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_include_without_cfg
55325532
[`doc_lazy_continuation`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_lazy_continuation
5533+
[`doc_link_code`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_link_code
55335534
[`doc_link_with_quotes`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_link_with_quotes
55345535
[`doc_markdown`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown
55355536
[`doc_nested_refdefs`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_nested_refdefs

Diff for: clippy_lints/src/declared_lints.rs

+1
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
139139
crate::disallowed_types::DISALLOWED_TYPES_INFO,
140140
crate::doc::DOC_INCLUDE_WITHOUT_CFG_INFO,
141141
crate::doc::DOC_LAZY_CONTINUATION_INFO,
142+
crate::doc::DOC_LINK_CODE_INFO,
142143
crate::doc::DOC_LINK_WITH_QUOTES_INFO,
143144
crate::doc::DOC_MARKDOWN_INFO,
144145
crate::doc::DOC_NESTED_REFDEFS_INFO,

Diff for: clippy_lints/src/doc/mod.rs

+62
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,28 @@ declare_clippy_lint! {
8484
"presence of `_`, `::` or camel-case outside backticks in documentation"
8585
}
8686

87+
declare_clippy_lint! {
88+
/// ### What it does
89+
/// Checks for links with code directly adjacent to code text:
90+
/// `` [`MyItem`]`<`[`u32`]`>` ``.
91+
///
92+
/// ### Why is this bad?
93+
/// It can be written more simply using HTML-style `<code>` tags.
94+
///
95+
/// ### Example
96+
/// ```no_run
97+
/// //! [`first`](x)`second`
98+
/// ```
99+
/// Use instead:
100+
/// ```no_run
101+
/// //! <code>[first](x)second</code>
102+
/// ```
103+
#[clippy::version = "1.86.0"]
104+
pub DOC_LINK_CODE,
105+
nursery,
106+
"link with code back-to-back with other code"
107+
}
108+
87109
declare_clippy_lint! {
88110
/// ### What it does
89111
/// Checks for the doc comments of publicly visible
@@ -638,6 +660,7 @@ impl Documentation {
638660
}
639661

640662
impl_lint_pass!(Documentation => [
663+
DOC_LINK_CODE,
641664
DOC_LINK_WITH_QUOTES,
642665
DOC_MARKDOWN,
643666
DOC_NESTED_REFDEFS,
@@ -844,6 +867,14 @@ enum Container {
844867
List(usize),
845868
}
846869

870+
#[derive(Clone, Copy, Eq, PartialEq)]
871+
enum CodeCluster {
872+
// true means already in a link, so only needs to be followed by code
873+
// false means we've hit code, and need to find a link
874+
First(usize, bool),
875+
Nth(usize, usize),
876+
}
877+
847878
/// Checks parsed documentation.
848879
/// This walks the "events" (think sections of markdown) produced by `pulldown_cmark`,
849880
/// so lints here will generally access that information.
@@ -875,9 +906,40 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
875906

876907
let mut containers = Vec::new();
877908

909+
let mut code_cluster = None;
910+
878911
let mut events = events.peekable();
879912

880913
while let Some((event, range)) = events.next() {
914+
code_cluster = match (code_cluster, &event) {
915+
(None, Code(_)) if in_link.is_some() && doc.as_bytes().get(range.start.wrapping_sub(1)) == Some(&b'[') => {
916+
Some(CodeCluster::First(range.start - 1, true))
917+
},
918+
(None, Code(_)) => Some(CodeCluster::First(range.start, false)),
919+
(Some(CodeCluster::First(pos, _)), Start(Link { .. })) | (Some(CodeCluster::First(pos, true)), Code(_)) => {
920+
Some(CodeCluster::Nth(pos, range.end))
921+
},
922+
(Some(CodeCluster::Nth(start, end)), Code(_) | Start(Link { .. })) => {
923+
Some(CodeCluster::Nth(start, range.end.max(end)))
924+
},
925+
(code_cluster @ Some(_), Code(_) | End(TagEnd::Link)) => code_cluster,
926+
(Some(CodeCluster::First(_, _)) | None, _) => None,
927+
(Some(CodeCluster::Nth(start, end)), _) => {
928+
if let Some(span) = fragments.span(cx, start..end) {
929+
span_lint_and_then(cx, DOC_LINK_CODE, span, "code link adjacent to code text", |diag| {
930+
let sugg = format!("<code>{}</code>", doc[start..end].replace('`', ""));
931+
diag.span_suggestion_verbose(
932+
span,
933+
"wrap the entire group in `<code>` tags",
934+
sugg,
935+
Applicability::MaybeIncorrect,
936+
);
937+
diag.help("separate code snippets will be shown with a gap");
938+
});
939+
}
940+
None
941+
},
942+
};
881943
match event {
882944
Html(tag) | InlineHtml(tag) => {
883945
if tag.starts_with("<code") {

Diff for: tests/ui/doc/link_adjacent.fixed

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#![warn(clippy::doc_link_code)]
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;

Diff for: tests/ui/doc/link_adjacent.rs

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#![warn(clippy::doc_link_code)]
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;

Diff for: 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-link-code` implied by `-D warnings`
9+
= help: to override `-D warnings` add `#[allow(clippy::doc_link_code)]`
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)