Skip to content

Commit 855a9d1

Browse files
Add new too_long_first_doc_paragraph first paragraph lint
1 parent 345c94c commit 855a9d1

9 files changed

+270
-33
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5913,6 +5913,7 @@ Released 2018-09-13
59135913
[`to_string_in_format_args`]: https://rust-lang.github.io/rust-clippy/master/index.html#to_string_in_format_args
59145914
[`to_string_trait_impl`]: https://rust-lang.github.io/rust-clippy/master/index.html#to_string_trait_impl
59155915
[`todo`]: https://rust-lang.github.io/rust-clippy/master/index.html#todo
5916+
[`too_long_first_doc_paragraph`]: https://rust-lang.github.io/rust-clippy/master/index.html#too_long_first_doc_paragraph
59165917
[`too_many_arguments`]: https://rust-lang.github.io/rust-clippy/master/index.html#too_many_arguments
59175918
[`too_many_lines`]: https://rust-lang.github.io/rust-clippy/master/index.html#too_many_lines
59185919
[`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: 80 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
mod lazy_continuation;
2+
mod too_long_first_doc_paragraph;
3+
24
use clippy_config::Conf;
35
use clippy_utils::attrs::is_doc_hidden;
46
use clippy_utils::diagnostics::{span_lint, span_lint_and_help};
@@ -422,6 +424,38 @@ declare_clippy_lint! {
422424
"require every line of a paragraph to be indented and marked"
423425
}
424426

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

453488
impl<'tcx> LateLintPass<'tcx> for Documentation {
@@ -457,39 +492,44 @@ impl<'tcx> LateLintPass<'tcx> for Documentation {
457492
};
458493

459494
match cx.tcx.hir_node(cx.last_node_with_lint_attrs) {
460-
Node::Item(item) => match item.kind {
461-
ItemKind::Fn(sig, _, body_id) => {
462-
if !(is_entrypoint_fn(cx, item.owner_id.to_def_id()) || in_external_macro(cx.tcx.sess, item.span)) {
463-
let body = cx.tcx.hir().body(body_id);
464-
465-
let panic_info = FindPanicUnwrap::find_span(cx, cx.tcx.typeck(item.owner_id), body.value);
466-
missing_headers::check(
495+
Node::Item(item) => {
496+
too_long_first_doc_paragraph::check(cx, attrs, item.kind, headers.first_paragraph_len);
497+
match item.kind {
498+
ItemKind::Fn(sig, _, body_id) => {
499+
if !(is_entrypoint_fn(cx, item.owner_id.to_def_id())
500+
|| in_external_macro(cx.tcx.sess, item.span))
501+
{
502+
let body = cx.tcx.hir().body(body_id);
503+
504+
let panic_info = FindPanicUnwrap::find_span(cx, cx.tcx.typeck(item.owner_id), body.value);
505+
missing_headers::check(
506+
cx,
507+
item.owner_id,
508+
sig,
509+
headers,
510+
Some(body_id),
511+
panic_info,
512+
self.check_private_items,
513+
);
514+
}
515+
},
516+
ItemKind::Trait(_, unsafety, ..) => match (headers.safety, unsafety) {
517+
(false, Safety::Unsafe) => span_lint(
467518
cx,
468-
item.owner_id,
469-
sig,
470-
headers,
471-
Some(body_id),
472-
panic_info,
473-
self.check_private_items,
474-
);
475-
}
476-
},
477-
ItemKind::Trait(_, unsafety, ..) => match (headers.safety, unsafety) {
478-
(false, Safety::Unsafe) => span_lint(
479-
cx,
480-
MISSING_SAFETY_DOC,
481-
cx.tcx.def_span(item.owner_id),
482-
"docs for unsafe trait missing `# Safety` section",
483-
),
484-
(true, Safety::Safe) => span_lint(
485-
cx,
486-
UNNECESSARY_SAFETY_DOC,
487-
cx.tcx.def_span(item.owner_id),
488-
"docs for safe trait have unnecessary `# Safety` section",
489-
),
519+
MISSING_SAFETY_DOC,
520+
cx.tcx.def_span(item.owner_id),
521+
"docs for unsafe trait missing `# Safety` section",
522+
),
523+
(true, Safety::Safe) => span_lint(
524+
cx,
525+
UNNECESSARY_SAFETY_DOC,
526+
cx.tcx.def_span(item.owner_id),
527+
"docs for safe trait have unnecessary `# Safety` section",
528+
),
529+
_ => (),
530+
},
490531
_ => (),
491-
},
492-
_ => (),
532+
}
493533
},
494534
Node::TraitItem(trait_item) => {
495535
if let TraitItemKind::Fn(sig, ..) = trait_item.kind
@@ -547,6 +587,7 @@ struct DocHeaders {
547587
safety: bool,
548588
errors: bool,
549589
panics: bool,
590+
first_paragraph_len: usize,
550591
}
551592

552593
/// Does some pre-processing on raw, desugared `#[doc]` attributes such as parsing them and
@@ -586,8 +627,9 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
586627
acc
587628
});
588629
doc.pop();
630+
let doc = doc.trim();
589631

590-
if doc.trim().is_empty() {
632+
if doc.is_empty() {
591633
if let Some(span) = span_of_fragments(&fragments) {
592634
span_lint_and_help(
593635
cx,
@@ -611,7 +653,7 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
611653
cx,
612654
valid_idents,
613655
parser.into_offset_iter(),
614-
&doc,
656+
doc,
615657
Fragments {
616658
fragments: &fragments,
617659
doc: &doc,
@@ -653,6 +695,7 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
653695
let mut paragraph_range = 0..0;
654696
let mut code_level = 0;
655697
let mut blockquote_level = 0;
698+
let mut is_first_paragraph = true;
656699

657700
let mut containers = Vec::new();
658701

@@ -720,6 +763,10 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
720763
}
721764
ticks_unbalanced = false;
722765
paragraph_range = range;
766+
if is_first_paragraph {
767+
headers.first_paragraph_len = doc[paragraph_range.clone()].chars().count();
768+
is_first_paragraph = false;
769+
}
723770
},
724771
End(TagEnd::Heading(_) | TagEnd::Paragraph | TagEnd::Item) => {
725772
if let End(TagEnd::Heading(_)) = event {
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
use rustc_ast::ast::Attribute;
2+
use rustc_errors::Applicability;
3+
use rustc_hir::ItemKind;
4+
use rustc_lint::LateContext;
5+
6+
use clippy_utils::diagnostics::{span_lint, span_lint_and_then};
7+
use clippy_utils::source::snippet_opt;
8+
9+
use super::TOO_LONG_FIRST_DOC_PARAGRAPH;
10+
11+
pub(super) fn check(
12+
cx: &LateContext<'_>,
13+
attrs: &[Attribute],
14+
item_kind: ItemKind<'_>,
15+
mut first_paragraph_len: usize,
16+
) {
17+
if first_paragraph_len <= 100
18+
|| !matches!(
19+
item_kind,
20+
ItemKind::Static(..)
21+
| ItemKind::Const(..)
22+
| ItemKind::Fn(..)
23+
| ItemKind::Macro(..)
24+
| ItemKind::Mod(..)
25+
| ItemKind::TyAlias(..)
26+
| ItemKind::Enum(..)
27+
| ItemKind::Struct(..)
28+
| ItemKind::Union(..)
29+
| ItemKind::Trait(..)
30+
| ItemKind::TraitAlias(..)
31+
)
32+
{
33+
return;
34+
}
35+
let mut spans = Vec::new();
36+
let mut should_suggest_empty_doc = false;
37+
38+
for attr in attrs {
39+
if let Some(doc) = attr.doc_str() {
40+
spans.push(attr.span);
41+
let doc = doc.as_str();
42+
let doc = doc.trim();
43+
if spans.len() == 1 {
44+
// We make this suggestion only if the first doc line ends with a punctuation
45+
// because if might just need to add an empty line with `///`.
46+
should_suggest_empty_doc = doc.ends_with('.') || doc.ends_with('!') || doc.ends_with('?');
47+
}
48+
let len = doc.chars().count();
49+
if len >= first_paragraph_len {
50+
break;
51+
}
52+
first_paragraph_len -= len;
53+
}
54+
}
55+
56+
let &[first_span, .., last_span] = spans.as_slice() else { return };
57+
58+
if should_suggest_empty_doc
59+
&& let Some(second_span) = spans.get(1)
60+
&& let new_span = first_span.with_hi(second_span.lo()).with_lo(first_span.hi())
61+
&& let Some(snippet) = snippet_opt(cx, new_span)
62+
{
63+
span_lint_and_then(
64+
cx,
65+
TOO_LONG_FIRST_DOC_PARAGRAPH,
66+
first_span.with_hi(last_span.lo()),
67+
"first doc comment paragraph is too long",
68+
|diag| {
69+
diag.span_suggestion(
70+
new_span,
71+
"add",
72+
format!("{snippet}///\n"),
73+
Applicability::MachineApplicable,
74+
);
75+
},
76+
);
77+
return;
78+
}
79+
span_lint(
80+
cx,
81+
TOO_LONG_FIRST_DOC_PARAGRAPH,
82+
first_span.with_hi(last_span.lo()),
83+
"first doc comment paragraph is too long",
84+
);
85+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#![warn(clippy::too_long_first_doc_paragraph)]
2+
3+
/// A very short summary.
4+
///
5+
/// A much longer explanation that goes into a lot more detail about
6+
/// how the thing works, possibly with doclinks and so one,
7+
/// and probably spanning a many rows.
8+
struct Foo;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#![warn(clippy::too_long_first_doc_paragraph)]
2+
3+
/// A very short summary.
4+
/// A much longer explanation that goes into a lot more detail about
5+
/// how the thing works, possibly with doclinks and so one,
6+
/// and probably spanning a many rows.
7+
struct Foo;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
error: first doc comment paragraph is too long
2+
--> tests/ui/too_long_first_doc_paragraph-fix.rs:3:1
3+
|
4+
LL | / /// A very short summary.
5+
LL | | /// A much longer explanation that goes into a lot more detail about
6+
LL | | /// how the thing works, possibly with doclinks and so one,
7+
LL | | /// and probably spanning a many rows.
8+
| |_
9+
|
10+
= note: `-D clippy::too-long-first-doc-paragraph` implied by `-D warnings`
11+
= help: to override `-D warnings` add `#[allow(clippy::too_long_first_doc_paragraph)]`
12+
help: add an empty line
13+
|
14+
LL ~ /// A very short summary.
15+
LL + ///
16+
|
17+
18+
error: aborting due to 1 previous error
19+
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//@no-rustfix
2+
3+
#![warn(clippy::too_long_first_doc_paragraph)]
4+
5+
/// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc turpis nunc, lacinia
6+
/// a dolor in, pellentesque aliquet enim. Cras nec maximus sem. Mauris arcu libero,
7+
/// gravida non lacinia at, rhoncus eu lacus.
8+
pub struct Bar;
9+
10+
// Should not warn! (not an item visible on mod page)
11+
/// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc turpis nunc, lacinia
12+
/// a dolor in, pellentesque aliquet enim. Cras nec maximus sem. Mauris arcu libero,
13+
/// gravida non lacinia at, rhoncus eu lacus.
14+
impl Bar {}
15+
16+
// Should not warn! (less than 80 characters)
17+
/// Lorem ipsum dolor sit amet, consectetur adipiscing elit.
18+
///
19+
/// Nunc turpis nunc, lacinia
20+
/// a dolor in, pellentesque aliquet enim. Cras nec maximus sem. Mauris arcu libero,
21+
/// gravida non lacinia at, rhoncus eu lacus.
22+
enum Enum {
23+
A,
24+
}
25+
26+
/// Lorem
27+
/// ipsum dolor sit amet, consectetur adipiscing elit. Nunc turpis nunc, lacinia
28+
/// a dolor in, pellentesque aliquet enim. Cras nec maximus sem. Mauris arcu libero,
29+
/// gravida non lacinia at, rhoncus eu lacus.
30+
union Union {
31+
a: u8,
32+
b: u8,
33+
}
34+
35+
// Should not warn! (title)
36+
/// # bla
37+
/// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc turpis nunc, lacinia
38+
/// a dolor in, pellentesque aliquet enim. Cras nec maximus sem. Mauris arcu libero,
39+
/// gravida non lacinia at, rhoncus eu lacus.
40+
union Union2 {
41+
a: u8,
42+
b: u8,
43+
}
44+
45+
fn main() {
46+
// test code goes here
47+
}
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:5: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:26: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)