@@ -7,6 +7,7 @@ use clippy_config::Conf;
77use clippy_utils:: attrs:: is_doc_hidden;
88use clippy_utils:: diagnostics:: { span_lint, span_lint_and_help, span_lint_and_then} ;
99use clippy_utils:: macros:: { is_panic, root_macro_call_first_node} ;
10+ use clippy_utils:: source:: snippet_opt;
1011use clippy_utils:: ty:: is_type_diagnostic_item;
1112use clippy_utils:: visitors:: Visitable ;
1213use clippy_utils:: { is_entrypoint_fn, is_trait_impl_item, method_chain_args} ;
@@ -33,6 +34,7 @@ use rustc_span::{Span, sym};
3334use std:: ops:: Range ;
3435use url:: Url ;
3536
37+ mod doc_comment_double_space_linebreak;
3638mod include_in_doc_without_cfg;
3739mod link_with_quotes;
3840mod markdown;
@@ -567,6 +569,38 @@ declare_clippy_lint! {
567569 "link reference defined in list item or quote"
568570}
569571
572+ declare_clippy_lint ! {
573+ /// Detects doc comment linebreaks that use double spaces to separate lines, instead of back-slash (`\`).
574+ ///
575+ /// ### Why is this bad?
576+ /// Double spaces, when used as doc comment linebreaks, can be difficult to see, and may
577+ /// accidentally be removed during automatic formatting or manual refactoring. The use of a back-slash (`\`)
578+ /// is clearer in this regard.
579+ ///
580+ /// ### Example
581+ /// The two replacement dots in this example represent a double space.
582+ /// ```no_run
583+ /// /// This command takes two numbers as inputs and··
584+ /// /// adds them together, and then returns the result.
585+ /// fn add(l: i32, r: i32) -> i32 {
586+ /// l + r
587+ /// }
588+ /// ```
589+ ///
590+ /// Use instead:
591+ /// ```no_run
592+ /// /// This command takes two numbers as inputs and\
593+ /// /// adds them together, and then returns the result.
594+ /// fn add(l: i32, r: i32) -> i32 {
595+ /// l + r
596+ /// }
597+ /// ```
598+ #[ clippy:: version = "1.80.0" ]
599+ pub DOC_COMMENT_DOUBLE_SPACE_LINEBREAK ,
600+ pedantic,
601+ "double-space used for doc comment linebreak instead of `\\ `"
602+ }
603+
570604pub struct Documentation {
571605 valid_idents : FxHashSet < String > ,
572606 check_private_items : bool ,
@@ -598,6 +632,7 @@ impl_lint_pass!(Documentation => [
598632 DOC_OVERINDENTED_LIST_ITEMS ,
599633 TOO_LONG_FIRST_DOC_PARAGRAPH ,
600634 DOC_INCLUDE_WITHOUT_CFG ,
635+ DOC_COMMENT_DOUBLE_SPACE_LINEBREAK
601636] ) ;
602637
603638impl EarlyLintPass for Documentation {
@@ -737,6 +772,8 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
737772 return None ;
738773 }
739774
775+ suspicious_doc_comments:: check ( cx, attrs) ;
776+
740777 let ( fragments, _) = attrs_to_doc_fragments (
741778 attrs. iter ( ) . filter_map ( |attr| {
742779 if attr. doc_str_and_comment_kind ( ) . is_none ( ) || attr. span ( ) . in_external_macro ( cx. sess ( ) . source_map ( ) ) {
@@ -894,6 +931,7 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
894931 let mut paragraph_range = 0 ..0 ;
895932 let mut code_level = 0 ;
896933 let mut blockquote_level = 0 ;
934+ let mut collected_breaks: Vec < Span > = Vec :: new ( ) ;
897935 let mut is_first_paragraph = true ;
898936
899937 let mut containers = Vec :: new ( ) ;
@@ -1069,6 +1107,14 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
10691107 & containers[ ..] ,
10701108 ) ;
10711109 }
1110+
1111+ if let Some ( span) = fragments. span ( cx, range. clone ( ) )
1112+ && !span. from_expansion ( )
1113+ && let Some ( snippet) = snippet_opt ( cx, span)
1114+ && !snippet. trim ( ) . starts_with ( '\\' )
1115+ && event == HardBreak {
1116+ collected_breaks. push ( span) ;
1117+ }
10721118 } ,
10731119 Text ( text) => {
10741120 paragraph_range. end = range. end ;
@@ -1119,6 +1165,9 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
11191165 FootnoteReference ( _) => { }
11201166 }
11211167 }
1168+
1169+ doc_comment_double_space_linebreak:: check ( cx, & collected_breaks) ;
1170+
11221171 headers
11231172}
11241173
0 commit comments