@@ -3,7 +3,7 @@ mod too_long_first_doc_paragraph;
33
44use clippy_config:: Conf ;
55use clippy_utils:: attrs:: is_doc_hidden;
6- use clippy_utils:: diagnostics:: { span_lint, span_lint_and_help} ;
6+ use clippy_utils:: diagnostics:: { span_lint, span_lint_and_help, span_lint_and_then } ;
77use clippy_utils:: macros:: { is_panic, root_macro_call_first_node} ;
88use clippy_utils:: ty:: is_type_diagnostic_item;
99use clippy_utils:: visitors:: Visitable ;
@@ -16,6 +16,7 @@ use pulldown_cmark::Tag::{BlockQuote, CodeBlock, FootnoteDefinition, Heading, It
1616use pulldown_cmark:: { BrokenLink , CodeBlockKind , CowStr , Options , TagEnd } ;
1717use rustc_ast:: ast:: Attribute ;
1818use rustc_data_structures:: fx:: FxHashSet ;
19+ use rustc_errors:: Applicability ;
1920use rustc_hir:: intravisit:: { self , Visitor } ;
2021use rustc_hir:: { AnonConst , Expr , ImplItemKind , ItemKind , Node , Safety , TraitItemKind } ;
2122use rustc_lint:: { LateContext , LateLintPass , LintContext } ;
@@ -532,6 +533,32 @@ declare_clippy_lint! {
532533 "empty line after doc comments"
533534}
534535
536+ declare_clippy_lint ! {
537+ /// ### What it does
538+ /// Warns if a link reference definition appears at the start of a
539+ /// list item.
540+ ///
541+ /// ### Why is this bad?
542+ /// This is probably intended as an intra-doc link. If it is really
543+ /// supposed to be a reference definition, it can be written outside
544+ /// of the list item.
545+ ///
546+ /// ### Example
547+ /// ```no_run
548+ /// //! - [link]: description
549+ /// ```
550+ /// Use instead:
551+ /// ```no_run
552+ /// //! - [link][]: description (for intra-doc link)
553+ /// //!
554+ /// //! [link]: destination (for link reference definition)
555+ /// ```
556+ #[ clippy:: version = "1.84.0" ]
557+ pub DOC_REFDEF_LIST_ITEM ,
558+ suspicious,
559+ "link reference defined in list item"
560+ }
561+
535562pub struct Documentation {
536563 valid_idents : FxHashSet < String > ,
537564 check_private_items : bool ,
@@ -836,11 +863,37 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
836863 in_heading = true ;
837864 }
838865 if let Start ( Item ) = event {
839- if let Some ( ( _next_event, next_range) ) = events. peek ( ) {
840- containers. push ( Container :: List ( next_range. start - range. start ) ) ;
866+ let indent = if let Some ( ( next_event, next_range) ) = events. peek ( ) {
867+ let next_start = match next_event {
868+ End ( TagEnd :: Item ) => next_range. end ,
869+ _ => next_range. start ,
870+ } ;
871+ if let Some ( refdefrange) = looks_like_refdef ( doc, range. start ..next_start) &&
872+ let Some ( refdefspan) = fragments. span ( cx, refdefrange. clone ( ) )
873+ {
874+ span_lint_and_then (
875+ cx,
876+ DOC_REFDEF_LIST_ITEM ,
877+ refdefspan,
878+ "link reference defined in list item" ,
879+ |diag| {
880+ diag. span_suggestion_short (
881+ refdefspan. shrink_to_hi ( ) ,
882+ "for an intra-doc link, add `[]` between the label and the colon" ,
883+ "[]" ,
884+ Applicability :: MaybeIncorrect ,
885+ ) ;
886+ diag. help ( "link definitions are not shown in rendered documentation" ) ;
887+ }
888+ ) ;
889+ refdefrange. start - range. start
890+ } else {
891+ next_range. start - range. start
892+ }
841893 } else {
842- containers. push ( Container :: List ( 0 ) ) ;
843- }
894+ 0
895+ } ;
896+ containers. push ( Container :: List ( indent) ) ;
844897 }
845898 ticks_unbalanced = false ;
846899 paragraph_range = range;
@@ -1012,3 +1065,25 @@ impl<'tcx> Visitor<'tcx> for FindPanicUnwrap<'_, 'tcx> {
10121065 self . cx . tcx . hir ( )
10131066 }
10141067}
1068+
1069+ #[ allow( clippy:: range_plus_one) ] // inclusive ranges aren't the same type
1070+ fn looks_like_refdef ( doc : & str , range : Range < usize > ) -> Option < Range < usize > > {
1071+ let offset = range. start ;
1072+ let mut iterator = doc. as_bytes ( ) [ range] . iter ( ) . copied ( ) . enumerate ( ) ;
1073+ let mut start = None ;
1074+ while let Some ( ( i, byte) ) = iterator. next ( ) {
1075+ if byte == b'\\' {
1076+ iterator. next ( ) ;
1077+ continue ;
1078+ }
1079+ if byte == b'[' {
1080+ start = Some ( i + offset) ;
1081+ }
1082+ if let Some ( start) = start
1083+ && byte == b']'
1084+ {
1085+ return Some ( start..i + offset + 1 ) ;
1086+ }
1087+ }
1088+ None
1089+ }
0 commit comments