@@ -5,7 +5,7 @@ mod too_long_first_doc_paragraph;
55
66use  clippy_config:: Conf ; 
77use  clippy_utils:: attrs:: is_doc_hidden; 
8- use  clippy_utils:: diagnostics:: { span_lint,  span_lint_and_help} ; 
8+ use  clippy_utils:: diagnostics:: { span_lint,  span_lint_and_help,  span_lint_and_then } ; 
99use  clippy_utils:: macros:: { is_panic,  root_macro_call_first_node} ; 
1010use  clippy_utils:: ty:: is_type_diagnostic_item; 
1111use  clippy_utils:: visitors:: Visitable ; 
@@ -18,6 +18,7 @@ use pulldown_cmark::Tag::{BlockQuote, CodeBlock, FootnoteDefinition, Heading, It
1818use  pulldown_cmark:: { BrokenLink ,  CodeBlockKind ,  CowStr ,  Options ,  TagEnd } ; 
1919use  rustc_ast:: ast:: Attribute ; 
2020use  rustc_data_structures:: fx:: FxHashSet ; 
21+ use  rustc_errors:: Applicability ; 
2122use  rustc_hir:: intravisit:: { self ,  Visitor } ; 
2223use  rustc_hir:: { AnonConst ,  Expr ,  ImplItemKind ,  ItemKind ,  Node ,  Safety ,  TraitItemKind } ; 
2324use  rustc_lint:: { LateContext ,  LateLintPass ,  LintContext } ; 
@@ -564,6 +565,32 @@ declare_clippy_lint! {
564565    "check if files included in documentation are behind `cfg(doc)`" 
565566} 
566567
568+ declare_clippy_lint !  { 
569+     /// ### What it does 
570+ /// Warns if a link reference definition appears at the start of a 
571+ /// list item or quote. 
572+ /// 
573+ /// ### Why is this bad? 
574+ /// This is probably intended as an intra-doc link. If it is really 
575+ /// supposed to be a reference definition, it can be written outside 
576+ /// of the list item or quote. 
577+ /// 
578+ /// ### Example 
579+ /// ```no_run 
580+ /// //! - [link]: description 
581+ /// ``` 
582+ /// Use instead: 
583+ /// ```no_run 
584+ /// //! - [link][]: description (for intra-doc link) 
585+ /// //! 
586+ /// //! [link]: destination (for link reference definition) 
587+ /// ``` 
588+ [ clippy:: version = "1.84.0" ] 
589+     pub  DOC_NESTED_REFDEFS , 
590+     suspicious, 
591+     "link reference defined in list item or quote" 
592+ } 
593+ 
567594pub  struct  Documentation  { 
568595    valid_idents :  FxHashSet < String > , 
569596    check_private_items :  bool , 
@@ -581,6 +608,7 @@ impl Documentation {
581608impl_lint_pass ! ( Documentation  => [ 
582609    DOC_LINK_WITH_QUOTES , 
583610    DOC_MARKDOWN , 
611+     DOC_NESTED_REFDEFS , 
584612    MISSING_SAFETY_DOC , 
585613    MISSING_ERRORS_DOC , 
586614    MISSING_PANICS_DOC , 
@@ -832,6 +860,31 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
832860            Start ( BlockQuote ( _) )  => { 
833861                blockquote_level += 1 ; 
834862                containers. push ( Container :: Blockquote ) ; 
863+                 if  let  Some ( ( next_event,  next_range) )  = events. peek ( )  { 
864+                     let  next_start = match  next_event { 
865+                         End ( TagEnd :: BlockQuote )  => next_range. end , 
866+                         _ => next_range. start , 
867+                     } ; 
868+                     if  let  Some ( refdefrange)  = looks_like_refdef ( doc,  range. start ..next_start)  &&
869+                         let  Some ( refdefspan)  = fragments. span ( cx,  refdefrange. clone ( ) ) 
870+                     { 
871+                         span_lint_and_then ( 
872+                             cx, 
873+                             DOC_NESTED_REFDEFS , 
874+                             refdefspan, 
875+                             "link reference defined in quote" , 
876+                             |diag| { 
877+                                 diag. span_suggestion_short ( 
878+                                     refdefspan. shrink_to_hi ( ) , 
879+                                     "for an intra-doc link, add `[]` between the label and the colon" , 
880+                                     "[]" , 
881+                                     Applicability :: MaybeIncorrect , 
882+                                 ) ; 
883+                                 diag. help ( "link definitions are not shown in rendered documentation" ) ; 
884+                             } 
885+                         ) ; 
886+                     } 
887+                 } 
835888            } , 
836889            End ( TagEnd :: BlockQuote )  => { 
837890                blockquote_level -= 1 ; 
@@ -870,11 +923,37 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
870923                    in_heading = true ; 
871924                } 
872925                if  let  Start ( Item )  = event { 
873-                     if  let  Some ( ( _next_event,  next_range) )  = events. peek ( )  { 
874-                         containers. push ( Container :: List ( next_range. start  - range. start ) ) ; 
926+                     let  indent = if  let  Some ( ( next_event,  next_range) )  = events. peek ( )  { 
927+                         let  next_start = match  next_event { 
928+                             End ( TagEnd :: Item )  => next_range. end , 
929+                             _ => next_range. start , 
930+                         } ; 
931+                         if  let  Some ( refdefrange)  = looks_like_refdef ( doc,  range. start ..next_start)  &&
932+                             let  Some ( refdefspan)  = fragments. span ( cx,  refdefrange. clone ( ) ) 
933+                         { 
934+                             span_lint_and_then ( 
935+                                 cx, 
936+                                 DOC_NESTED_REFDEFS , 
937+                                 refdefspan, 
938+                                 "link reference defined in list item" , 
939+                                 |diag| { 
940+                                     diag. span_suggestion_short ( 
941+                                         refdefspan. shrink_to_hi ( ) , 
942+                                         "for an intra-doc link, add `[]` between the label and the colon" , 
943+                                         "[]" , 
944+                                         Applicability :: MaybeIncorrect , 
945+                                     ) ; 
946+                                     diag. help ( "link definitions are not shown in rendered documentation" ) ; 
947+                                 } 
948+                             ) ; 
949+                             refdefrange. start  - range. start 
950+                         }  else  { 
951+                             next_range. start  - range. start 
952+                         } 
875953                    }  else  { 
876-                         containers. push ( Container :: List ( 0 ) ) ; 
877-                     } 
954+                         0 
955+                     } ; 
956+                     containers. push ( Container :: List ( indent) ) ; 
878957                } 
879958                ticks_unbalanced = false ; 
880959                paragraph_range = range; 
@@ -1046,3 +1125,25 @@ impl<'tcx> Visitor<'tcx> for FindPanicUnwrap<'_, 'tcx> {
10461125        self . cx . tcx . hir ( ) 
10471126    } 
10481127} 
1128+ 
1129+ #[ expect( clippy:: range_plus_one) ]   // inclusive ranges aren't the same type 
1130+ fn  looks_like_refdef ( doc :  & str ,  range :  Range < usize > )  -> Option < Range < usize > >  { 
1131+     let  offset = range. start ; 
1132+     let  mut  iterator = doc. as_bytes ( ) [ range] . iter ( ) . copied ( ) . enumerate ( ) ; 
1133+     let  mut  start = None ; 
1134+     while  let  Some ( ( i,  byte) )  = iterator. next ( )  { 
1135+         match  byte { 
1136+             b'\\'  => { 
1137+                 iterator. next ( ) ; 
1138+             } , 
1139+             b'['  => { 
1140+                 start = Some ( i + offset) ; 
1141+             } , 
1142+             b']'  if  let  Some ( start)  = start => { 
1143+                 return  Some ( start..i + offset + 1 ) ; 
1144+             } , 
1145+             _ => { } , 
1146+         } 
1147+     } 
1148+     None 
1149+ } 
0 commit comments