Skip to content

new lint: doc_comment_double_space_linebreaks #12876

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5570,6 +5570,7 @@ Released 2018-09-13
[`disallowed_type`]: https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_type
[`disallowed_types`]: https://rust-lang.github.io/rust-clippy/master/index.html#disallowed_types
[`diverging_sub_expression`]: https://rust-lang.github.io/rust-clippy/master/index.html#diverging_sub_expression
[`doc_comment_double_space_linebreaks`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_comment_double_space_linebreaks
[`doc_include_without_cfg`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_include_without_cfg
[`doc_lazy_continuation`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_lazy_continuation
[`doc_link_code`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_link_code
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/declared_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
crate::disallowed_names::DISALLOWED_NAMES_INFO,
crate::disallowed_script_idents::DISALLOWED_SCRIPT_IDENTS_INFO,
crate::disallowed_types::DISALLOWED_TYPES_INFO,
crate::doc::DOC_COMMENT_DOUBLE_SPACE_LINEBREAKS_INFO,
crate::doc::DOC_INCLUDE_WITHOUT_CFG_INFO,
crate::doc::DOC_LAZY_CONTINUATION_INFO,
crate::doc::DOC_LINK_CODE_INFO,
Expand Down
33 changes: 33 additions & 0 deletions clippy_lints/src/doc/doc_comment_double_space_linebreaks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use clippy_utils::diagnostics::span_lint_and_then;
use rustc_errors::Applicability;
use rustc_lint::LateContext;
use rustc_span::{BytePos, Span};

use super::DOC_COMMENT_DOUBLE_SPACE_LINEBREAKS;

pub fn check(cx: &LateContext<'_>, collected_breaks: &[Span]) {
if collected_breaks.is_empty() {
return;
}

let breaks: Vec<_> = collected_breaks
.iter()
.map(|span| span.with_hi(span.lo() + BytePos(2)))
.collect();

span_lint_and_then(
cx,
DOC_COMMENT_DOUBLE_SPACE_LINEBREAKS,
breaks.clone(),
"doc comment uses two spaces for a hard line break",
|diag| {
let suggs: Vec<_> = breaks.iter().map(|span| (*span, "\\".to_string())).collect();
diag.tool_only_multipart_suggestion(
"replace this double space with a backslash:",
suggs,
Applicability::MachineApplicable,
);
diag.help("replace this double space with a backslash: `\\`");
},
);
}
53 changes: 50 additions & 3 deletions clippy_lints/src/doc/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
#![allow(clippy::lint_without_lint_pass)]

mod lazy_continuation;
mod too_long_first_doc_paragraph;

use clippy_config::Conf;
use clippy_utils::attrs::is_doc_hidden;
use clippy_utils::diagnostics::{span_lint, span_lint_and_help, span_lint_and_then};
use clippy_utils::macros::{is_panic, root_macro_call_first_node};
use clippy_utils::source::snippet_opt;
use clippy_utils::ty::is_type_diagnostic_item;
use clippy_utils::visitors::Visitable;
use clippy_utils::{is_entrypoint_fn, is_trait_impl_item, method_chain_args};
Expand All @@ -33,12 +31,15 @@ use rustc_span::{Span, sym};
use std::ops::Range;
use url::Url;

mod doc_comment_double_space_linebreaks;
mod include_in_doc_without_cfg;
mod lazy_continuation;
mod link_with_quotes;
mod markdown;
mod missing_headers;
mod needless_doctest_main;
mod suspicious_doc_comments;
mod too_long_first_doc_paragraph;

declare_clippy_lint! {
/// ### What it does
Expand Down Expand Up @@ -567,6 +568,39 @@ declare_clippy_lint! {
"link reference defined in list item or quote"
}

declare_clippy_lint! {
/// ### What it does
/// Detects doc comment linebreaks that use double spaces to separate lines, instead of back-slash (`\`).
///
/// ### Why is this bad?
/// Double spaces, when used as doc comment linebreaks, can be difficult to see, and may
/// accidentally be removed during automatic formatting or manual refactoring. The use of a back-slash (`\`)
/// is clearer in this regard.
///
/// ### Example
/// The two replacement dots in this example represent a double space.
/// ```no_run
/// /// This command takes two numbers as inputs and··
/// /// adds them together, and then returns the result.
/// fn add(l: i32, r: i32) -> i32 {
/// l + r
/// }
/// ```
///
/// Use instead:
/// ```no_run
/// /// This command takes two numbers as inputs and\
/// /// adds them together, and then returns the result.
/// fn add(l: i32, r: i32) -> i32 {
/// l + r
/// }
/// ```
#[clippy::version = "1.87.0"]
pub DOC_COMMENT_DOUBLE_SPACE_LINEBREAKS,
pedantic,
"double-space used for doc comment linebreak instead of `\\`"
}

pub struct Documentation {
valid_idents: FxHashSet<String>,
check_private_items: bool,
Expand Down Expand Up @@ -598,6 +632,7 @@ impl_lint_pass!(Documentation => [
DOC_OVERINDENTED_LIST_ITEMS,
TOO_LONG_FIRST_DOC_PARAGRAPH,
DOC_INCLUDE_WITHOUT_CFG,
DOC_COMMENT_DOUBLE_SPACE_LINEBREAKS
]);

impl EarlyLintPass for Documentation {
Expand Down Expand Up @@ -894,6 +929,7 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
let mut paragraph_range = 0..0;
let mut code_level = 0;
let mut blockquote_level = 0;
let mut collected_breaks: Vec<Span> = Vec::new();
let mut is_first_paragraph = true;

let mut containers = Vec::new();
Expand Down Expand Up @@ -1069,6 +1105,14 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
&containers[..],
);
}

if let Some(span) = fragments.span(cx, range.clone())
&& !span.from_expansion()
&& let Some(snippet) = snippet_opt(cx, span)
&& !snippet.trim().starts_with('\\')
&& event == HardBreak {
collected_breaks.push(span);
}
},
Text(text) => {
paragraph_range.end = range.end;
Expand Down Expand Up @@ -1119,6 +1163,9 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
FootnoteReference(_) => {}
}
}

doc_comment_double_space_linebreaks::check(cx, &collected_breaks);

headers
}

Expand Down
98 changes: 98 additions & 0 deletions tests/ui/doc/doc_comment_double_space_linebreaks.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#![feature(custom_inner_attributes)]
#![rustfmt::skip]

#![warn(clippy::doc_comment_double_space_linebreaks)]
#![allow(unused, clippy::empty_docs)]

//~v doc_comment_double_space_linebreaks
//! Should warn on double space linebreaks\
//! in file/module doc comment

/// Should not warn on single-line doc comments
fn single_line() {}

/// Should not warn on single-line doc comments
/// split across multiple lines
fn single_line_split() {}

// Should not warn on normal comments

// note: cargo fmt can remove double spaces from normal and block comments
// Should not warn on normal comments
// with double spaces at the end of a line

#[doc = "This is a doc attribute, which should not be linted"]
fn normal_comment() {
/*
Should not warn on block comments
*/

/*
Should not warn on block comments
with double space at the end of a line
*/
}

//~v doc_comment_double_space_linebreaks
/// Should warn when doc comment uses double space\
/// as a line-break, even when there are multiple\
/// in a row
fn double_space_doc_comment() {}

/// Should not warn when back-slash is used \
/// as a line-break
fn back_slash_doc_comment() {}

//~v doc_comment_double_space_linebreaks
/// 🌹 are 🟥\
/// 🌷 are 🟦\
/// 📎 is 😎\
/// and so are 🫵\
/// (hopefully no formatting weirdness linting this)
fn multi_byte_chars_tada() {}

macro_rules! macro_that_makes_function {
() => {
/// Shouldn't lint on this!
/// (hopefully)
fn my_macro_created_function() {}
}
}

macro_that_makes_function!();

// dont lint when its alone on a line
///
fn alone() {}

/// | First column | Second column |
/// | ------------ | ------------- |
/// | Not a line | break when |
/// | after a line | in a table |
fn table() {}

/// ```text
/// It's also not a hard line break if
/// there's two spaces at the end of a
/// line in a block code.
/// ```
fn codeblock() {}

/// It's also not a hard line break `if
/// there's` two spaces in the middle of inline code.
fn inline() {}

/// It's also not a hard line break [when](
/// https://example.com) in a URL.
fn url() {}

//~v doc_comment_double_space_linebreaks
/// here we mix\
/// double spaces\
/// and also\
/// adding backslash\
/// to some of them\
/// to see how that looks
fn mixed() {}

fn main() {}
98 changes: 98 additions & 0 deletions tests/ui/doc/doc_comment_double_space_linebreaks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#![feature(custom_inner_attributes)]
#![rustfmt::skip]

#![warn(clippy::doc_comment_double_space_linebreaks)]
#![allow(unused, clippy::empty_docs)]

//~v doc_comment_double_space_linebreaks
//! Should warn on double space linebreaks
//! in file/module doc comment

/// Should not warn on single-line doc comments
fn single_line() {}

/// Should not warn on single-line doc comments
/// split across multiple lines
fn single_line_split() {}

// Should not warn on normal comments

// note: cargo fmt can remove double spaces from normal and block comments
// Should not warn on normal comments
// with double spaces at the end of a line

#[doc = "This is a doc attribute, which should not be linted"]
fn normal_comment() {
/*
Should not warn on block comments
*/

/*
Should not warn on block comments
with double space at the end of a line
*/
}

//~v doc_comment_double_space_linebreaks
/// Should warn when doc comment uses double space
/// as a line-break, even when there are multiple
/// in a row
fn double_space_doc_comment() {}

/// Should not warn when back-slash is used \
/// as a line-break
fn back_slash_doc_comment() {}

//~v doc_comment_double_space_linebreaks
/// 🌹 are 🟥
/// 🌷 are 🟦
/// 📎 is 😎
/// and so are 🫵
/// (hopefully no formatting weirdness linting this)
fn multi_byte_chars_tada() {}

macro_rules! macro_that_makes_function {
() => {
/// Shouldn't lint on this!
/// (hopefully)
fn my_macro_created_function() {}
}
}

macro_that_makes_function!();

// dont lint when its alone on a line
///
fn alone() {}

/// | First column | Second column |
/// | ------------ | ------------- |
/// | Not a line | break when |
/// | after a line | in a table |
fn table() {}

/// ```text
/// It's also not a hard line break if
/// there's two spaces at the end of a
/// line in a block code.
/// ```
fn codeblock() {}

/// It's also not a hard line break `if
/// there's` two spaces in the middle of inline code.
fn inline() {}

/// It's also not a hard line break [when](
/// https://example.com) in a URL.
fn url() {}

//~v doc_comment_double_space_linebreaks
/// here we mix
/// double spaces\
/// and also
/// adding backslash\
/// to some of them
/// to see how that looks
fn mixed() {}

fn main() {}
Loading