@@ -5,7 +5,7 @@ mod too_long_first_doc_paragraph;
5
5
6
6
use clippy_config:: Conf ;
7
7
use 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 } ;
9
9
use clippy_utils:: macros:: { is_panic, root_macro_call_first_node} ;
10
10
use clippy_utils:: ty:: is_type_diagnostic_item;
11
11
use clippy_utils:: visitors:: Visitable ;
@@ -18,6 +18,7 @@ use pulldown_cmark::Tag::{BlockQuote, CodeBlock, FootnoteDefinition, Heading, It
18
18
use pulldown_cmark:: { BrokenLink , CodeBlockKind , CowStr , Options , TagEnd } ;
19
19
use rustc_ast:: ast:: Attribute ;
20
20
use rustc_data_structures:: fx:: FxHashSet ;
21
+ use rustc_errors:: Applicability ;
21
22
use rustc_hir:: intravisit:: { self , Visitor } ;
22
23
use rustc_hir:: { AnonConst , Expr , ImplItemKind , ItemKind , Node , Safety , TraitItemKind } ;
23
24
use rustc_lint:: { LateContext , LateLintPass , LintContext } ;
@@ -564,6 +565,32 @@ declare_clippy_lint! {
564
565
"check if files included in documentation are behind `cfg(doc)`"
565
566
}
566
567
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
+
567
594
pub struct Documentation {
568
595
valid_idents : FxHashSet < String > ,
569
596
check_private_items : bool ,
@@ -581,6 +608,7 @@ impl Documentation {
581
608
impl_lint_pass ! ( Documentation => [
582
609
DOC_LINK_WITH_QUOTES ,
583
610
DOC_MARKDOWN ,
611
+ DOC_NESTED_REFDEFS ,
584
612
MISSING_SAFETY_DOC ,
585
613
MISSING_ERRORS_DOC ,
586
614
MISSING_PANICS_DOC ,
@@ -832,6 +860,31 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
832
860
Start ( BlockQuote ( _) ) => {
833
861
blockquote_level += 1 ;
834
862
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
+ }
835
888
} ,
836
889
End ( TagEnd :: BlockQuote ) => {
837
890
blockquote_level -= 1 ;
@@ -870,11 +923,37 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
870
923
in_heading = true ;
871
924
}
872
925
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
+ }
875
953
} else {
876
- containers. push ( Container :: List ( 0 ) ) ;
877
- }
954
+ 0
955
+ } ;
956
+ containers. push ( Container :: List ( indent) ) ;
878
957
}
879
958
ticks_unbalanced = false ;
880
959
paragraph_range = range;
@@ -1046,3 +1125,25 @@ impl<'tcx> Visitor<'tcx> for FindPanicUnwrap<'_, 'tcx> {
1046
1125
self . cx . tcx . hir ( )
1047
1126
}
1048
1127
}
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