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.
This commit is contained in:
Yutaro Ohno 2025-01-14 17:06:31 +09:00
parent 66dc8a1a30
commit 4693d0a9ff
11 changed files with 219 additions and 52 deletions

View file

@ -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,

View file

@ -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 => {},
}
}

View file

@ -428,6 +428,39 @@ declare_clippy_lint! {
"require every line of a paragraph to be indented and marked"
}
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.
@ -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,