@@ -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 } ;
@@ -558,6 +559,32 @@ declare_clippy_lint! {
558
559
"check if files included in documentation are behind `cfg(doc)`"
559
560
}
560
561
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
+
561
588
pub struct Documentation {
562
589
valid_idents : FxHashSet < String > ,
563
590
check_private_items : bool ,
@@ -575,6 +602,7 @@ impl Documentation {
575
602
impl_lint_pass ! ( Documentation => [
576
603
DOC_LINK_WITH_QUOTES ,
577
604
DOC_MARKDOWN ,
605
+ DOC_NESTED_REFDEFS ,
578
606
MISSING_SAFETY_DOC ,
579
607
MISSING_ERRORS_DOC ,
580
608
MISSING_PANICS_DOC ,
@@ -826,6 +854,31 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
826
854
Start ( BlockQuote ( _) ) => {
827
855
blockquote_level += 1 ;
828
856
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
+ }
829
882
} ,
830
883
End ( TagEnd :: BlockQuote ) => {
831
884
blockquote_level -= 1 ;
@@ -864,11 +917,37 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
864
917
in_heading = true ;
865
918
}
866
919
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
+ }
869
947
} else {
870
- containers. push ( Container :: List ( 0 ) ) ;
871
- }
948
+ 0
949
+ } ;
950
+ containers. push ( Container :: List ( indent) ) ;
872
951
}
873
952
ticks_unbalanced = false ;
874
953
paragraph_range = range;
@@ -1040,3 +1119,25 @@ impl<'tcx> Visitor<'tcx> for FindPanicUnwrap<'_, 'tcx> {
1040
1119
self . cx . tcx . hir ( )
1041
1120
}
1042
1121
}
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