Skip to content

Commit b043fe3

Browse files
Add new too_long_first_doc_paragraph first paragraph lint
1 parent 8631790 commit b043fe3

File tree

6 files changed

+206
-33
lines changed

6 files changed

+206
-33
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5864,6 +5864,7 @@ Released 2018-09-13
58645864
[`to_string_in_format_args`]: https://rust-lang.github.io/rust-clippy/master/index.html#to_string_in_format_args
58655865
[`to_string_trait_impl`]: https://rust-lang.github.io/rust-clippy/master/index.html#to_string_trait_impl
58665866
[`todo`]: https://rust-lang.github.io/rust-clippy/master/index.html#todo
5867+
[`too_long_first_doc_paragraph`]: https://rust-lang.github.io/rust-clippy/master/index.html#too_long_first_doc_paragraph
58675868
[`too_many_arguments`]: https://rust-lang.github.io/rust-clippy/master/index.html#too_many_arguments
58685869
[`too_many_lines`]: https://rust-lang.github.io/rust-clippy/master/index.html#too_many_lines
58695870
[`toplevel_ref_arg`]: https://rust-lang.github.io/rust-clippy/master/index.html#toplevel_ref_arg

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
148148
crate::doc::NEEDLESS_DOCTEST_MAIN_INFO,
149149
crate::doc::SUSPICIOUS_DOC_COMMENTS_INFO,
150150
crate::doc::TEST_ATTR_IN_DOCTEST_INFO,
151+
crate::doc::TOO_LONG_FIRST_DOC_PARAGRAPH_INFO,
151152
crate::doc::UNNECESSARY_SAFETY_DOC_INFO,
152153
crate::double_parens::DOUBLE_PARENS_INFO,
153154
crate::drop_forget_ref::DROP_NON_DROP_INFO,

clippy_lints/src/doc/mod.rs

Lines changed: 78 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod lazy_continuation;
2+
mod too_long_first_doc_paragraph;
23
use clippy_utils::attrs::is_doc_hidden;
34
use clippy_utils::diagnostics::{span_lint, span_lint_and_help};
45
use clippy_utils::macros::{is_panic, root_macro_call_first_node};
@@ -420,6 +421,37 @@ declare_clippy_lint! {
420421
"require every line of a paragraph to be indented and marked"
421422
}
422423

424+
declare_clippy_lint! {
425+
/// ### What it does
426+
/// Checks if the first line in the documentation of items listed in module page is too long.
427+
///
428+
/// ### Why is this bad?
429+
/// Documentation will show the first paragraph of the doscstring in the summary page of a
430+
/// module, so having a nice, short summary in the first paragraph is part of writing good docs.
431+
///
432+
/// ### Example
433+
/// ```no_run
434+
/// /// A very short summary.
435+
/// /// A much longer explanation that goes into a lot more detail about
436+
/// /// how the thing works, possibly with doclinks and so one,
437+
/// /// and probably spanning a many rows.
438+
/// struct Foo {}
439+
/// ```
440+
/// Use instead:
441+
/// ```no_run
442+
/// /// A very short summary.
443+
/// ///
444+
/// /// A much longer explanation that goes into a lot more detail about
445+
/// /// how the thing works, possibly with doclinks and so one,
446+
/// /// and probably spanning a many rows.
447+
/// struct Foo {}
448+
/// ```
449+
#[clippy::version = "1.81.0"]
450+
pub TOO_LONG_FIRST_DOC_PARAGRAPH,
451+
style,
452+
"ensure that the first line of a documentation paragraph isn't too long"
453+
}
454+
423455
#[derive(Clone)]
424456
pub struct Documentation {
425457
valid_idents: FxHashSet<String>,
@@ -447,6 +479,7 @@ impl_lint_pass!(Documentation => [
447479
SUSPICIOUS_DOC_COMMENTS,
448480
EMPTY_DOCS,
449481
DOC_LAZY_CONTINUATION,
482+
TOO_LONG_FIRST_DOC_PARAGRAPH,
450483
]);
451484

452485
impl<'tcx> LateLintPass<'tcx> for Documentation {
@@ -456,39 +489,44 @@ impl<'tcx> LateLintPass<'tcx> for Documentation {
456489
};
457490

458491
match cx.tcx.hir_node(cx.last_node_with_lint_attrs) {
459-
Node::Item(item) => match item.kind {
460-
ItemKind::Fn(sig, _, body_id) => {
461-
if !(is_entrypoint_fn(cx, item.owner_id.to_def_id()) || in_external_macro(cx.tcx.sess, item.span)) {
462-
let body = cx.tcx.hir().body(body_id);
463-
464-
let panic_info = FindPanicUnwrap::find_span(cx, cx.tcx.typeck(item.owner_id), body.value);
465-
missing_headers::check(
492+
Node::Item(item) => {
493+
too_long_first_doc_paragraph::check(cx, attrs, item.kind, headers.first_paragraph_len);
494+
match item.kind {
495+
ItemKind::Fn(sig, _, body_id) => {
496+
if !(is_entrypoint_fn(cx, item.owner_id.to_def_id())
497+
|| in_external_macro(cx.tcx.sess, item.span))
498+
{
499+
let body = cx.tcx.hir().body(body_id);
500+
501+
let panic_info = FindPanicUnwrap::find_span(cx, cx.tcx.typeck(item.owner_id), body.value);
502+
missing_headers::check(
503+
cx,
504+
item.owner_id,
505+
sig,
506+
headers,
507+
Some(body_id),
508+
panic_info,
509+
self.check_private_items,
510+
);
511+
}
512+
},
513+
ItemKind::Trait(_, unsafety, ..) => match (headers.safety, unsafety) {
514+
(false, Safety::Unsafe) => span_lint(
466515
cx,
467-
item.owner_id,
468-
sig,
469-
headers,
470-
Some(body_id),
471-
panic_info,
472-
self.check_private_items,
473-
);
474-
}
475-
},
476-
ItemKind::Trait(_, unsafety, ..) => match (headers.safety, unsafety) {
477-
(false, Safety::Unsafe) => span_lint(
478-
cx,
479-
MISSING_SAFETY_DOC,
480-
cx.tcx.def_span(item.owner_id),
481-
"docs for unsafe trait missing `# Safety` section",
482-
),
483-
(true, Safety::Safe) => span_lint(
484-
cx,
485-
UNNECESSARY_SAFETY_DOC,
486-
cx.tcx.def_span(item.owner_id),
487-
"docs for safe trait have unnecessary `# Safety` section",
488-
),
516+
MISSING_SAFETY_DOC,
517+
cx.tcx.def_span(item.owner_id),
518+
"docs for unsafe trait missing `# Safety` section",
519+
),
520+
(true, Safety::Safe) => span_lint(
521+
cx,
522+
UNNECESSARY_SAFETY_DOC,
523+
cx.tcx.def_span(item.owner_id),
524+
"docs for safe trait have unnecessary `# Safety` section",
525+
),
526+
_ => (),
527+
},
489528
_ => (),
490-
},
491-
_ => (),
529+
}
492530
},
493531
Node::TraitItem(trait_item) => {
494532
if let TraitItemKind::Fn(sig, ..) = trait_item.kind
@@ -546,6 +584,7 @@ struct DocHeaders {
546584
safety: bool,
547585
errors: bool,
548586
panics: bool,
587+
first_paragraph_len: usize,
549588
}
550589

551590
/// Does some pre-processing on raw, desugared `#[doc]` attributes such as parsing them and
@@ -585,8 +624,9 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
585624
acc
586625
});
587626
doc.pop();
627+
let doc = doc.trim();
588628

589-
if doc.trim().is_empty() {
629+
if doc.is_empty() {
590630
if let Some(span) = span_of_fragments(&fragments) {
591631
span_lint_and_help(
592632
cx,
@@ -610,7 +650,7 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
610650
cx,
611651
valid_idents,
612652
parser.into_offset_iter(),
613-
&doc,
653+
doc,
614654
Fragments {
615655
fragments: &fragments,
616656
doc: &doc,
@@ -652,6 +692,7 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
652692
let mut paragraph_range = 0..0;
653693
let mut code_level = 0;
654694
let mut blockquote_level = 0;
695+
let mut is_first_paragraph = true;
655696

656697
let mut containers = Vec::new();
657698

@@ -719,6 +760,10 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
719760
}
720761
ticks_unbalanced = false;
721762
paragraph_range = range;
763+
if is_first_paragraph {
764+
headers.first_paragraph_len = doc[paragraph_range.clone()].chars().count();
765+
is_first_paragraph = false;
766+
}
722767
},
723768
End(Heading(_, _, _) | Paragraph | Item) => {
724769
if let End(Heading(_, _, _)) = event {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use rustc_ast::ast::Attribute;
2+
use rustc_hir::ItemKind;
3+
use rustc_lint::LateContext;
4+
5+
use clippy_utils::diagnostics::span_lint;
6+
7+
use super::TOO_LONG_FIRST_DOC_PARAGRAPH;
8+
9+
pub(super) fn check(
10+
cx: &LateContext<'_>,
11+
attrs: &[Attribute],
12+
item_kind: ItemKind<'_>,
13+
mut first_paragraph_len: usize,
14+
) {
15+
if first_paragraph_len <= 80
16+
|| !matches!(
17+
item_kind,
18+
ItemKind::Static(..)
19+
| ItemKind::Const(..)
20+
| ItemKind::Fn(..)
21+
| ItemKind::Macro(..)
22+
| ItemKind::Mod(..)
23+
| ItemKind::TyAlias(..)
24+
| ItemKind::Enum(..)
25+
| ItemKind::Struct(..)
26+
| ItemKind::Union(..)
27+
| ItemKind::Trait(..)
28+
| ItemKind::TraitAlias(..)
29+
)
30+
{
31+
return;
32+
}
33+
let mut attr_start_span = None;
34+
let mut attr_end_span = None;
35+
36+
for attr in attrs {
37+
if let Some(doc) = attr.doc_str() {
38+
if attr_start_span.is_none() {
39+
attr_start_span = Some(attr.span);
40+
}
41+
let len = doc.as_str().trim().chars().count();
42+
attr_end_span = Some(attr.span);
43+
if len >= first_paragraph_len {
44+
break;
45+
} else {
46+
first_paragraph_len -= len;
47+
}
48+
}
49+
}
50+
let (Some(start_span), Some(end_span)) = (attr_start_span, attr_end_span) else {
51+
return;
52+
};
53+
span_lint(
54+
cx,
55+
TOO_LONG_FIRST_DOC_PARAGRAPH,
56+
start_span.with_hi(end_span.lo()),
57+
"first doc comment paragraph is too long",
58+
);
59+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#![warn(clippy::too_long_first_doc_paragraph)]
2+
3+
/// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc turpis nunc, lacinia
4+
/// a dolor in, pellentesque aliquet enim. Cras nec maximus sem. Mauris arcu libero,
5+
/// gravida non lacinia at, rhoncus eu lacus.
6+
pub struct Bar;
7+
8+
// Should not warn! (not an item visible on mod page)
9+
/// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc turpis nunc, lacinia
10+
/// a dolor in, pellentesque aliquet enim. Cras nec maximus sem. Mauris arcu libero,
11+
/// gravida non lacinia at, rhoncus eu lacus.
12+
impl Bar {}
13+
14+
// Should not warn! (less than 80 characters)
15+
/// Lorem ipsum dolor sit amet, consectetur adipiscing elit.
16+
///
17+
/// Nunc turpis nunc, lacinia
18+
/// a dolor in, pellentesque aliquet enim. Cras nec maximus sem. Mauris arcu libero,
19+
/// gravida non lacinia at, rhoncus eu lacus.
20+
enum Enum {
21+
A,
22+
}
23+
24+
/// Lorem
25+
/// ipsum dolor sit amet, consectetur adipiscing elit. Nunc turpis nunc, lacinia
26+
/// a dolor in, pellentesque aliquet enim. Cras nec maximus sem. Mauris arcu libero,
27+
/// gravida non lacinia at, rhoncus eu lacus.
28+
union Union {
29+
a: u8,
30+
b: u8,
31+
}
32+
33+
// Should not warn! (title)
34+
/// # bla
35+
/// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc turpis nunc, lacinia
36+
/// a dolor in, pellentesque aliquet enim. Cras nec maximus sem. Mauris arcu libero,
37+
/// gravida non lacinia at, rhoncus eu lacus.
38+
union Union2 {
39+
a: u8,
40+
b: u8,
41+
}
42+
43+
fn main() {
44+
// test code goes here
45+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
error: first doc comment paragraph is too long
2+
--> tests/ui/too_long_first_doc_paragraph.rs:3:1
3+
|
4+
LL | / /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc turpis nunc, lacinia
5+
LL | | /// a dolor in, pellentesque aliquet enim. Cras nec maximus sem. Mauris arcu libero,
6+
LL | | /// gravida non lacinia at, rhoncus eu lacus.
7+
| |_
8+
|
9+
= note: `-D clippy::too-long-first-doc-paragraph` implied by `-D warnings`
10+
= help: to override `-D warnings` add `#[allow(clippy::too_long_first_doc_paragraph)]`
11+
12+
error: first doc comment paragraph is too long
13+
--> tests/ui/too_long_first_doc_paragraph.rs:24:1
14+
|
15+
LL | / /// Lorem
16+
LL | | /// ipsum dolor sit amet, consectetur adipiscing elit. Nunc turpis nunc, lacinia
17+
LL | | /// a dolor in, pellentesque aliquet enim. Cras nec maximus sem. Mauris arcu libero,
18+
LL | | /// gravida non lacinia at, rhoncus eu lacus.
19+
| |_
20+
21+
error: aborting due to 2 previous errors
22+

0 commit comments

Comments
 (0)