Skip to content

Commit 66b15ad

Browse files
authored
doc_nested_refdefs: new lint for suspicious list syntax (#13707)
rust-lang/rust#133150 This is more likely to be intended as an intra-doc link than it is to be intended as a refdef. If a refdef is intended, it does not need to be nested within a list item. ```markdown - [`LONG_INTRA_DOC_LINK`]: this looks like an intra-doc link, but is actually a refdef. The first line will seem to disappear when rendered as HTML. ``` > - [`LONG_INTRA_DOC_LINK`]: this > looks like an intra-doc link, > but is actually a refdef. > The first line will seem to > disappear when rendered as HTML. changelog: [`doc_nested_refdefs`]: add suspicious lint for link def at start of list items and block quotes
2 parents df46e4c + 8dd45f1 commit 66b15ad

9 files changed

+812
-5
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5490,6 +5490,7 @@ Released 2018-09-13
54905490
[`doc_lazy_continuation`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_lazy_continuation
54915491
[`doc_link_with_quotes`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_link_with_quotes
54925492
[`doc_markdown`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown
5493+
[`doc_nested_refdefs`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_nested_refdefs
54935494
[`double_comparisons`]: https://rust-lang.github.io/rust-clippy/master/index.html#double_comparisons
54945495
[`double_must_use`]: https://rust-lang.github.io/rust-clippy/master/index.html#double_must_use
54955496
[`double_neg`]: https://rust-lang.github.io/rust-clippy/master/index.html#double_neg

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::doc::DOC_LAZY_CONTINUATION_INFO,
140140
crate::doc::DOC_LINK_WITH_QUOTES_INFO,
141141
crate::doc::DOC_MARKDOWN_INFO,
142+
crate::doc::DOC_NESTED_REFDEFS_INFO,
142143
crate::doc::EMPTY_DOCS_INFO,
143144
crate::doc::EMPTY_LINE_AFTER_DOC_COMMENTS_INFO,
144145
crate::doc::EMPTY_LINE_AFTER_OUTER_ATTR_INFO,

clippy_lints/src/doc/mod.rs

+106-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ mod too_long_first_doc_paragraph;
55

66
use clippy_config::Conf;
77
use clippy_utils::attrs::is_doc_hidden;
8-
use clippy_utils::diagnostics::{span_lint, span_lint_and_help};
8+
use clippy_utils::diagnostics::{span_lint, span_lint_and_help, span_lint_and_then};
99
use clippy_utils::macros::{is_panic, root_macro_call_first_node};
1010
use clippy_utils::ty::is_type_diagnostic_item;
1111
use clippy_utils::visitors::Visitable;
@@ -18,6 +18,7 @@ use pulldown_cmark::Tag::{BlockQuote, CodeBlock, FootnoteDefinition, Heading, It
1818
use pulldown_cmark::{BrokenLink, CodeBlockKind, CowStr, Options, TagEnd};
1919
use rustc_ast::ast::Attribute;
2020
use rustc_data_structures::fx::FxHashSet;
21+
use rustc_errors::Applicability;
2122
use rustc_hir::intravisit::{self, Visitor};
2223
use rustc_hir::{AnonConst, Expr, ImplItemKind, ItemKind, Node, Safety, TraitItemKind};
2324
use rustc_lint::{LateContext, LateLintPass, LintContext};
@@ -564,6 +565,32 @@ declare_clippy_lint! {
564565
"check if files included in documentation are behind `cfg(doc)`"
565566
}
566567

568+
declare_clippy_lint! {
569+
/// ### What it does
570+
/// Warns if a link reference definition appears at the start of a
571+
/// list item or quote.
572+
///
573+
/// ### Why is this bad?
574+
/// This is probably intended as an intra-doc link. If it is really
575+
/// supposed to be a reference definition, it can be written outside
576+
/// of the list item or quote.
577+
///
578+
/// ### Example
579+
/// ```no_run
580+
/// //! - [link]: description
581+
/// ```
582+
/// Use instead:
583+
/// ```no_run
584+
/// //! - [link][]: description (for intra-doc link)
585+
/// //!
586+
/// //! [link]: destination (for link reference definition)
587+
/// ```
588+
#[clippy::version = "1.84.0"]
589+
pub DOC_NESTED_REFDEFS,
590+
suspicious,
591+
"link reference defined in list item or quote"
592+
}
593+
567594
pub struct Documentation {
568595
valid_idents: FxHashSet<String>,
569596
check_private_items: bool,
@@ -581,6 +608,7 @@ impl Documentation {
581608
impl_lint_pass!(Documentation => [
582609
DOC_LINK_WITH_QUOTES,
583610
DOC_MARKDOWN,
611+
DOC_NESTED_REFDEFS,
584612
MISSING_SAFETY_DOC,
585613
MISSING_ERRORS_DOC,
586614
MISSING_PANICS_DOC,
@@ -832,6 +860,31 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
832860
Start(BlockQuote(_)) => {
833861
blockquote_level += 1;
834862
containers.push(Container::Blockquote);
863+
if let Some((next_event, next_range)) = events.peek() {
864+
let next_start = match next_event {
865+
End(TagEnd::BlockQuote) => next_range.end,
866+
_ => next_range.start,
867+
};
868+
if let Some(refdefrange) = looks_like_refdef(doc, range.start..next_start) &&
869+
let Some(refdefspan) = fragments.span(cx, refdefrange.clone())
870+
{
871+
span_lint_and_then(
872+
cx,
873+
DOC_NESTED_REFDEFS,
874+
refdefspan,
875+
"link reference defined in quote",
876+
|diag| {
877+
diag.span_suggestion_short(
878+
refdefspan.shrink_to_hi(),
879+
"for an intra-doc link, add `[]` between the label and the colon",
880+
"[]",
881+
Applicability::MaybeIncorrect,
882+
);
883+
diag.help("link definitions are not shown in rendered documentation");
884+
}
885+
);
886+
}
887+
}
835888
},
836889
End(TagEnd::BlockQuote) => {
837890
blockquote_level -= 1;
@@ -870,11 +923,37 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
870923
in_heading = true;
871924
}
872925
if let Start(Item) = event {
873-
if let Some((_next_event, next_range)) = events.peek() {
874-
containers.push(Container::List(next_range.start - range.start));
926+
let indent = if let Some((next_event, next_range)) = events.peek() {
927+
let next_start = match next_event {
928+
End(TagEnd::Item) => next_range.end,
929+
_ => next_range.start,
930+
};
931+
if let Some(refdefrange) = looks_like_refdef(doc, range.start..next_start) &&
932+
let Some(refdefspan) = fragments.span(cx, refdefrange.clone())
933+
{
934+
span_lint_and_then(
935+
cx,
936+
DOC_NESTED_REFDEFS,
937+
refdefspan,
938+
"link reference defined in list item",
939+
|diag| {
940+
diag.span_suggestion_short(
941+
refdefspan.shrink_to_hi(),
942+
"for an intra-doc link, add `[]` between the label and the colon",
943+
"[]",
944+
Applicability::MaybeIncorrect,
945+
);
946+
diag.help("link definitions are not shown in rendered documentation");
947+
}
948+
);
949+
refdefrange.start - range.start
950+
} else {
951+
next_range.start - range.start
952+
}
875953
} else {
876-
containers.push(Container::List(0));
877-
}
954+
0
955+
};
956+
containers.push(Container::List(indent));
878957
}
879958
ticks_unbalanced = false;
880959
paragraph_range = range;
@@ -1046,3 +1125,25 @@ impl<'tcx> Visitor<'tcx> for FindPanicUnwrap<'_, 'tcx> {
10461125
self.cx.tcx.hir()
10471126
}
10481127
}
1128+
1129+
#[expect(clippy::range_plus_one)] // inclusive ranges aren't the same type
1130+
fn looks_like_refdef(doc: &str, range: Range<usize>) -> Option<Range<usize>> {
1131+
let offset = range.start;
1132+
let mut iterator = doc.as_bytes()[range].iter().copied().enumerate();
1133+
let mut start = None;
1134+
while let Some((i, byte)) = iterator.next() {
1135+
match byte {
1136+
b'\\' => {
1137+
iterator.next();
1138+
},
1139+
b'[' => {
1140+
start = Some(i + offset);
1141+
},
1142+
b']' if let Some(start) = start => {
1143+
return Some(start..i + offset + 1);
1144+
},
1145+
_ => {},
1146+
}
1147+
}
1148+
None
1149+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// https://github.com/rust-lang/rust/issues/133150
2+
#![warn(clippy::doc_nested_refdefs)]
3+
#[rustfmt::skip]
4+
/// > [link][]: def
5+
//~^ ERROR: link reference defined in quote
6+
///
7+
/// > [link][]: def (title)
8+
//~^ ERROR: link reference defined in quote
9+
///
10+
/// > [link][]: def "title"
11+
//~^ ERROR: link reference defined in quote
12+
///
13+
/// > [link]: not def
14+
///
15+
/// > [link][]: notdef
16+
///
17+
/// > [link]\: notdef
18+
pub struct Empty;
19+
20+
#[rustfmt::skip]
21+
/// > [link][]: def
22+
//~^ ERROR: link reference defined in quote
23+
/// > inner text
24+
///
25+
/// > [link][]: def (title)
26+
//~^ ERROR: link reference defined in quote
27+
/// > inner text
28+
///
29+
/// > [link][]: def "title"
30+
//~^ ERROR: link reference defined in quote
31+
/// > inner text
32+
///
33+
/// > [link]: not def
34+
/// > inner text
35+
///
36+
/// > [link][]: notdef
37+
/// > inner text
38+
///
39+
/// > [link]\: notdef
40+
/// > inner text
41+
pub struct NotEmpty;
42+
43+
#[rustfmt::skip]
44+
/// > [link][]: def
45+
//~^ ERROR: link reference defined in quote
46+
/// >
47+
/// > inner text
48+
///
49+
/// > [link][]: def (title)
50+
//~^ ERROR: link reference defined in quote
51+
/// >
52+
/// > inner text
53+
///
54+
/// > [link][]: def "title"
55+
//~^ ERROR: link reference defined in quote
56+
/// >
57+
/// > inner text
58+
///
59+
/// > [link]: not def
60+
/// >
61+
/// > inner text
62+
///
63+
/// > [link][]: notdef
64+
/// >
65+
/// > inner text
66+
///
67+
/// > [link]\: notdef
68+
/// >
69+
/// > inner text
70+
pub struct NotEmptyLoose;
71+
72+
#[rustfmt::skip]
73+
/// > first lines
74+
/// > [link]: def
75+
///
76+
/// > first lines
77+
/// > [link]: def (title)
78+
///
79+
/// > firs lines
80+
/// > [link]: def "title"
81+
///
82+
/// > firs lines
83+
/// > [link]: not def
84+
///
85+
/// > first lines
86+
/// > [link][]: notdef
87+
///
88+
/// > first lines
89+
/// > [link]\: notdef
90+
pub struct NotAtStartTight;
91+
92+
#[rustfmt::skip]
93+
/// > first lines
94+
/// >
95+
/// > [link]: def
96+
///
97+
/// > first lines
98+
/// >
99+
/// > [link]: def (title)
100+
///
101+
/// > firs lines
102+
/// >
103+
/// > [link]: def "title"
104+
///
105+
/// > firs lines
106+
/// >
107+
/// > [link]: not def
108+
///
109+
/// > first lines
110+
/// >
111+
/// > [link][]: notdef
112+
///
113+
/// > first lines
114+
/// >
115+
/// > [link]\: notdef
116+
pub struct NotAtStartLoose;
117+
118+
#[rustfmt::skip]
119+
/// > - [link][]: def
120+
//~^ ERROR: link reference defined in list item
121+
/// >
122+
/// > - [link][]: def (title)
123+
//~^ ERROR: link reference defined in list item
124+
/// >
125+
/// > - [link][]: def "title"
126+
//~^ ERROR: link reference defined in list item
127+
/// >
128+
/// > - [link]: not def
129+
/// >
130+
/// > - [link][]: notdef
131+
/// >
132+
/// > - [link]\: notdef
133+
pub struct ListNested;

0 commit comments

Comments
 (0)