about summary refs log tree commit diff
diff options
context:
space:
mode:
authorYutaro Ohno <yutaro.ono.418@gmail.com>2025-01-14 17:06:31 +0900
committerYutaro Ohno <yutaro.ono.418@gmail.com>2025-01-28 16:22:28 +0900
commit4693d0a9ffceeabed36d2e82be3cfce5e1322b30 (patch)
tree5e0ff5fa639c5f416904b0598a894c7368c08471
parent66dc8a1a30b19f31454a9e58f8723500a1a8ad17 (diff)
downloadrust-4693d0a9ffceeabed36d2e82be3cfce5e1322b30.tar.gz
rust-4693d0a9ffceeabed36d2e82be3cfce5e1322b30.zip
Add new lint `doc_overindented_list_items`
Add a new lint `doc_overindented_list_items` to detect and fix list items
in docs that are overindented.

For example,

```rs
/// - first line
///      second line
fn foo() {}
```

this would be fixed to:

```rs
/// - first line
///   second line
fn foo() {}
```

This lint improves readabiliy and consistency in doc.
-rw-r--r--CHANGELOG.md1
-rw-r--r--README.md6
-rw-r--r--clippy_lints/src/declared_lints.rs1
-rw-r--r--clippy_lints/src/doc/lazy_continuation.rs108
-rw-r--r--clippy_lints/src/doc/mod.rs34
-rw-r--r--tests/ui/doc/doc_lazy_list.fixed1
-rw-r--r--tests/ui/doc/doc_lazy_list.rs1
-rw-r--r--tests/ui/doc/doc_lazy_list.stderr22
-rw-r--r--tests/ui/doc/doc_overindented_list_items.fixed28
-rw-r--r--tests/ui/doc/doc_overindented_list_items.rs28
-rw-r--r--tests/ui/doc/doc_overindented_list_items.stderr41
11 files changed, 219 insertions, 52 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2757597fc51..bc42c07224e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5533,6 +5533,7 @@ Released 2018-09-13
 [`doc_link_with_quotes`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_link_with_quotes
 [`doc_markdown`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_markdown
 [`doc_nested_refdefs`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_nested_refdefs
+[`doc_overindented_list_items`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_overindented_list_items
 [`double_comparisons`]: https://rust-lang.github.io/rust-clippy/master/index.html#double_comparisons
 [`double_ended_iterator_last`]: https://rust-lang.github.io/rust-clippy/master/index.html#double_ended_iterator_last
 [`double_must_use`]: https://rust-lang.github.io/rust-clippy/master/index.html#double_must_use
diff --git a/README.md b/README.md
index cb3a22d4288..32c1d33e2ed 100644
--- a/README.md
+++ b/README.md
@@ -159,11 +159,11 @@ line. (You can swap `clippy::all` with the specific lint category you are target
 You can add options to your code to `allow`/`warn`/`deny` Clippy lints:
 
 * the whole set of `Warn` lints using the `clippy` lint group (`#![deny(clippy::all)]`).
-    Note that `rustc` has additional [lint groups](https://doc.rust-lang.org/rustc/lints/groups.html).
+  Note that `rustc` has additional [lint groups](https://doc.rust-lang.org/rustc/lints/groups.html).
 
 * all lints using both the `clippy` and `clippy::pedantic` lint groups (`#![deny(clippy::all)]`,
-    `#![deny(clippy::pedantic)]`). Note that `clippy::pedantic` contains some very aggressive
-    lints prone to false positives.
+  `#![deny(clippy::pedantic)]`). Note that `clippy::pedantic` contains some very aggressive
+  lints prone to false positives.
 
 * only some lints (`#![deny(clippy::single_match, clippy::box_vec)]`, etc.)
 
diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs
index ec223381aec..da73ef3ced4 100644
--- a/clippy_lints/src/declared_lints.rs
+++ b/clippy_lints/src/declared_lints.rs
@@ -142,6 +142,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
     crate::doc::DOC_LINK_WITH_QUOTES_INFO,
     crate::doc::DOC_MARKDOWN_INFO,
     crate::doc::DOC_NESTED_REFDEFS_INFO,
+    crate::doc::DOC_OVERINDENTED_LIST_ITEMS_INFO,
     crate::doc::EMPTY_DOCS_INFO,
     crate::doc::EMPTY_LINE_AFTER_DOC_COMMENTS_INFO,
     crate::doc::EMPTY_LINE_AFTER_OUTER_ATTR_INFO,
diff --git a/clippy_lints/src/doc/lazy_continuation.rs b/clippy_lints/src/doc/lazy_continuation.rs
index d6ab9ac6749..2577324f23d 100644
--- a/clippy_lints/src/doc/lazy_continuation.rs
+++ b/clippy_lints/src/doc/lazy_continuation.rs
@@ -1,11 +1,12 @@
-use clippy_utils::diagnostics::span_lint_and_then;
+use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then};
 use itertools::Itertools;
 use rustc_errors::Applicability;
 use rustc_lint::LateContext;
 use rustc_span::{BytePos, Span};
+use std::cmp::Ordering;
 use std::ops::Range;
 
-use super::DOC_LAZY_CONTINUATION;
+use super::{DOC_LAZY_CONTINUATION, DOC_OVERINDENTED_LIST_ITEMS};
 
 fn map_container_to_text(c: &super::Container) -> &'static str {
     match c {
@@ -28,12 +29,57 @@ pub(super) fn check(
         return;
     }
 
+    // Blockquote
     let ccount = doc[range.clone()].chars().filter(|c| *c == '>').count();
     let blockquote_level = containers
         .iter()
         .filter(|c| matches!(c, super::Container::Blockquote))
         .count();
-    let lcount = doc[range.clone()].chars().filter(|c| *c == ' ').count();
+    if ccount < blockquote_level {
+        span_lint_and_then(
+            cx,
+            DOC_LAZY_CONTINUATION,
+            span,
+            "doc quote line without `>` marker",
+            |diag| {
+                let mut doc_start_range = &doc[range];
+                let mut suggested = String::new();
+                for c in containers {
+                    let text = map_container_to_text(c);
+                    if doc_start_range.starts_with(text) {
+                        doc_start_range = &doc_start_range[text.len()..];
+                        span = span.with_lo(
+                            span.lo() + BytePos(u32::try_from(text.len()).expect("text is not 2**32 or bigger")),
+                        );
+                    } else if matches!(c, super::Container::Blockquote)
+                        && let Some(i) = doc_start_range.find('>')
+                    {
+                        doc_start_range = &doc_start_range[i + 1..];
+                        span = span
+                            .with_lo(span.lo() + BytePos(u32::try_from(i).expect("text is not 2**32 or bigger") + 1));
+                    } else {
+                        suggested.push_str(text);
+                    }
+                }
+                diag.span_suggestion_verbose(
+                    span,
+                    "add markers to start of line",
+                    suggested,
+                    Applicability::MachineApplicable,
+                );
+                diag.help("if this not intended to be a quote at all, escape it with `\\>`");
+            },
+        );
+        return;
+    }
+
+    if ccount != 0 && blockquote_level != 0 {
+        // If this doc is a blockquote, we don't go further.
+        return;
+    }
+
+    // List
+    let leading_spaces = doc[range].chars().filter(|c| *c == ' ').count();
     let list_indentation = containers
         .iter()
         .map(|c| {
@@ -44,16 +90,15 @@ pub(super) fn check(
             }
         })
         .sum();
-    if ccount < blockquote_level || lcount < list_indentation {
-        let msg = if ccount < blockquote_level {
-            "doc quote line without `>` marker"
-        } else {
-            "doc list item without indentation"
-        };
-        span_lint_and_then(cx, DOC_LAZY_CONTINUATION, span, msg, |diag| {
-            if ccount == 0 && blockquote_level == 0 {
+    match leading_spaces.cmp(&list_indentation) {
+        Ordering::Less => span_lint_and_then(
+            cx,
+            DOC_LAZY_CONTINUATION,
+            span,
+            "doc list item without indentation",
+            |diag| {
                 // simpler suggestion style for indentation
-                let indent = list_indentation - lcount;
+                let indent = list_indentation - leading_spaces;
                 diag.span_suggestion_verbose(
                     span.shrink_to_hi(),
                     "indent this line",
@@ -61,33 +106,20 @@ pub(super) fn check(
                     Applicability::MaybeIncorrect,
                 );
                 diag.help("if this is supposed to be its own paragraph, add a blank line");
-                return;
-            }
-            let mut doc_start_range = &doc[range];
-            let mut suggested = String::new();
-            for c in containers {
-                let text = map_container_to_text(c);
-                if doc_start_range.starts_with(text) {
-                    doc_start_range = &doc_start_range[text.len()..];
-                    span = span
-                        .with_lo(span.lo() + BytePos(u32::try_from(text.len()).expect("text is not 2**32 or bigger")));
-                } else if matches!(c, super::Container::Blockquote)
-                    && let Some(i) = doc_start_range.find('>')
-                {
-                    doc_start_range = &doc_start_range[i + 1..];
-                    span =
-                        span.with_lo(span.lo() + BytePos(u32::try_from(i).expect("text is not 2**32 or bigger") + 1));
-                } else {
-                    suggested.push_str(text);
-                }
-            }
-            diag.span_suggestion_verbose(
+            },
+        ),
+        Ordering::Greater => {
+            let sugg = std::iter::repeat_n(" ", list_indentation).join("");
+            span_lint_and_sugg(
+                cx,
+                DOC_OVERINDENTED_LIST_ITEMS,
                 span,
-                "add markers to start of line",
-                suggested,
-                Applicability::MachineApplicable,
+                "doc list item overindented",
+                format!("try using `{sugg}` ({list_indentation} spaces)"),
+                sugg,
+                Applicability::MaybeIncorrect,
             );
-            diag.help("if this not intended to be a quote at all, escape it with `\\>`");
-        });
+        },
+        Ordering::Equal => {},
     }
 }
diff --git a/clippy_lints/src/doc/mod.rs b/clippy_lints/src/doc/mod.rs
index 7561a6cf2a7..15530c3dbc5 100644
--- a/clippy_lints/src/doc/mod.rs
+++ b/clippy_lints/src/doc/mod.rs
@@ -430,6 +430,39 @@ declare_clippy_lint! {
 
 declare_clippy_lint! {
     /// ### What it does
+    ///
+    /// Detects overindented list items in doc comments where the continuation
+    /// lines are indented more than necessary.
+    ///
+    /// ### Why is this bad?
+    ///
+    /// Overindented list items in doc comments can lead to inconsistent and
+    /// poorly formatted documentation when rendered. Excessive indentation may
+    /// cause the text to be misinterpreted as a nested list item or code block,
+    /// affecting readability and the overall structure of the documentation.
+    ///
+    /// ### Example
+    ///
+    /// ```no_run
+    /// /// - This is the first item in a list
+    /// ///      and this line is overindented.
+    /// # fn foo() {}
+    /// ```
+    ///
+    /// Fixes this into:
+    /// ```no_run
+    /// /// - This is the first item in a list
+    /// ///   and this line is overindented.
+    /// # fn foo() {}
+    /// ```
+    #[clippy::version = "1.80.0"]
+    pub DOC_OVERINDENTED_LIST_ITEMS,
+    style,
+    "ensure list items are not overindented"
+}
+
+declare_clippy_lint! {
+    /// ### What it does
     /// Checks if the first paragraph in the documentation of items listed in the module page is too long.
     ///
     /// ### Why is this bad?
@@ -617,6 +650,7 @@ impl_lint_pass!(Documentation => [
     SUSPICIOUS_DOC_COMMENTS,
     EMPTY_DOCS,
     DOC_LAZY_CONTINUATION,
+    DOC_OVERINDENTED_LIST_ITEMS,
     EMPTY_LINE_AFTER_OUTER_ATTR,
     EMPTY_LINE_AFTER_DOC_COMMENTS,
     TOO_LONG_FIRST_DOC_PARAGRAPH,
diff --git a/tests/ui/doc/doc_lazy_list.fixed b/tests/ui/doc/doc_lazy_list.fixed
index 0822cc7c635..8e2ed1bbd18 100644
--- a/tests/ui/doc/doc_lazy_list.fixed
+++ b/tests/ui/doc/doc_lazy_list.fixed
@@ -1,4 +1,5 @@
 #![warn(clippy::doc_lazy_continuation)]
+#![allow(clippy::doc_overindented_list_items)]
 
 /// 1. nest here
 ///    lazy continuation
diff --git a/tests/ui/doc/doc_lazy_list.rs b/tests/ui/doc/doc_lazy_list.rs
index 068de140e00..1da11d8fae2 100644
--- a/tests/ui/doc/doc_lazy_list.rs
+++ b/tests/ui/doc/doc_lazy_list.rs
@@ -1,4 +1,5 @@
 #![warn(clippy::doc_lazy_continuation)]
+#![allow(clippy::doc_overindented_list_items)]
 
 /// 1. nest here
 /// lazy continuation
diff --git a/tests/ui/doc/doc_lazy_list.stderr b/tests/ui/doc/doc_lazy_list.stderr
index b38f43b7555..cea6157119f 100644
--- a/tests/ui/doc/doc_lazy_list.stderr
+++ b/tests/ui/doc/doc_lazy_list.stderr
@@ -1,5 +1,5 @@
 error: doc list item without indentation
-  --> tests/ui/doc/doc_lazy_list.rs:4:5
+  --> tests/ui/doc/doc_lazy_list.rs:5:5
    |
 LL | /// lazy continuation
    |     ^
@@ -13,7 +13,7 @@ LL | ///    lazy continuation
    |     +++
 
 error: doc list item without indentation
-  --> tests/ui/doc/doc_lazy_list.rs:9:5
+  --> tests/ui/doc/doc_lazy_list.rs:10:5
    |
 LL | /// lazy list continuations don't make warnings with this lint
    |     ^
@@ -25,7 +25,7 @@ LL | ///    lazy list continuations don't make warnings with this lint
    |     +++
 
 error: doc list item without indentation
-  --> tests/ui/doc/doc_lazy_list.rs:11:5
+  --> tests/ui/doc/doc_lazy_list.rs:12:5
    |
 LL | /// because they don't have the
    |     ^
@@ -37,7 +37,7 @@ LL | ///    because they don't have the
    |     +++
 
 error: doc list item without indentation
-  --> tests/ui/doc/doc_lazy_list.rs:16:5
+  --> tests/ui/doc/doc_lazy_list.rs:17:5
    |
 LL | /// lazy continuation
    |     ^
@@ -49,7 +49,7 @@ LL | ///     lazy continuation
    |     ++++
 
 error: doc list item without indentation
-  --> tests/ui/doc/doc_lazy_list.rs:21:5
+  --> tests/ui/doc/doc_lazy_list.rs:22:5
    |
 LL | /// lazy list continuations don't make warnings with this lint
    |     ^
@@ -61,7 +61,7 @@ LL | ///     lazy list continuations don't make warnings with this lint
    |     ++++
 
 error: doc list item without indentation
-  --> tests/ui/doc/doc_lazy_list.rs:23:5
+  --> tests/ui/doc/doc_lazy_list.rs:24:5
    |
 LL | /// because they don't have the
    |     ^
@@ -73,7 +73,7 @@ LL | ///     because they don't have the
    |     ++++
 
 error: doc list item without indentation
-  --> tests/ui/doc/doc_lazy_list.rs:28:5
+  --> tests/ui/doc/doc_lazy_list.rs:29:5
    |
 LL | /// lazy continuation
    |     ^
@@ -85,7 +85,7 @@ LL | ///     lazy continuation
    |     ++++
 
 error: doc list item without indentation
-  --> tests/ui/doc/doc_lazy_list.rs:33:5
+  --> tests/ui/doc/doc_lazy_list.rs:34:5
    |
 LL | /// this will warn on the lazy continuation
    |     ^
@@ -97,7 +97,7 @@ LL | ///       this will warn on the lazy continuation
    |     ++++++
 
 error: doc list item without indentation
-  --> tests/ui/doc/doc_lazy_list.rs:35:5
+  --> tests/ui/doc/doc_lazy_list.rs:36:5
    |
 LL | ///     and so should this
    |     ^^^^
@@ -109,7 +109,7 @@ LL | ///       and so should this
    |         ++
 
 error: doc list item without indentation
-  --> tests/ui/doc/doc_lazy_list.rs:56:5
+  --> tests/ui/doc/doc_lazy_list.rs:57:5
    |
 LL | ///  'protocol_descriptors': [
    |     ^
@@ -121,7 +121,7 @@ LL | ///   'protocol_descriptors': [
    |      +
 
 error: doc list item without indentation
-  --> tests/ui/doc/doc_lazy_list.rs:75:5
+  --> tests/ui/doc/doc_lazy_list.rs:76:5
    |
 LL | ///  ]
    |     ^
diff --git a/tests/ui/doc/doc_overindented_list_items.fixed b/tests/ui/doc/doc_overindented_list_items.fixed
new file mode 100644
index 00000000000..940cff48c1e
--- /dev/null
+++ b/tests/ui/doc/doc_overindented_list_items.fixed
@@ -0,0 +1,28 @@
+#![warn(clippy::doc_overindented_list_items)]
+
+#[rustfmt::skip]
+/// - first list item
+///   overindented line
+//~^ ERROR: doc list item overindented
+///   this is overindented line too
+//~^ ERROR: doc list item overindented
+/// - second list item
+fn foo() {}
+
+#[rustfmt::skip]
+///   - first list item
+///     overindented line
+//~^ ERROR: doc list item overindented
+///     this is overindented line too
+//~^ ERROR: doc list item overindented
+///   - second list item
+fn bar() {}
+
+#[rustfmt::skip]
+/// * first list item
+///   overindented line
+//~^ ERROR: doc list item overindented
+///   this is overindented line too
+//~^ ERROR: doc list item overindented
+/// * second list item
+fn baz() {}
diff --git a/tests/ui/doc/doc_overindented_list_items.rs b/tests/ui/doc/doc_overindented_list_items.rs
new file mode 100644
index 00000000000..77f3ee8a64d
--- /dev/null
+++ b/tests/ui/doc/doc_overindented_list_items.rs
@@ -0,0 +1,28 @@
+#![warn(clippy::doc_overindented_list_items)]
+
+#[rustfmt::skip]
+/// - first list item
+///        overindented line
+//~^ ERROR: doc list item overindented
+///      this is overindented line too
+//~^ ERROR: doc list item overindented
+/// - second list item
+fn foo() {}
+
+#[rustfmt::skip]
+///   - first list item
+///        overindented line
+//~^ ERROR: doc list item overindented
+///      this is overindented line too
+//~^ ERROR: doc list item overindented
+///   - second list item
+fn bar() {}
+
+#[rustfmt::skip]
+/// * first list item
+///        overindented line
+//~^ ERROR: doc list item overindented
+///      this is overindented line too
+//~^ ERROR: doc list item overindented
+/// * second list item
+fn baz() {}
diff --git a/tests/ui/doc/doc_overindented_list_items.stderr b/tests/ui/doc/doc_overindented_list_items.stderr
new file mode 100644
index 00000000000..ff201ba5eb9
--- /dev/null
+++ b/tests/ui/doc/doc_overindented_list_items.stderr
@@ -0,0 +1,41 @@
+error: doc list item overindented
+  --> tests/ui/doc/doc_overindented_list_items.rs:5:5
+   |
+LL | ///        overindented line
+   |     ^^^^^^^ help: try using `  ` (2 spaces)
+   |
+   = note: `-D clippy::doc-overindented-list-items` implied by `-D warnings`
+   = help: to override `-D warnings` add `#[allow(clippy::doc_overindented_list_items)]`
+
+error: doc list item overindented
+  --> tests/ui/doc/doc_overindented_list_items.rs:7:5
+   |
+LL | ///      this is overindented line too
+   |     ^^^^^ help: try using `  ` (2 spaces)
+
+error: doc list item overindented
+  --> tests/ui/doc/doc_overindented_list_items.rs:14:7
+   |
+LL | ///        overindented line
+   |       ^^^^^ help: try using `  ` (2 spaces)
+
+error: doc list item overindented
+  --> tests/ui/doc/doc_overindented_list_items.rs:16:7
+   |
+LL | ///      this is overindented line too
+   |       ^^^ help: try using `  ` (2 spaces)
+
+error: doc list item overindented
+  --> tests/ui/doc/doc_overindented_list_items.rs:23:5
+   |
+LL | ///        overindented line
+   |     ^^^^^^^ help: try using `  ` (2 spaces)
+
+error: doc list item overindented
+  --> tests/ui/doc/doc_overindented_list_items.rs:25:5
+   |
+LL | ///      this is overindented line too
+   |     ^^^^^ help: try using `  ` (2 spaces)
+
+error: aborting due to 6 previous errors
+