Skip to content

Commit 30e0b69

Browse files
committed
Auto merge of rust-lang#12993 - GuillaumeGomez:too_long_first_doc_paragraph, r=Centri3
Add new `too_long_first_doc_paragraph` first paragraph lint Fixes rust-lang/rust-clippy#12989. changelog: Add new `too_long_first_doc_paragraph` first paragraph lint
2 parents 497177f + a203342 commit 30e0b69

13 files changed

+327
-56
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5914,6 +5914,7 @@ Released 2018-09-13
59145914
[`to_string_in_format_args`]: https://rust-lang.github.io/rust-clippy/master/index.html#to_string_in_format_args
59155915
[`to_string_trait_impl`]: https://rust-lang.github.io/rust-clippy/master/index.html#to_string_trait_impl
59165916
[`todo`]: https://rust-lang.github.io/rust-clippy/master/index.html#todo
5917+
[`too_long_first_doc_paragraph`]: https://rust-lang.github.io/rust-clippy/master/index.html#too_long_first_doc_paragraph
59175918
[`too_many_arguments`]: https://rust-lang.github.io/rust-clippy/master/index.html#too_many_arguments
59185919
[`too_many_lines`]: https://rust-lang.github.io/rust-clippy/master/index.html#too_many_lines
59195920
[`toplevel_ref_arg`]: https://rust-lang.github.io/rust-clippy/master/index.html#toplevel_ref_arg

clippy_lints/src/declared_lints.rs

+1
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
144144
crate::doc::NEEDLESS_DOCTEST_MAIN_INFO,
145145
crate::doc::SUSPICIOUS_DOC_COMMENTS_INFO,
146146
crate::doc::TEST_ATTR_IN_DOCTEST_INFO,
147+
crate::doc::TOO_LONG_FIRST_DOC_PARAGRAPH_INFO,
147148
crate::doc::UNNECESSARY_SAFETY_DOC_INFO,
148149
crate::double_parens::DOUBLE_PARENS_INFO,
149150
crate::drop_forget_ref::DROP_NON_DROP_INFO,

clippy_lints/src/doc/mod.rs

+83-31
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: 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,50 @@ 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(
497+
cx,
498+
item,
499+
attrs,
500+
headers.first_paragraph_len,
501+
self.check_private_items,
502+
);
503+
match item.kind {
504+
ItemKind::Fn(sig, _, body_id) => {
505+
if !(is_entrypoint_fn(cx, item.owner_id.to_def_id())
506+
|| in_external_macro(cx.tcx.sess, item.span))
507+
{
508+
let body = cx.tcx.hir().body(body_id);
509+
510+
let panic_info = FindPanicUnwrap::find_span(cx, cx.tcx.typeck(item.owner_id), body.value);
511+
missing_headers::check(
512+
cx,
513+
item.owner_id,
514+
sig,
515+
headers,
516+
Some(body_id),
517+
panic_info,
518+
self.check_private_items,
519+
);
520+
}
521+
},
522+
ItemKind::Trait(_, unsafety, ..) => match (headers.safety, unsafety) {
523+
(false, Safety::Unsafe) => span_lint(
467524
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-
),
525+
MISSING_SAFETY_DOC,
526+
cx.tcx.def_span(item.owner_id),
527+
"docs for unsafe trait missing `# Safety` section",
528+
),
529+
(true, Safety::Safe) => span_lint(
530+
cx,
531+
UNNECESSARY_SAFETY_DOC,
532+
cx.tcx.def_span(item.owner_id),
533+
"docs for safe trait have unnecessary `# Safety` section",
534+
),
535+
_ => (),
536+
},
490537
_ => (),
491-
},
492-
_ => (),
538+
}
493539
},
494540
Node::TraitItem(trait_item) => {
495541
if let TraitItemKind::Fn(sig, ..) = trait_item.kind
@@ -547,6 +593,7 @@ struct DocHeaders {
547593
safety: bool,
548594
errors: bool,
549595
panics: bool,
596+
first_paragraph_len: usize,
550597
}
551598

552599
/// Does some pre-processing on raw, desugared `#[doc]` attributes such as parsing them and
@@ -653,6 +700,7 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
653700
let mut paragraph_range = 0..0;
654701
let mut code_level = 0;
655702
let mut blockquote_level = 0;
703+
let mut is_first_paragraph = true;
656704

657705
let mut containers = Vec::new();
658706

@@ -720,6 +768,10 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
720768
}
721769
ticks_unbalanced = false;
722770
paragraph_range = range;
771+
if is_first_paragraph {
772+
headers.first_paragraph_len = doc[paragraph_range.clone()].chars().count();
773+
is_first_paragraph = false;
774+
}
723775
},
724776
End(TagEnd::Heading(_) | TagEnd::Paragraph | TagEnd::Item) => {
725777
if let End(TagEnd::Heading(_)) = event {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
use rustc_ast::ast::Attribute;
2+
use rustc_errors::Applicability;
3+
use rustc_hir::{Item, ItemKind};
4+
use rustc_lint::LateContext;
5+
6+
use clippy_utils::diagnostics::span_lint_and_then;
7+
use clippy_utils::is_from_proc_macro;
8+
use clippy_utils::source::snippet_opt;
9+
10+
use super::TOO_LONG_FIRST_DOC_PARAGRAPH;
11+
12+
pub(super) fn check(
13+
cx: &LateContext<'_>,
14+
item: &Item<'_>,
15+
attrs: &[Attribute],
16+
mut first_paragraph_len: usize,
17+
check_private_items: bool,
18+
) {
19+
if !check_private_items && !cx.effective_visibilities.is_exported(item.owner_id.def_id) {
20+
return;
21+
}
22+
if first_paragraph_len <= 200
23+
|| !matches!(
24+
item.kind,
25+
// This is the list of items which can be documented AND are displayed on the module
26+
// page. So associated items or impl blocks are not part of this list.
27+
ItemKind::Static(..)
28+
| ItemKind::Const(..)
29+
| ItemKind::Fn(..)
30+
| ItemKind::Macro(..)
31+
| ItemKind::Mod(..)
32+
| ItemKind::TyAlias(..)
33+
| ItemKind::Enum(..)
34+
| ItemKind::Struct(..)
35+
| ItemKind::Union(..)
36+
| ItemKind::Trait(..)
37+
| ItemKind::TraitAlias(..)
38+
)
39+
{
40+
return;
41+
}
42+
43+
let mut spans = Vec::new();
44+
let mut should_suggest_empty_doc = false;
45+
46+
for attr in attrs {
47+
if let Some(doc) = attr.doc_str() {
48+
spans.push(attr.span);
49+
let doc = doc.as_str();
50+
let doc = doc.trim();
51+
if spans.len() == 1 {
52+
// We make this suggestion only if the first doc line ends with a punctuation
53+
// because it might just need to add an empty line with `///`.
54+
should_suggest_empty_doc = doc.ends_with('.') || doc.ends_with('!') || doc.ends_with('?');
55+
}
56+
let len = doc.chars().count();
57+
if len >= first_paragraph_len {
58+
break;
59+
}
60+
first_paragraph_len -= len;
61+
}
62+
}
63+
64+
let &[first_span, .., last_span] = spans.as_slice() else {
65+
return;
66+
};
67+
if is_from_proc_macro(cx, item) {
68+
return;
69+
}
70+
71+
span_lint_and_then(
72+
cx,
73+
TOO_LONG_FIRST_DOC_PARAGRAPH,
74+
first_span.with_hi(last_span.lo()),
75+
"first doc comment paragraph is too long",
76+
|diag| {
77+
if should_suggest_empty_doc
78+
&& let Some(second_span) = spans.get(1)
79+
&& let new_span = first_span.with_hi(second_span.lo()).with_lo(first_span.hi())
80+
&& let Some(snippet) = snippet_opt(cx, new_span)
81+
{
82+
diag.span_suggestion(
83+
new_span,
84+
"add an empty line",
85+
format!("{snippet}///\n"),
86+
Applicability::MachineApplicable,
87+
);
88+
}
89+
},
90+
);
91+
}

clippy_utils/src/lib.rs

+13-8
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ macro_rules! extract_msrv_attr {
149149

150150
/// If the given expression is a local binding, find the initializer expression.
151151
/// If that initializer expression is another local binding, find its initializer again.
152+
///
152153
/// This process repeats as long as possible (but usually no more than once). Initializer
153154
/// expressions with adjustments are ignored. If this is not desired, use [`find_binding_init`]
154155
/// instead.
@@ -179,6 +180,7 @@ pub fn expr_or_init<'a, 'b, 'tcx: 'b>(cx: &LateContext<'tcx>, mut expr: &'a Expr
179180
}
180181

181182
/// Finds the initializer expression for a local binding. Returns `None` if the binding is mutable.
183+
///
182184
/// By only considering immutable bindings, we guarantee that the returned expression represents the
183185
/// value of the binding wherever it is referenced.
184186
///
@@ -431,12 +433,12 @@ pub fn qpath_generic_tys<'tcx>(qpath: &QPath<'tcx>) -> impl Iterator<Item = &'tc
431433
})
432434
}
433435

434-
/// THIS METHOD IS DEPRECATED and will eventually be removed since it does not match against the
436+
/// THIS METHOD IS DEPRECATED. Matches a `QPath` against a slice of segment string literals.
437+
///
438+
/// This method is deprecated and will eventually be removed since it does not match against the
435439
/// entire path or resolved `DefId`. Prefer using `match_def_path`. Consider getting a `DefId` from
436440
/// `QPath::Resolved.1.res.opt_def_id()`.
437441
///
438-
/// Matches a `QPath` against a slice of segment string literals.
439-
///
440442
/// There is also `match_path` if you are dealing with a `rustc_hir::Path` instead of a
441443
/// `rustc_hir::QPath`.
442444
///
@@ -485,12 +487,12 @@ pub fn is_path_diagnostic_item<'tcx>(
485487
path_def_id(cx, maybe_path).map_or(false, |id| cx.tcx.is_diagnostic_item(diag_item, id))
486488
}
487489

488-
/// THIS METHOD IS DEPRECATED and will eventually be removed since it does not match against the
490+
/// THIS METHOD IS DEPRECATED. Matches a `Path` against a slice of segment string literals.
491+
///
492+
/// This method is deprecated and will eventually be removed since it does not match against the
489493
/// entire path or resolved `DefId`. Prefer using `match_def_path`. Consider getting a `DefId` from
490494
/// `QPath::Resolved.1.res.opt_def_id()`.
491495
///
492-
/// Matches a `Path` against a slice of segment string literals.
493-
///
494496
/// There is also `match_qpath` if you are dealing with a `rustc_hir::QPath` instead of a
495497
/// `rustc_hir::Path`.
496498
///
@@ -905,6 +907,7 @@ pub fn is_default_equivalent_call(cx: &LateContext<'_>, repl_func: &Expr<'_>) ->
905907
}
906908

907909
/// Returns true if the expr is equal to `Default::default()` of it's type when evaluated.
910+
///
908911
/// It doesn't cover all cases, for example indirect function calls (some of std
909912
/// functions are supported) but it is the best we have.
910913
pub fn is_default_equivalent(cx: &LateContext<'_>, e: &Expr<'_>) -> bool {
@@ -1061,6 +1064,7 @@ impl std::ops::BitOrAssign for CaptureKind {
10611064
}
10621065

10631066
/// Given an expression referencing a local, determines how it would be captured in a closure.
1067+
///
10641068
/// Note as this will walk up to parent expressions until the capture can be determined it should
10651069
/// only be used while making a closure somewhere a value is consumed. e.g. a block, match arm, or
10661070
/// function argument (other than a receiver).
@@ -2365,8 +2369,9 @@ pub fn fn_def_id_with_node_args<'tcx>(
23652369
}
23662370

23672371
/// Returns `Option<String>` where String is a textual representation of the type encapsulated in
2368-
/// the slice iff the given expression is a slice of primitives (as defined in the
2369-
/// `is_recursively_primitive_type` function) and `None` otherwise.
2372+
/// the slice iff the given expression is a slice of primitives.
2373+
///
2374+
/// (As defined in the `is_recursively_primitive_type` function.) Returns `None` otherwise.
23702375
pub fn is_slice_of_primitives(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<String> {
23712376
let expr_type = cx.typeck_results().expr_ty_adjusted(expr);
23722377
let expr_kind = expr_type.kind();

clippy_utils/src/macros.rs

+5-4
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,11 @@ pub fn first_node_macro_backtrace(cx: &LateContext<'_>, node: &impl HirNode) ->
150150
}
151151

152152
/// If `node` is the "first node" in a macro expansion, returns `Some` with the `ExpnId` of the
153-
/// macro call site (i.e. the parent of the macro expansion). This generally means that `node`
154-
/// is the outermost node of an entire macro expansion, but there are some caveats noted below.
155-
/// This is useful for finding macro calls while visiting the HIR without processing the macro call
156-
/// at every node within its expansion.
153+
/// macro call site (i.e. the parent of the macro expansion).
154+
///
155+
/// This generally means that `node` is the outermost node of an entire macro expansion, but there
156+
/// are some caveats noted below. This is useful for finding macro calls while visiting the HIR
157+
/// without processing the macro call at every node within its expansion.
157158
///
158159
/// If you already have immediate access to the parent node, it is simpler to
159160
/// just check the context of that span directly (e.g. `parent.span.from_expansion()`).

clippy_utils/src/source.rs

+8-6
Original file line numberDiff line numberDiff line change
@@ -589,9 +589,10 @@ pub fn snippet_block_with_context<'a>(
589589
(reindent_multiline(snip, true, indent), from_macro)
590590
}
591591

592-
/// Same as `snippet_with_applicability`, but first walks the span up to the given context. This
593-
/// will result in the macro call, rather than the expansion, if the span is from a child context.
594-
/// If the span is not from a child context, it will be used directly instead.
592+
/// Same as `snippet_with_applicability`, but first walks the span up to the given context.
593+
///
594+
/// This will result in the macro call, rather than the expansion, if the span is from a child
595+
/// context. If the span is not from a child context, it will be used directly instead.
595596
///
596597
/// e.g. Given the expression `&vec![]`, getting a snippet from the span for `vec![]` as a HIR node
597598
/// would result in `box []`. If given the context of the address of expression, this function will
@@ -634,9 +635,10 @@ fn snippet_with_context_sess<'a>(
634635
}
635636

636637
/// Walks the span up to the target context, thereby returning the macro call site if the span is
637-
/// inside a macro expansion, or the original span if it is not. Note this will return `None` in the
638-
/// case of the span being in a macro expansion, but the target context is from expanding a macro
639-
/// argument.
638+
/// inside a macro expansion, or the original span if it is not.
639+
///
640+
/// Note this will return `None` in the case of the span being in a macro expansion, but the target
641+
/// context is from expanding a macro argument.
640642
///
641643
/// Given the following
642644
///

0 commit comments

Comments
 (0)