@@ -10,10 +10,12 @@ use pulldown_cmark::Event::{
10
10
} ;
11
11
use pulldown_cmark:: Tag :: { BlockQuote , CodeBlock , FootnoteDefinition , Heading , Item , Link , Paragraph } ;
12
12
use pulldown_cmark:: { BrokenLink , CodeBlockKind , CowStr , Options , TagEnd } ;
13
+ use rustc_ast:: attr:: AttributeExt ;
14
+ use rustc_ast:: token:: CommentKind ;
13
15
use rustc_data_structures:: fx:: FxHashSet ;
14
- use rustc_errors:: Applicability ;
15
- use rustc_hir:: { Attribute , ImplItemKind , ItemKind , Node , Safety , TraitItemKind } ;
16
- use rustc_lint:: { EarlyContext , EarlyLintPass , LateContext , LateLintPass , LintContext } ;
16
+ use rustc_errors:: { Applicability , Diag , DiagMessage , MultiSpan } ;
17
+ use rustc_hir:: { AttrStyle , Attribute , ImplItemKind , ItemKind , Node , Safety , TraitItemKind } ;
18
+ use rustc_lint:: { EarlyContext , EarlyLintPass , LateContext , LateLintPass , Lint , LintContext } ;
17
19
use rustc_resolve:: rustdoc:: {
18
20
DocFragment , add_doc_fragment, attrs_to_doc_fragments, main_body_opts, source_span_for_markdown_range,
19
21
span_of_fragments,
@@ -740,14 +742,14 @@ impl<'tcx> LateLintPass<'tcx> for Documentation {
740
742
) ;
741
743
}
742
744
} ,
743
- ItemKind :: Trait ( _, _, unsafety, ..) => match ( headers. safety , unsafety) {
745
+ ItemKind :: Trait ( _, _, unsafety, ..) => match ( headers. safety . is_missing ( ) , unsafety) {
744
746
( false , Safety :: Unsafe ) => span_lint (
745
747
cx,
746
748
MISSING_SAFETY_DOC ,
747
749
cx. tcx . def_span ( item. owner_id ) ,
748
750
"docs for unsafe trait missing `# Safety` section" ,
749
751
) ,
750
- ( true , Safety :: Safe ) => span_lint (
752
+ ( true , Safety :: Safe ) => headers . safety . lint (
751
753
cx,
752
754
UNNECESSARY_SAFETY_DOC ,
753
755
cx. tcx . def_span ( item. owner_id ) ,
@@ -800,11 +802,101 @@ impl Fragments<'_> {
800
802
}
801
803
}
802
804
803
- #[ derive( Copy , Clone , Default ) ]
805
+ #[ derive( Clone , Default ) ]
806
+ enum DocHeaderInfo {
807
+ #[ default]
808
+ None ,
809
+ Found ,
810
+ SuspiciousHtml ( Option < Span > , & ' static str , Vec < Container > ) ,
811
+ }
812
+
813
+ impl DocHeaderInfo {
814
+ fn suspicious_html (
815
+ cx : & LateContext < ' _ > ,
816
+ fragments : Fragments < ' _ > ,
817
+ idx : usize ,
818
+ attrs : & [ Attribute ] ,
819
+ containers : & [ Container ] ,
820
+ ) -> DocHeaderInfo {
821
+ use rustc_ast:: token:: CommentKind ;
822
+ use rustc_hir:: AttrStyle ;
823
+ let span = fragments. span ( cx, idx..idx) ;
824
+ let indent = span
825
+ . and_then ( |span| fragments. fragments . iter ( ) . find ( |fragment| fragment. span . overlaps ( span) ) )
826
+ . map ( |fragment| fragment. indent )
827
+ . unwrap_or ( 0 ) ;
828
+ DocHeaderInfo :: SuspiciousHtml (
829
+ span,
830
+ span. and_then ( |span| find_doc_attr_by_span ( attrs, span) ) . map_or (
831
+ "" ,
832
+ |( _doc_attr, doc_attr_comment_kind, attr_style) | match ( doc_attr_comment_kind, attr_style) {
833
+ ( CommentKind :: Block , _) => & " " [ ..indent] ,
834
+ ( CommentKind :: Line , AttrStyle :: Outer ) => & "/// " [ ..indent + 3 ] ,
835
+ ( CommentKind :: Line , AttrStyle :: Inner ) => & "//! " [ ..indent + 3 ] ,
836
+ } ,
837
+ ) ,
838
+ containers. to_vec ( ) ,
839
+ )
840
+ }
841
+ fn is_missing ( & self ) -> bool {
842
+ matches ! ( self , DocHeaderInfo :: None | DocHeaderInfo :: SuspiciousHtml ( ..) )
843
+ }
844
+ fn lint ( & self , cx : & LateContext < ' _ > , lint : & ' static Lint , sp : impl Into < MultiSpan > , msg : impl Into < DiagMessage > ) {
845
+ self . lint_and_then ( cx, lint, sp, msg, |_| { } )
846
+ }
847
+ fn lint_and_then (
848
+ & self ,
849
+ cx : & LateContext < ' _ > ,
850
+ lint : & ' static Lint ,
851
+ sp : impl Into < MultiSpan > ,
852
+ msg : impl Into < DiagMessage > ,
853
+ f : impl FnOnce ( & mut Diag < ' _ , ( ) > ) ,
854
+ ) {
855
+ if let DocHeaderInfo :: SuspiciousHtml ( html_span, comment_prefix, containers) = self {
856
+ span_lint_and_then ( cx, lint, sp, msg, |diag| {
857
+ f ( diag) ;
858
+ diag. note ( "markdown syntax is not recognized within a block of raw HTML code" ) ;
859
+ if let Some ( html_span) = html_span {
860
+ diag. span_suggestion (
861
+ * html_span,
862
+ "to recognize this text as a header, add a blank line" ,
863
+ format ! (
864
+ "\n {comment_prefix}{containers}" ,
865
+ containers = containers
866
+ . iter( )
867
+ . map( Container :: map_to_text)
868
+ . collect:: <Vec <& str >>( )
869
+ . join( "" )
870
+ ) ,
871
+ Applicability :: Unspecified ,
872
+ ) ;
873
+ } else {
874
+ diag. help (
875
+ "to recognize a markdown header nested within an HTML element,\
876
+ add a blank line between the `#` and the HTML",
877
+ ) ;
878
+ }
879
+ } ) ;
880
+ } else {
881
+ span_lint ( cx, lint, sp, msg) ;
882
+ }
883
+ }
884
+ }
885
+
886
+ fn find_doc_attr_by_span ( attrs : & [ Attribute ] , span : Span ) -> Option < ( & Attribute , CommentKind , AttrStyle ) > {
887
+ let ( doc_attr, ( _, doc_attr_comment_kind) , attr_style) = attrs
888
+ . iter ( )
889
+ . filter ( |attr| attr. span ( ) . overlaps ( span) )
890
+ . rev ( )
891
+ . find_map ( |attr| Some ( ( attr, attr. doc_str_and_comment_kind ( ) ?, attr. doc_resolution_scope ( ) ?) ) ) ?;
892
+ Some ( ( doc_attr, doc_attr_comment_kind, attr_style) )
893
+ }
894
+
895
+ #[ derive( Clone , Default ) ]
804
896
struct DocHeaders {
805
- safety : bool ,
806
- errors : bool ,
807
- panics : bool ,
897
+ safety : DocHeaderInfo ,
898
+ errors : DocHeaderInfo ,
899
+ panics : DocHeaderInfo ,
808
900
first_paragraph_len : usize ,
809
901
}
810
902
@@ -900,11 +992,22 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
900
992
901
993
const RUST_CODE : & [ & str ] = & [ "rust" , "no_run" , "should_panic" , "compile_fail" ] ;
902
994
995
+ #[ derive( Clone ) ]
903
996
enum Container {
904
997
Blockquote ,
905
998
List ( usize ) ,
906
999
}
907
1000
1001
+ impl Container {
1002
+ fn map_to_text ( & self ) -> & ' static str {
1003
+ match self {
1004
+ Container :: Blockquote => "> " ,
1005
+ // numbered list can have up to nine digits, plus the dot, plus four spaces on either side
1006
+ Container :: List ( indent) => & " " [ 0 ..* indent] ,
1007
+ }
1008
+ }
1009
+ }
1010
+
908
1011
/// Scan the documentation for code links that are back-to-back with code spans.
909
1012
///
910
1013
/// This is done separately from the rest of the docs, because that makes it easier to produce
@@ -1011,6 +1114,51 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
1011
1114
} else if tag. starts_with ( "</blockquote" ) || tag. starts_with ( "</q" ) {
1012
1115
blockquote_level -= 1 ;
1013
1116
}
1117
+ if headers. safety . is_missing ( ) &&
1118
+ let Some ( idx) = tag. find ( "# Safety" ) . filter ( |idx| tag[ idx + 8 ..] . trim ( ) . bytes ( ) . all ( |c| c == b'#' ) )
1119
+ . or_else ( || tag. find ( "Safety\n " ) . filter ( |_| matches ! ( events. peek( ) , Some ( ( Html ( ln) | InlineHtml ( ln) , _) ) if ln. trim( ) . bytes( ) . all( |c| c == b'=' ) || ln. trim( ) . bytes( ) . all( |c| c == b'-' ) ) ) )
1120
+ . or_else ( || tag. find ( "# SAFETY" ) . filter ( |idx| tag[ idx + 8 ..] . trim ( ) . bytes ( ) . all ( |c| c == b'#' ) ) )
1121
+ . or_else ( || tag. find ( "SAFETY\n " ) . filter ( |_| matches ! ( events. peek( ) , Some ( ( Html ( ln) | InlineHtml ( ln) , _) ) if ln. trim( ) . bytes( ) . all( |c| c == b'=' ) || ln. trim( ) . bytes( ) . all( |c| c == b'-' ) ) ) )
1122
+ . or_else ( || tag. find ( "# Implementation safety" ) . filter ( |idx| tag[ idx + 23 ..] . trim ( ) . bytes ( ) . all ( |c| c == b'#' ) ) )
1123
+ . or_else ( || tag. find ( "Implementation safety\n " ) . filter ( |_| matches ! ( events. peek( ) , Some ( ( Html ( ln) | InlineHtml ( ln) , _) ) if ln. trim( ) . bytes( ) . all( |c| c == b'=' ) || ln. trim( ) . bytes( ) . all( |c| c == b'-' ) ) ) )
1124
+ . or_else ( || tag. find ( "# Implementation Safety" ) . filter ( |idx| tag[ idx + 23 ..] . trim ( ) . bytes ( ) . all ( |c| c == b'#' ) ) )
1125
+ . or_else ( || tag. find ( "Implementation Safety\n " ) . filter ( |_| matches ! ( events. peek( ) , Some ( ( Html ( ln) | InlineHtml ( ln) , _) ) if ln. trim( ) . bytes( ) . all( |c| c == b'=' ) || ln. trim( ) . bytes( ) . all( |c| c == b'-' ) ) ) ) &&
1126
+ tag[ ..idx] . trim ( ) . bytes ( ) . all ( |c| c == b'#' )
1127
+ {
1128
+ headers. safety = DocHeaderInfo :: suspicious_html (
1129
+ cx,
1130
+ fragments,
1131
+ range. start ,
1132
+ attrs,
1133
+ & containers,
1134
+ ) ;
1135
+ }
1136
+ if headers. errors . is_missing ( ) &&
1137
+ let Some ( idx) = tag. find ( "# Errors" ) . filter ( |idx| tag[ idx + 8 ..] . trim ( ) . bytes ( ) . all ( |c| c == b'#' ) )
1138
+ . or_else ( || tag. find ( "Errors\n " ) . filter ( |_| matches ! ( events. peek( ) , Some ( ( Html ( ln) | InlineHtml ( ln) , _) ) if ln. trim( ) . bytes( ) . all( |c| c == b'=' ) || ln. trim( ) . bytes( ) . all( |c| c == b'-' ) ) ) ) &&
1139
+ tag[ ..idx] . trim ( ) . bytes ( ) . all ( |c| c == b'#' )
1140
+ {
1141
+ headers. errors = DocHeaderInfo :: suspicious_html (
1142
+ cx,
1143
+ fragments,
1144
+ range. start ,
1145
+ attrs,
1146
+ & containers,
1147
+ ) ;
1148
+ }
1149
+ if headers. panics . is_missing ( ) &&
1150
+ let Some ( idx) = tag. find ( "# Panics" ) . filter ( |idx| tag[ idx + 8 ..] . trim ( ) . bytes ( ) . all ( |c| c == b'#' ) )
1151
+ . or_else ( || tag. find ( "Panics\n " ) . filter ( |_| matches ! ( events. peek( ) , Some ( ( Html ( ln) | InlineHtml ( ln) , _) ) if ln. trim( ) . bytes( ) . all( |c| c == b'=' ) || ln. trim( ) . bytes( ) . all( |c| c == b'-' ) ) ) ) &&
1152
+ tag[ ..idx] . trim ( ) . bytes ( ) . all ( |c| c == b'#' )
1153
+ {
1154
+ headers. panics = DocHeaderInfo :: suspicious_html (
1155
+ cx,
1156
+ fragments,
1157
+ range. start ,
1158
+ attrs,
1159
+ & containers,
1160
+ ) ;
1161
+ }
1014
1162
} ,
1015
1163
Start ( BlockQuote ( _) ) => {
1016
1164
blockquote_level += 1 ;
@@ -1197,12 +1345,28 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
1197
1345
continue ;
1198
1346
}
1199
1347
let trimmed_text = text. trim ( ) ;
1200
- headers. safety |= in_heading && trimmed_text == "Safety" ;
1201
- headers. safety |= in_heading && trimmed_text == "SAFETY" ;
1202
- headers. safety |= in_heading && trimmed_text == "Implementation safety" ;
1203
- headers. safety |= in_heading && trimmed_text == "Implementation Safety" ;
1204
- headers. errors |= in_heading && trimmed_text == "Errors" ;
1205
- headers. panics |= in_heading && trimmed_text == "Panics" ;
1348
+ if in_heading {
1349
+ if headers. safety . is_missing ( )
1350
+ && (
1351
+ trimmed_text == "Safety"
1352
+ || trimmed_text == "SAFETY"
1353
+ || trimmed_text == "Implementation safety"
1354
+ || trimmed_text == "Implementation Safety"
1355
+ )
1356
+ {
1357
+ headers. safety = DocHeaderInfo :: Found ;
1358
+ }
1359
+ if headers. errors . is_missing ( )
1360
+ && trimmed_text == "Errors"
1361
+ {
1362
+ headers. errors = DocHeaderInfo :: Found ;
1363
+ }
1364
+ if headers. panics . is_missing ( )
1365
+ && trimmed_text == "Panics"
1366
+ {
1367
+ headers. panics = DocHeaderInfo :: Found ;
1368
+ }
1369
+ }
1206
1370
if in_code {
1207
1371
if is_rust && !no_test {
1208
1372
let edition = edition. unwrap_or_else ( || cx. tcx . sess . edition ( ) ) ;
0 commit comments