@@ -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