Skip to content

Commit 0e5bded

Browse files
committed
Auto merge of #12770 - notriddle:notriddle/doc-lazy-continuation, r=llogiq
Add new lint `doc_lazy_continuation` changelog: [`doc_lazy_continuation`]: add lint that warns on so-called "lazy paragraph continuations" This is a follow-up for #121659, since most cases of unintended block quotes are lazy continuations. The lint is designed to be more generally useful than that, though, because it will also catch unintended list items and unintended block quotes that didn't coincidentally hit a pulldown-cmark bug. The second commit is the result of running `cargo dev dogfood --fix`, and manually fixing anything that seems wrong. NOTE: this lint's suggestions should never change the parser's interpretation of the markdown, but in many cases, it seems that doc comments in clippy were written without regard for this feature of Markdown (which, I suppose, is why this lint should exist).
2 parents 68dbc84 + 133549c commit 0e5bded

19 files changed

+597
-14
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5249,6 +5249,7 @@ Released 2018-09-13
52495249
[`disallowed_type`]: https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_type
52505250
[`disallowed_types`]: https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_types
52515251
[`diverging_sub_expression`]: https://rust-lang.github.io/rust-clippy/master/index.html#diverging_sub_expression
5252+
[`doc_lazy_continuation`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_lazy_continuation
52525253
[`doc_link_with_quotes`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_link_with_quotes
52535254
[`doc_markdown`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown
52545255
[`double_comparisons`]: https://rust-lang.github.io/rust-clippy/master/index.html#double_comparisons

clippy_lints/src/casts/cast_sign_loss.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,10 @@ fn expr_add_sign(cx: &LateContext<'_>, expr: &Expr<'_>) -> Sign {
255255

256256
/// Peels binary operators such as [`BinOpKind::Mul`], [`BinOpKind::Div`] or [`BinOpKind::Rem`],
257257
/// where the result depends on:
258+
///
258259
/// - the number of negative values in the entire expression, or
259260
/// - the number of negative values on the left hand side of the expression.
261+
///
260262
/// Ignores overflow.
261263
///
262264
///
@@ -303,8 +305,10 @@ fn exprs_with_muldiv_binop_peeled<'e>(expr: &'e Expr<'_>) -> Vec<&'e Expr<'e>> {
303305
}
304306

305307
/// Peels binary operators such as [`BinOpKind::Add`], where the result depends on:
308+
///
306309
/// - all the expressions being positive, or
307310
/// - all the expressions being negative.
311+
///
308312
/// Ignores overflow.
309313
///
310314
/// Expressions using other operators are preserved, so we can try to evaluate them later.

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
140140
crate::disallowed_names::DISALLOWED_NAMES_INFO,
141141
crate::disallowed_script_idents::DISALLOWED_SCRIPT_IDENTS_INFO,
142142
crate::disallowed_types::DISALLOWED_TYPES_INFO,
143+
crate::doc::DOC_LAZY_CONTINUATION_INFO,
143144
crate::doc::DOC_LINK_WITH_QUOTES_INFO,
144145
crate::doc::DOC_MARKDOWN_INFO,
145146
crate::doc::EMPTY_DOCS_INFO,
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
use clippy_utils::diagnostics::span_lint_and_then;
2+
use itertools::Itertools;
3+
use rustc_errors::{Applicability, SuggestionStyle};
4+
use rustc_lint::LateContext;
5+
use rustc_span::{BytePos, Span};
6+
use std::ops::Range;
7+
8+
use super::DOC_LAZY_CONTINUATION;
9+
10+
fn map_container_to_text(c: &super::Container) -> &'static str {
11+
match c {
12+
super::Container::Blockquote => "> ",
13+
// numbered list can have up to nine digits, plus the dot, plus four spaces on either side
14+
super::Container::List(indent) => &" "[0..*indent],
15+
}
16+
}
17+
18+
// TODO: Adjust the parameters as necessary
19+
pub(super) fn check(
20+
cx: &LateContext<'_>,
21+
doc: &str,
22+
range: Range<usize>,
23+
mut span: Span,
24+
containers: &[super::Container],
25+
) {
26+
if doc[range.clone()].contains('\t') {
27+
// We don't do tab stops correctly.
28+
return;
29+
}
30+
31+
let ccount = doc[range.clone()].chars().filter(|c| *c == '>').count();
32+
let blockquote_level = containers
33+
.iter()
34+
.filter(|c| matches!(c, super::Container::Blockquote))
35+
.count();
36+
let lcount = doc[range.clone()].chars().filter(|c| *c == ' ').count();
37+
let list_indentation = containers
38+
.iter()
39+
.map(|c| {
40+
if let super::Container::List(indent) = c {
41+
*indent
42+
} else {
43+
0
44+
}
45+
})
46+
.sum();
47+
if ccount < blockquote_level || lcount < list_indentation {
48+
let msg = if ccount < blockquote_level {
49+
"doc quote missing `>` marker"
50+
} else {
51+
"doc list item missing indentation"
52+
};
53+
span_lint_and_then(cx, DOC_LAZY_CONTINUATION, span, msg, |diag| {
54+
if ccount == 0 && blockquote_level == 0 {
55+
// simpler suggestion style for indentation
56+
let indent = list_indentation - lcount;
57+
diag.span_suggestion_with_style(
58+
span.shrink_to_hi(),
59+
"indent this line",
60+
std::iter::repeat(" ").take(indent).join(""),
61+
Applicability::MaybeIncorrect,
62+
SuggestionStyle::ShowAlways,
63+
);
64+
diag.help("if this is supposed to be its own paragraph, add a blank line");
65+
return;
66+
}
67+
let mut doc_start_range = &doc[range];
68+
let mut suggested = String::new();
69+
for c in containers {
70+
let text = map_container_to_text(c);
71+
if doc_start_range.starts_with(text) {
72+
doc_start_range = &doc_start_range[text.len()..];
73+
span = span
74+
.with_lo(span.lo() + BytePos(u32::try_from(text.len()).expect("text is not 2**32 or bigger")));
75+
} else if matches!(c, super::Container::Blockquote)
76+
&& let Some(i) = doc_start_range.find('>')
77+
{
78+
doc_start_range = &doc_start_range[i + 1..];
79+
span =
80+
span.with_lo(span.lo() + BytePos(u32::try_from(i).expect("text is not 2**32 or bigger") + 1));
81+
} else {
82+
suggested.push_str(text);
83+
}
84+
}
85+
diag.span_suggestion_with_style(
86+
span,
87+
"add markers to start of line",
88+
suggested,
89+
Applicability::MachineApplicable,
90+
SuggestionStyle::ShowAlways,
91+
);
92+
diag.help("if this not intended to be a quote at all, escape it with `\\>`");
93+
});
94+
}
95+
}

clippy_lints/src/doc/mod.rs

Lines changed: 110 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
mod lazy_continuation;
12
use clippy_utils::attrs::is_doc_hidden;
23
use clippy_utils::diagnostics::{span_lint, span_lint_and_help};
34
use clippy_utils::macros::{is_panic, root_macro_call_first_node};
@@ -7,7 +8,7 @@ use clippy_utils::{is_entrypoint_fn, is_trait_impl_item, method_chain_args};
78
use pulldown_cmark::Event::{
89
Code, End, FootnoteReference, HardBreak, Html, Rule, SoftBreak, Start, TaskListMarker, Text,
910
};
10-
use pulldown_cmark::Tag::{BlockQuote, CodeBlock, Heading, Item, Link, Paragraph};
11+
use pulldown_cmark::Tag::{BlockQuote, CodeBlock, FootnoteDefinition, Heading, Item, Link, Paragraph};
1112
use pulldown_cmark::{BrokenLink, CodeBlockKind, CowStr, Options};
1213
use rustc_ast::ast::Attribute;
1314
use rustc_data_structures::fx::FxHashSet;
@@ -362,6 +363,63 @@ declare_clippy_lint! {
362363
"docstrings exist but documentation is empty"
363364
}
364365

366+
declare_clippy_lint! {
367+
/// ### What it does
368+
///
369+
/// In CommonMark Markdown, the language used to write doc comments, a
370+
/// paragraph nested within a list or block quote does not need any line
371+
/// after the first one to be indented or marked. The specification calls
372+
/// this a "lazy paragraph continuation."
373+
///
374+
/// ### Why is this bad?
375+
///
376+
/// This is easy to write but hard to read. Lazy continuations makes
377+
/// unintended markers hard to see, and make it harder to deduce the
378+
/// document's intended structure.
379+
///
380+
/// ### Example
381+
///
382+
/// This table is probably intended to have two rows,
383+
/// but it does not. It has zero rows, and is followed by
384+
/// a block quote.
385+
/// ```no_run
386+
/// /// Range | Description
387+
/// /// ----- | -----------
388+
/// /// >= 1 | fully opaque
389+
/// /// < 1 | partially see-through
390+
/// fn set_opacity(opacity: f32) {}
391+
/// ```
392+
///
393+
/// Fix it by escaping the marker:
394+
/// ```no_run
395+
/// /// Range | Description
396+
/// /// ----- | -----------
397+
/// /// \>= 1 | fully opaque
398+
/// /// < 1 | partially see-through
399+
/// fn set_opacity(opacity: f32) {}
400+
/// ```
401+
///
402+
/// This example is actually intended to be a list:
403+
/// ```no_run
404+
/// /// * Do nothing.
405+
/// /// * Then do something. Whatever it is needs done,
406+
/// /// it should be done right now.
407+
/// # fn do_stuff() {}
408+
/// ```
409+
///
410+
/// Fix it by indenting the list contents:
411+
/// ```no_run
412+
/// /// * Do nothing.
413+
/// /// * Then do something. Whatever it is needs done,
414+
/// /// it should be done right now.
415+
/// # fn do_stuff() {}
416+
/// ```
417+
#[clippy::version = "1.80.0"]
418+
pub DOC_LAZY_CONTINUATION,
419+
style,
420+
"require every line of a paragraph to be indented and marked"
421+
}
422+
365423
#[derive(Clone)]
366424
pub struct Documentation {
367425
valid_idents: FxHashSet<String>,
@@ -388,6 +446,7 @@ impl_lint_pass!(Documentation => [
388446
UNNECESSARY_SAFETY_DOC,
389447
SUSPICIOUS_DOC_COMMENTS,
390448
EMPTY_DOCS,
449+
DOC_LAZY_CONTINUATION,
391450
]);
392451

393452
impl<'tcx> LateLintPass<'tcx> for Documentation {
@@ -551,6 +610,7 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
551610
cx,
552611
valid_idents,
553612
parser.into_offset_iter(),
613+
&doc,
554614
Fragments {
555615
fragments: &fragments,
556616
doc: &doc,
@@ -560,6 +620,11 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
560620

561621
const RUST_CODE: &[&str] = &["rust", "no_run", "should_panic", "compile_fail"];
562622

623+
enum Container {
624+
Blockquote,
625+
List(usize),
626+
}
627+
563628
/// Checks parsed documentation.
564629
/// This walks the "events" (think sections of markdown) produced by `pulldown_cmark`,
565630
/// so lints here will generally access that information.
@@ -569,13 +634,15 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
569634
cx: &LateContext<'_>,
570635
valid_idents: &FxHashSet<String>,
571636
events: Events,
637+
doc: &str,
572638
fragments: Fragments<'_>,
573639
) -> DocHeaders {
574640
// true if a safety header was found
575641
let mut headers = DocHeaders::default();
576642
let mut in_code = false;
577643
let mut in_link = None;
578644
let mut in_heading = false;
645+
let mut in_footnote_definition = false;
579646
let mut is_rust = false;
580647
let mut no_test = false;
581648
let mut ignore = false;
@@ -586,7 +653,11 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
586653
let mut code_level = 0;
587654
let mut blockquote_level = 0;
588655

589-
for (event, range) in events {
656+
let mut containers = Vec::new();
657+
658+
let mut events = events.peekable();
659+
660+
while let Some((event, range)) = events.next() {
590661
match event {
591662
Html(tag) => {
592663
if tag.starts_with("<code") {
@@ -599,8 +670,14 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
599670
blockquote_level -= 1;
600671
}
601672
},
602-
Start(BlockQuote) => blockquote_level += 1,
603-
End(BlockQuote) => blockquote_level -= 1,
673+
Start(BlockQuote) => {
674+
blockquote_level += 1;
675+
containers.push(Container::Blockquote);
676+
},
677+
End(BlockQuote) => {
678+
blockquote_level -= 1;
679+
containers.pop();
680+
},
604681
Start(CodeBlock(ref kind)) => {
605682
in_code = true;
606683
if let CodeBlockKind::Fenced(lang) = kind {
@@ -633,13 +710,23 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
633710
if let Start(Heading(_, _, _)) = event {
634711
in_heading = true;
635712
}
713+
if let Start(Item) = event {
714+
if let Some((_next_event, next_range)) = events.peek() {
715+
containers.push(Container::List(next_range.start - range.start));
716+
} else {
717+
containers.push(Container::List(0));
718+
}
719+
}
636720
ticks_unbalanced = false;
637721
paragraph_range = range;
638722
},
639723
End(Heading(_, _, _) | Paragraph | Item) => {
640724
if let End(Heading(_, _, _)) = event {
641725
in_heading = false;
642726
}
727+
if let End(Item) = event {
728+
containers.pop();
729+
}
643730
if ticks_unbalanced && let Some(span) = fragments.span(cx, paragraph_range.clone()) {
644731
span_lint_and_help(
645732
cx,
@@ -658,8 +745,26 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
658745
}
659746
text_to_check = Vec::new();
660747
},
748+
Start(FootnoteDefinition(..)) => in_footnote_definition = true,
749+
End(FootnoteDefinition(..)) => in_footnote_definition = false,
661750
Start(_tag) | End(_tag) => (), // We don't care about other tags
662-
SoftBreak | HardBreak | TaskListMarker(_) | Code(_) | Rule => (),
751+
SoftBreak | HardBreak => {
752+
if !containers.is_empty()
753+
&& let Some((_next_event, next_range)) = events.peek()
754+
&& let Some(next_span) = fragments.span(cx, next_range.clone())
755+
&& let Some(span) = fragments.span(cx, range.clone())
756+
&& !in_footnote_definition
757+
{
758+
lazy_continuation::check(
759+
cx,
760+
doc,
761+
range.end..next_range.start,
762+
Span::new(span.hi(), next_span.lo(), span.ctxt(), span.parent()),
763+
&containers[..],
764+
);
765+
}
766+
},
767+
TaskListMarker(_) | Code(_) | Rule => (),
663768
FootnoteReference(text) | Text(text) => {
664769
paragraph_range.end = range.end;
665770
ticks_unbalanced |= text.contains('`') && !in_code;

clippy_lints/src/manual_clamp.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,15 +611,22 @@ impl<'tcx> BinaryOp<'tcx> {
611611

612612
/// The clamp meta pattern is a pattern shared between many (but not all) patterns.
613613
/// In summary, this pattern consists of two if statements that meet many criteria,
614+
///
614615
/// - binary operators that are one of [`>`, `<`, `>=`, `<=`].
616+
///
615617
/// - Both binary statements must have a shared argument
618+
///
616619
/// - Which can appear on the left or right side of either statement
620+
///
617621
/// - The binary operators must define a finite range for the shared argument. To put this in
618622
/// the terms of Rust `std` library, the following ranges are acceptable
623+
///
619624
/// - `Range`
620625
/// - `RangeInclusive`
626+
///
621627
/// And all other range types are not accepted. For the purposes of `clamp` it's irrelevant
622628
/// whether the range is inclusive or not, the output is the same.
629+
///
623630
/// - The result of each if statement must be equal to the argument unique to that if statement. The
624631
/// result can not be the shared argument in either case.
625632
fn is_clamp_meta_pattern<'tcx>(

clippy_lints/src/methods/iter_filter.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,15 +126,15 @@ enum FilterType {
126126
///
127127
/// How this is done:
128128
/// 1. we know that this is invoked in a method call with `filter` as the method name via `mod.rs`
129-
/// 2. we check that we are in a trait method. Therefore we are in an
130-
/// `(x as Iterator).filter({filter_arg})` method call.
129+
/// 2. we check that we are in a trait method. Therefore we are in an `(x as
130+
/// Iterator).filter({filter_arg})` method call.
131131
/// 3. we check that the parent expression is not a map. This is because we don't want to lint
132132
/// twice, and we already have a specialized lint for that.
133133
/// 4. we check that the span of the filter does not contain a comment.
134134
/// 5. we get the type of the `Item` in the `Iterator`, and compare against the type of Option and
135-
/// Result.
135+
/// Result.
136136
/// 6. we finally check the contents of the filter argument to see if it is a call to `is_some` or
137-
/// `is_ok`.
137+
/// `is_ok`.
138138
/// 7. if all of the above are true, then we return the `FilterType`
139139
fn expression_type(
140140
cx: &LateContext<'_>,

clippy_lints/src/methods/iter_kv_map.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ use rustc_middle::ty;
1212
use rustc_span::{sym, Span};
1313

1414
/// lint use of:
15+
///
1516
/// - `hashmap.iter().map(|(_, v)| v)`
1617
/// - `hashmap.into_iter().map(|(_, v)| v)`
18+
///
1719
/// on `HashMaps` and `BTreeMaps` in std
1820
1921
pub(super) fn check<'tcx>(

clippy_lints/src/methods/utils.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ fn pat_bindings(pat: &Pat<'_>) -> Vec<HirId> {
120120
/// operations performed on `binding_hir_ids` are:
121121
/// * to take non-mutable references to them
122122
/// * to use them as non-mutable `&self` in method calls
123+
///
123124
/// If any of `binding_hir_ids` is used in any other way, then `clone_or_copy_needed` will be true
124125
/// when `CloneOrCopyVisitor` is done visiting.
125126
struct CloneOrCopyVisitor<'cx, 'tcx> {

0 commit comments

Comments
 (0)