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,63 @@ 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
+ /// # 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
+
365
423
#[ derive( Clone ) ]
366
424
pub struct Documentation {
367
425
valid_idents : FxHashSet < String > ,
@@ -388,6 +446,7 @@ impl_lint_pass!(Documentation => [
388
446
UNNECESSARY_SAFETY_DOC ,
389
447
SUSPICIOUS_DOC_COMMENTS ,
390
448
EMPTY_DOCS ,
449
+ DOC_LAZY_CONTINUATION ,
391
450
] ) ;
392
451
393
452
impl < ' tcx > LateLintPass < ' tcx > for Documentation {
@@ -551,6 +610,7 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
551
610
cx,
552
611
valid_idents,
553
612
parser. into_offset_iter ( ) ,
613
+ & doc,
554
614
Fragments {
555
615
fragments : & fragments,
556
616
doc : & doc,
@@ -560,6 +620,11 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
560
620
561
621
const RUST_CODE : & [ & str ] = & [ "rust" , "no_run" , "should_panic" , "compile_fail" ] ;
562
622
623
+ enum Container {
624
+ Blockquote ,
625
+ List ( usize ) ,
626
+ }
627
+
563
628
/// Checks parsed documentation.
564
629
/// This walks the "events" (think sections of markdown) produced by `pulldown_cmark`,
565
630
/// 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
569
634
cx : & LateContext < ' _ > ,
570
635
valid_idents : & FxHashSet < String > ,
571
636
events : Events ,
637
+ doc : & str ,
572
638
fragments : Fragments < ' _ > ,
573
639
) -> DocHeaders {
574
640
// true if a safety header was found
575
641
let mut headers = DocHeaders :: default ( ) ;
576
642
let mut in_code = false ;
577
643
let mut in_link = None ;
578
644
let mut in_heading = false ;
645
+ let mut in_footnote_definition = false ;
579
646
let mut is_rust = false ;
580
647
let mut no_test = false ;
581
648
let mut ignore = false ;
@@ -586,7 +653,11 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
586
653
let mut code_level = 0 ;
587
654
let mut blockquote_level = 0 ;
588
655
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 ( ) {
590
661
match event {
591
662
Html ( tag) => {
592
663
if tag. starts_with ( "<code" ) {
@@ -599,8 +670,14 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
599
670
blockquote_level -= 1 ;
600
671
}
601
672
} ,
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
+ } ,
604
681
Start ( CodeBlock ( ref kind) ) => {
605
682
in_code = true ;
606
683
if let CodeBlockKind :: Fenced ( lang) = kind {
@@ -633,13 +710,23 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
633
710
if let Start ( Heading ( _, _, _) ) = event {
634
711
in_heading = true ;
635
712
}
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
+ }
636
720
ticks_unbalanced = false ;
637
721
paragraph_range = range;
638
722
} ,
639
723
End ( Heading ( _, _, _) | Paragraph | Item ) => {
640
724
if let End ( Heading ( _, _, _) ) = event {
641
725
in_heading = false ;
642
726
}
727
+ if let End ( Item ) = event {
728
+ containers. pop ( ) ;
729
+ }
643
730
if ticks_unbalanced && let Some ( span) = fragments. span ( cx, paragraph_range. clone ( ) ) {
644
731
span_lint_and_help (
645
732
cx,
@@ -658,8 +745,26 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
658
745
}
659
746
text_to_check = Vec :: new ( ) ;
660
747
} ,
748
+ Start ( FootnoteDefinition ( ..) ) => in_footnote_definition = true ,
749
+ End ( FootnoteDefinition ( ..) ) => in_footnote_definition = false ,
661
750
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 => ( ) ,
663
768
FootnoteReference ( text) | Text ( text) => {
664
769
paragraph_range. end = range. end ;
665
770
ticks_unbalanced |= text. contains ( '`' ) && !in_code;
0 commit comments