1+ mod lazy_continuation;
12use clippy_utils:: attrs:: is_doc_hidden;
23use clippy_utils:: diagnostics:: { span_lint, span_lint_and_help} ;
34use 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};
78use 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 } ;
1112use pulldown_cmark:: { BrokenLink , CodeBlockKind , CowStr , Options } ;
1213use rustc_ast:: ast:: Attribute ;
1314use 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 ) ]
366424pub 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
393452impl < ' 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
561621const 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;
0 commit comments