@@ -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 } ;
@@ -558,6 +559,32 @@ declare_clippy_lint! {
558559 "check if files included in documentation are behind `cfg(doc)`"
559560}
560561
562+ declare_clippy_lint ! {
563+ /// ### What it does
564+ /// Warns if a link reference definition appears at the start of a
565+ /// list item or quote.
566+ ///
567+ /// ### Why is this bad?
568+ /// This is probably intended as an intra-doc link. If it is really
569+ /// supposed to be a reference definition, it can be written outside
570+ /// of the list item or quote.
571+ ///
572+ /// ### Example
573+ /// ```no_run
574+ /// //! - [link]: description
575+ /// ```
576+ /// Use instead:
577+ /// ```no_run
578+ /// //! - [link][]: description (for intra-doc link)
579+ /// //!
580+ /// //! [link]: destination (for link reference definition)
581+ /// ```
582+ #[ clippy:: version = "1.84.0" ]
583+ pub DOC_NESTED_REFDEFS ,
584+ suspicious,
585+ "link reference defined in list item or quote"
586+ }
587+
561588pub struct Documentation {
562589 valid_idents : FxHashSet < String > ,
563590 check_private_items : bool ,
@@ -575,6 +602,7 @@ impl Documentation {
575602impl_lint_pass ! ( Documentation => [
576603 DOC_LINK_WITH_QUOTES ,
577604 DOC_MARKDOWN ,
605+ DOC_NESTED_REFDEFS ,
578606 MISSING_SAFETY_DOC ,
579607 MISSING_ERRORS_DOC ,
580608 MISSING_PANICS_DOC ,
@@ -826,6 +854,31 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
826854 Start ( BlockQuote ( _) ) => {
827855 blockquote_level += 1 ;
828856 containers. push ( Container :: Blockquote ) ;
857+ if let Some ( ( next_event, next_range) ) = events. peek ( ) {
858+ let next_start = match next_event {
859+ End ( TagEnd :: BlockQuote ) => next_range. end ,
860+ _ => next_range. start ,
861+ } ;
862+ if let Some ( refdefrange) = looks_like_refdef ( doc, range. start ..next_start) &&
863+ let Some ( refdefspan) = fragments. span ( cx, refdefrange. clone ( ) )
864+ {
865+ span_lint_and_then (
866+ cx,
867+ DOC_NESTED_REFDEFS ,
868+ refdefspan,
869+ "link reference defined in quote" ,
870+ |diag| {
871+ diag. span_suggestion_short (
872+ refdefspan. shrink_to_hi ( ) ,
873+ "for an intra-doc link, add `[]` between the label and the colon" ,
874+ "[]" ,
875+ Applicability :: MaybeIncorrect ,
876+ ) ;
877+ diag. help ( "link definitions are not shown in rendered documentation" ) ;
878+ }
879+ ) ;
880+ }
881+ }
829882 } ,
830883 End ( TagEnd :: BlockQuote ) => {
831884 blockquote_level -= 1 ;
@@ -864,11 +917,37 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
864917 in_heading = true ;
865918 }
866919 if let Start ( Item ) = event {
867- if let Some ( ( _next_event, next_range) ) = events. peek ( ) {
868- containers. push ( Container :: List ( next_range. start - range. start ) ) ;
920+ let indent = if let Some ( ( next_event, next_range) ) = events. peek ( ) {
921+ let next_start = match next_event {
922+ End ( TagEnd :: Item ) => next_range. end ,
923+ _ => next_range. start ,
924+ } ;
925+ if let Some ( refdefrange) = looks_like_refdef ( doc, range. start ..next_start) &&
926+ let Some ( refdefspan) = fragments. span ( cx, refdefrange. clone ( ) )
927+ {
928+ span_lint_and_then (
929+ cx,
930+ DOC_NESTED_REFDEFS ,
931+ refdefspan,
932+ "link reference defined in list item" ,
933+ |diag| {
934+ diag. span_suggestion_short (
935+ refdefspan. shrink_to_hi ( ) ,
936+ "for an intra-doc link, add `[]` between the label and the colon" ,
937+ "[]" ,
938+ Applicability :: MaybeIncorrect ,
939+ ) ;
940+ diag. help ( "link definitions are not shown in rendered documentation" ) ;
941+ }
942+ ) ;
943+ refdefrange. start - range. start
944+ } else {
945+ next_range. start - range. start
946+ }
869947 } else {
870- containers. push ( Container :: List ( 0 ) ) ;
871- }
948+ 0
949+ } ;
950+ containers. push ( Container :: List ( indent) ) ;
872951 }
873952 ticks_unbalanced = false ;
874953 paragraph_range = range;
@@ -1040,3 +1119,25 @@ impl<'tcx> Visitor<'tcx> for FindPanicUnwrap<'_, 'tcx> {
10401119 self . cx . tcx . hir ( )
10411120 }
10421121}
1122+
1123+ #[ allow( clippy:: range_plus_one) ] // inclusive ranges aren't the same type
1124+ fn looks_like_refdef ( doc : & str , range : Range < usize > ) -> Option < Range < usize > > {
1125+ let offset = range. start ;
1126+ let mut iterator = doc. as_bytes ( ) [ range] . iter ( ) . copied ( ) . enumerate ( ) ;
1127+ let mut start = None ;
1128+ while let Some ( ( i, byte) ) = iterator. next ( ) {
1129+ if byte == b'\\' {
1130+ iterator. next ( ) ;
1131+ continue ;
1132+ }
1133+ if byte == b'[' {
1134+ start = Some ( i + offset) ;
1135+ }
1136+ if let Some ( start) = start
1137+ && byte == b']'
1138+ {
1139+ return Some ( start..i + offset + 1 ) ;
1140+ }
1141+ }
1142+ None
1143+ }
0 commit comments