Skip to content

Commit 50ecb6e

Browse files
authored
doc_link_code: add check for links with code spans that render weird (#14121)
This is the lint described at rust-lang/rust#136308 (comment) that recommends using HTML to nest links inside code. changelog: [`doc_link_code`]: warn when a link with code and a code span are back-to-back
2 parents b83762c + aff497f commit 50ecb6e

File tree

6 files changed

+328
-0
lines changed

6 files changed

+328
-0
lines changed

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

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,

clippy_lints/src/doc/mod.rs

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

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

639661
impl_lint_pass!(Documentation => [
662+
DOC_LINK_CODE,
640663
DOC_LINK_WITH_QUOTES,
641664
DOC_MARKDOWN,
642665
DOC_NESTED_REFDEFS,
@@ -820,6 +843,21 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
820843

821844
let mut cb = fake_broken_link_callback;
822845

846+
check_for_code_clusters(
847+
cx,
848+
pulldown_cmark::Parser::new_with_broken_link_callback(
849+
&doc,
850+
main_body_opts() - Options::ENABLE_SMART_PUNCTUATION,
851+
Some(&mut cb),
852+
)
853+
.into_offset_iter(),
854+
&doc,
855+
Fragments {
856+
doc: &doc,
857+
fragments: &fragments,
858+
},
859+
);
860+
823861
// disable smart punctuation to pick up ['link'] more easily
824862
let opts = main_body_opts() - Options::ENABLE_SMART_PUNCTUATION;
825863
let parser = pulldown_cmark::Parser::new_with_broken_link_callback(&doc, opts, Some(&mut cb));
@@ -843,6 +881,66 @@ enum Container {
843881
List(usize),
844882
}
845883

884+
/// Scan the documentation for code links that are back-to-back with code spans.
885+
///
886+
/// This is done separately from the rest of the docs, because that makes it easier to produce
887+
/// the correct messages.
888+
fn check_for_code_clusters<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize>)>>(
889+
cx: &LateContext<'_>,
890+
events: Events,
891+
doc: &str,
892+
fragments: Fragments<'_>,
893+
) {
894+
let mut events = events.peekable();
895+
let mut code_starts_at = None;
896+
let mut code_ends_at = None;
897+
let mut code_includes_link = false;
898+
while let Some((event, range)) = events.next() {
899+
match event {
900+
Start(Link { .. }) if matches!(events.peek(), Some((Code(_), _range))) => {
901+
if code_starts_at.is_some() {
902+
code_ends_at = Some(range.end);
903+
} else {
904+
code_starts_at = Some(range.start);
905+
}
906+
code_includes_link = true;
907+
// skip the nested "code", because we're already handling it here
908+
let _ = events.next();
909+
},
910+
Code(_) => {
911+
if code_starts_at.is_some() {
912+
code_ends_at = Some(range.end);
913+
} else {
914+
code_starts_at = Some(range.start);
915+
}
916+
},
917+
End(TagEnd::Link) => {},
918+
_ => {
919+
if let Some(start) = code_starts_at
920+
&& let Some(end) = code_ends_at
921+
&& code_includes_link
922+
{
923+
if let Some(span) = fragments.span(cx, start..end) {
924+
span_lint_and_then(cx, DOC_LINK_CODE, span, "code link adjacent to code text", |diag| {
925+
let sugg = format!("<code>{}</code>", doc[start..end].replace('`', ""));
926+
diag.span_suggestion_verbose(
927+
span,
928+
"wrap the entire group in `<code>` tags",
929+
sugg,
930+
Applicability::MaybeIncorrect,
931+
);
932+
diag.help("separate code snippets will be shown with a gap");
933+
});
934+
}
935+
}
936+
code_includes_link = false;
937+
code_starts_at = None;
938+
code_ends_at = None;
939+
},
940+
}
941+
}
942+
}
943+
846944
/// Checks parsed documentation.
847945
/// This walks the "events" (think sections of markdown) produced by `pulldown_cmark`,
848946
/// so lints here will generally access that information.

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;

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;

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)