Skip to content

Commit 89c4707

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

6 files changed

+207
-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: 79 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,38 @@ 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 not
427+
/// too long.
428+
///
429+
/// ### Why is this bad?
430+
/// Documentation will show the first paragraph of the doscstring in the summary page of a
431+
/// module, so having a nice, short summary in the first paragraph is part of writing good docs.
432+
///
433+
/// ### Example
434+
/// ```no_run
435+
/// /// A very short summary.
436+
/// /// A much longer explanation that goes into a lot more detail about
437+
/// /// how the thing works, possibly with doclinks and so one,
438+
/// /// and probably spanning a many rows.
439+
/// struct Foo {}
440+
/// ```
441+
/// Use instead:
442+
/// ```no_run
443+
/// /// A very short summary.
444+
/// ///
445+
/// /// A much longer explanation that goes into a lot more detail about
446+
/// /// how the thing works, possibly with doclinks and so one,
447+
/// /// and probably spanning a many rows.
448+
/// struct Foo {}
449+
/// ```
450+
#[clippy::version = "1.81.0"]
451+
pub TOO_LONG_FIRST_DOC_PARAGRAPH,
452+
style,
453+
"ensure that the first line of a documentation paragraph isn't too long"
454+
}
455+
423456
#[derive(Clone)]
424457
pub struct Documentation {
425458
valid_idents: FxHashSet<String>,
@@ -447,6 +480,7 @@ impl_lint_pass!(Documentation => [
447480
SUSPICIOUS_DOC_COMMENTS,
448481
EMPTY_DOCS,
449482
DOC_LAZY_CONTINUATION,
483+
TOO_LONG_FIRST_DOC_PARAGRAPH,
450484
]);
451485

452486
impl<'tcx> LateLintPass<'tcx> for Documentation {
@@ -456,39 +490,44 @@ impl<'tcx> LateLintPass<'tcx> for Documentation {
456490
};
457491

458492
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(
493+
Node::Item(item) => {
494+
too_long_first_doc_paragraph::check(cx, attrs, item.kind, headers.first_paragraph_len);
495+
match item.kind {
496+
ItemKind::Fn(sig, _, body_id) => {
497+
if !(is_entrypoint_fn(cx, item.owner_id.to_def_id())
498+
|| in_external_macro(cx.tcx.sess, item.span))
499+
{
500+
let body = cx.tcx.hir().body(body_id);
501+
502+
let panic_info = FindPanicUnwrap::find_span(cx, cx.tcx.typeck(item.owner_id), body.value);
503+
missing_headers::check(
504+
cx,
505+
item.owner_id,
506+
sig,
507+
headers,
508+
Some(body_id),
509+
panic_info,
510+
self.check_private_items,
511+
);
512+
}
513+
},
514+
ItemKind::Trait(_, unsafety, ..) => match (headers.safety, unsafety) {
515+
(false, Safety::Unsafe) => span_lint(
466516
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-
),
517+
MISSING_SAFETY_DOC,
518+
cx.tcx.def_span(item.owner_id),
519+
"docs for unsafe trait missing `# Safety` section",
520+
),
521+
(true, Safety::Safe) => span_lint(
522+
cx,
523+
UNNECESSARY_SAFETY_DOC,
524+
cx.tcx.def_span(item.owner_id),
525+
"docs for safe trait have unnecessary `# Safety` section",
526+
),
527+
_ => (),
528+
},
489529
_ => (),
490-
},
491-
_ => (),
530+
}
492531
},
493532
Node::TraitItem(trait_item) => {
494533
if let TraitItemKind::Fn(sig, ..) = trait_item.kind
@@ -546,6 +585,7 @@ struct DocHeaders {
546585
safety: bool,
547586
errors: bool,
548587
panics: bool,
588+
first_paragraph_len: usize,
549589
}
550590

551591
/// Does some pre-processing on raw, desugared `#[doc]` attributes such as parsing them and
@@ -585,8 +625,9 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
585625
acc
586626
});
587627
doc.pop();
628+
let doc = doc.trim();
588629

589-
if doc.trim().is_empty() {
630+
if doc.is_empty() {
590631
if let Some(span) = span_of_fragments(&fragments) {
591632
span_lint_and_help(
592633
cx,
@@ -610,7 +651,7 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
610651
cx,
611652
valid_idents,
612653
parser.into_offset_iter(),
613-
&doc,
654+
doc,
614655
Fragments {
615656
fragments: &fragments,
616657
doc: &doc,
@@ -652,6 +693,7 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
652693
let mut paragraph_range = 0..0;
653694
let mut code_level = 0;
654695
let mut blockquote_level = 0;
696+
let mut is_first_paragraph = true;
655697

656698
let mut containers = Vec::new();
657699

@@ -719,6 +761,10 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
719761
}
720762
ticks_unbalanced = false;
721763
paragraph_range = range;
764+
if is_first_paragraph {
765+
headers.first_paragraph_len = doc[paragraph_range.clone()].chars().count();
766+
is_first_paragraph = false;
767+
}
722768
},
723769
End(Heading(_, _, _) | Paragraph | Item) => {
724770
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)