1
+ mod lazy_continuation;
1
2
use clippy_utils:: attrs:: is_doc_hidden;
2
3
use clippy_utils:: diagnostics:: { span_lint, span_lint_and_help} ;
3
4
use 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};
7
8
use pulldown_cmark:: Event :: {
8
9
Code , End , FootnoteReference , HardBreak , Html , Rule , SoftBreak , Start , TaskListMarker , Text ,
9
10
} ;
10
- use pulldown_cmark:: Tag :: { BlockQuote , CodeBlock , Heading , Item , Link , Paragraph } ;
11
+ use pulldown_cmark:: Tag :: { BlockQuote , CodeBlock , FootnoteDefinition , Heading , Item , Link , Paragraph } ;
11
12
use pulldown_cmark:: { BrokenLink , CodeBlockKind , CowStr , Options } ;
12
13
use rustc_ast:: ast:: Attribute ;
13
14
use rustc_data_structures:: fx:: FxHashSet ;
@@ -362,6 +363,61 @@ declare_clippy_lint! {
362
363
"docstrings exist but documentation is empty"
363
364
}
364
365
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
+ /// ```
408
+ ///
409
+ /// Fix it by indenting the list contents:
410
+ /// ```no_run
411
+ /// /// * Do nothing.
412
+ /// /// * Then do something. Whatever it is needs done,
413
+ /// /// it should be done right now.
414
+ /// ```
415
+ #[ clippy:: version = "1.80.0" ]
416
+ pub DOC_LAZY_CONTINUATION ,
417
+ style,
418
+ "require every line of a paragraph to be indented and marked"
419
+ }
420
+
365
421
#[ derive( Clone ) ]
366
422
pub struct Documentation {
367
423
valid_idents : FxHashSet < String > ,
@@ -388,6 +444,7 @@ impl_lint_pass!(Documentation => [
388
444
UNNECESSARY_SAFETY_DOC ,
389
445
SUSPICIOUS_DOC_COMMENTS ,
390
446
EMPTY_DOCS ,
447
+ DOC_LAZY_CONTINUATION ,
391
448
] ) ;
392
449
393
450
impl < ' tcx > LateLintPass < ' tcx > for Documentation {
@@ -551,6 +608,7 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
551
608
cx,
552
609
valid_idents,
553
610
parser. into_offset_iter ( ) ,
611
+ & doc,
554
612
Fragments {
555
613
fragments : & fragments,
556
614
doc : & doc,
@@ -560,6 +618,11 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
560
618
561
619
const RUST_CODE : & [ & str ] = & [ "rust" , "no_run" , "should_panic" , "compile_fail" ] ;
562
620
621
+ enum Container {
622
+ Blockquote ,
623
+ List ( usize ) ,
624
+ }
625
+
563
626
/// Checks parsed documentation.
564
627
/// This walks the "events" (think sections of markdown) produced by `pulldown_cmark`,
565
628
/// so lints here will generally access that information.
@@ -569,13 +632,15 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
569
632
cx : & LateContext < ' _ > ,
570
633
valid_idents : & FxHashSet < String > ,
571
634
events : Events ,
635
+ doc : & str ,
572
636
fragments : Fragments < ' _ > ,
573
637
) -> DocHeaders {
574
638
// true if a safety header was found
575
639
let mut headers = DocHeaders :: default ( ) ;
576
640
let mut in_code = false ;
577
641
let mut in_link = None ;
578
642
let mut in_heading = false ;
643
+ let mut in_footnote_definition = false ;
579
644
let mut is_rust = false ;
580
645
let mut no_test = false ;
581
646
let mut ignore = false ;
@@ -586,7 +651,11 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
586
651
let mut code_level = 0 ;
587
652
let mut blockquote_level = 0 ;
588
653
589
- for ( event, range) in events {
654
+ let mut containers = Vec :: new ( ) ;
655
+
656
+ let mut events = events. peekable ( ) ;
657
+
658
+ while let Some ( ( event, range) ) = events. next ( ) {
590
659
match event {
591
660
Html ( tag) => {
592
661
if tag. starts_with ( "<code" ) {
@@ -599,8 +668,14 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
599
668
blockquote_level -= 1 ;
600
669
}
601
670
} ,
602
- Start ( BlockQuote ) => blockquote_level += 1 ,
603
- End ( BlockQuote ) => blockquote_level -= 1 ,
671
+ Start ( BlockQuote ) => {
672
+ blockquote_level += 1 ;
673
+ containers. push ( Container :: Blockquote ) ;
674
+ } ,
675
+ End ( BlockQuote ) => {
676
+ blockquote_level -= 1 ;
677
+ containers. pop ( ) ;
678
+ } ,
604
679
Start ( CodeBlock ( ref kind) ) => {
605
680
in_code = true ;
606
681
if let CodeBlockKind :: Fenced ( lang) = kind {
@@ -633,13 +708,23 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
633
708
if let Start ( Heading ( _, _, _) ) = event {
634
709
in_heading = true ;
635
710
}
711
+ if let Start ( Item ) = event {
712
+ if let Some ( ( _next_event, next_range) ) = events. peek ( ) {
713
+ containers. push ( Container :: List ( next_range. start - range. start ) ) ;
714
+ } else {
715
+ containers. push ( Container :: List ( 0 ) ) ;
716
+ }
717
+ }
636
718
ticks_unbalanced = false ;
637
719
paragraph_range = range;
638
720
} ,
639
721
End ( Heading ( _, _, _) | Paragraph | Item ) => {
640
722
if let End ( Heading ( _, _, _) ) = event {
641
723
in_heading = false ;
642
724
}
725
+ if let End ( Item ) = event {
726
+ containers. pop ( ) ;
727
+ }
643
728
if ticks_unbalanced && let Some ( span) = fragments. span ( cx, paragraph_range. clone ( ) ) {
644
729
span_lint_and_help (
645
730
cx,
@@ -658,8 +743,26 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
658
743
}
659
744
text_to_check = Vec :: new ( ) ;
660
745
} ,
746
+ Start ( FootnoteDefinition ( ..) ) => in_footnote_definition = true ,
747
+ End ( FootnoteDefinition ( ..) ) => in_footnote_definition = false ,
661
748
Start ( _tag) | End ( _tag) => ( ) , // We don't care about other tags
662
- SoftBreak | HardBreak | TaskListMarker ( _) | Code ( _) | Rule => ( ) ,
749
+ SoftBreak | HardBreak => {
750
+ if !containers. is_empty ( )
751
+ && let Some ( ( _next_event, next_range) ) = events. peek ( )
752
+ && let Some ( next_span) = fragments. span ( cx, next_range. clone ( ) )
753
+ && let Some ( span) = fragments. span ( cx, range. clone ( ) )
754
+ && !in_footnote_definition
755
+ {
756
+ lazy_continuation:: check (
757
+ cx,
758
+ doc,
759
+ range. end ..next_range. start ,
760
+ Span :: new ( span. hi ( ) , next_span. lo ( ) , span. ctxt ( ) , span. parent ( ) ) ,
761
+ & containers[ ..] ,
762
+ ) ;
763
+ }
764
+ } ,
765
+ TaskListMarker ( _) | Code ( _) | Rule => ( ) ,
663
766
FootnoteReference ( text) | Text ( text) => {
664
767
paragraph_range. end = range. end ;
665
768
ticks_unbalanced |= text. contains ( '`' ) && !in_code;
0 commit comments