@@ -38,7 +38,7 @@ fn drop_tag(
38
38
tags : & mut Vec < ( String , Range < usize > ) > ,
39
39
tag_name : String ,
40
40
range : Range < usize > ,
41
- f : & impl Fn ( & str , & Range < usize > ) ,
41
+ f : & impl Fn ( & str , & Range < usize > , bool ) ,
42
42
) {
43
43
let tag_name_low = tag_name. to_lowercase ( ) ;
44
44
if let Some ( pos) = tags. iter ( ) . rposition ( |( t, _) | t. to_lowercase ( ) == tag_name_low) {
@@ -59,14 +59,42 @@ fn drop_tag(
59
59
// `tags` is used as a queue, meaning that everything after `pos` is included inside it.
60
60
// So `<h2><h3></h2>` will look like `["h2", "h3"]`. So when closing `h2`, we will still
61
61
// have `h3`, meaning the tag wasn't closed as it should have.
62
- f ( & format ! ( "unclosed HTML tag `{}`" , last_tag_name) , & last_tag_span) ;
62
+ f ( & format ! ( "unclosed HTML tag `{}`" , last_tag_name) , & last_tag_span, true ) ;
63
63
}
64
64
// Remove the `tag_name` that was originally closed
65
65
tags. pop ( ) ;
66
66
} else {
67
67
// It can happen for example in this case: `<h2></script></h2>` (the `h2` tag isn't required
68
68
// but it helps for the visualization).
69
- f ( & format ! ( "unopened HTML tag `{}`" , tag_name) , & range) ;
69
+ f ( & format ! ( "unopened HTML tag `{}`" , tag_name) , & range, false ) ;
70
+ }
71
+ }
72
+
73
+ fn extract_path_backwards ( text : & str , end_pos : usize ) -> Option < usize > {
74
+ use rustc_lexer:: { is_id_continue, is_id_start} ;
75
+ let mut current_pos = end_pos;
76
+ loop {
77
+ if current_pos >= 2 && text[ ..current_pos] . ends_with ( "::" ) {
78
+ current_pos -= 2 ;
79
+ }
80
+ let new_pos = text[ ..current_pos]
81
+ . char_indices ( )
82
+ . rev ( )
83
+ . take_while ( |( _, c) | is_id_start ( * c) || is_id_continue ( * c) )
84
+ . reduce ( |_accum, item| item)
85
+ . and_then ( |( new_pos, c) | is_id_start ( c) . then_some ( new_pos) ) ;
86
+ if let Some ( new_pos) = new_pos {
87
+ if current_pos != new_pos {
88
+ current_pos = new_pos;
89
+ continue ;
90
+ }
91
+ }
92
+ break ;
93
+ }
94
+ if current_pos == end_pos {
95
+ return None ;
96
+ } else {
97
+ return Some ( current_pos) ;
70
98
}
71
99
}
72
100
@@ -76,7 +104,7 @@ fn extract_html_tag(
76
104
range : & Range < usize > ,
77
105
start_pos : usize ,
78
106
iter : & mut Peekable < CharIndices < ' _ > > ,
79
- f : & impl Fn ( & str , & Range < usize > ) ,
107
+ f : & impl Fn ( & str , & Range < usize > , bool ) ,
80
108
) {
81
109
let mut tag_name = String :: new ( ) ;
82
110
let mut is_closing = false ;
@@ -140,7 +168,7 @@ fn extract_tags(
140
168
text : & str ,
141
169
range : Range < usize > ,
142
170
is_in_comment : & mut Option < Range < usize > > ,
143
- f : & impl Fn ( & str , & Range < usize > ) ,
171
+ f : & impl Fn ( & str , & Range < usize > , bool ) ,
144
172
) {
145
173
let mut iter = text. char_indices ( ) . peekable ( ) ;
146
174
@@ -178,14 +206,42 @@ impl<'a, 'tcx> DocVisitor for InvalidHtmlTagsLinter<'a, 'tcx> {
178
206
} ;
179
207
let dox = item. attrs . collapsed_doc_value ( ) . unwrap_or_default ( ) ;
180
208
if !dox. is_empty ( ) {
181
- let report_diag = |msg : & str , range : & Range < usize > | {
209
+ let report_diag = |msg : & str , range : & Range < usize > , is_open_tag : bool | {
182
210
let sp = match super :: source_span_for_markdown_range ( tcx, & dox, range, & item. attrs )
183
211
{
184
212
Some ( sp) => sp,
185
213
None => item. attr_span ( tcx) ,
186
214
} ;
187
215
tcx. struct_span_lint_hir ( crate :: lint:: INVALID_HTML_TAGS , hir_id, sp, |lint| {
188
- lint. build ( msg) . emit ( )
216
+ use rustc_lint_defs:: Applicability ;
217
+ let mut diag = lint. build ( msg) ;
218
+ // If a tag looks like `<this>`, it might actually be a generic.
219
+ // We don't try to detect stuff `<like, this>` because that's not valid HTML,
220
+ // and we don't try to detect stuff `<like this>` because that's not valid Rust.
221
+ if let Some ( Some ( generics_start) ) = ( is_open_tag
222
+ && dox[ ..range. end ] . ends_with ( ">" ) )
223
+ . then ( || extract_path_backwards ( & dox, range. start ) )
224
+ {
225
+ let generics_sp = match super :: source_span_for_markdown_range (
226
+ tcx,
227
+ & dox,
228
+ & ( generics_start..range. end ) ,
229
+ & item. attrs ,
230
+ ) {
231
+ Some ( sp) => sp,
232
+ None => item. attr_span ( tcx) ,
233
+ } ;
234
+ // multipart form is chosen here because ``Vec<i32>`` would be confusing.
235
+ diag. multipart_suggestion (
236
+ "try marking as source code" ,
237
+ vec ! [
238
+ ( generics_sp. shrink_to_lo( ) , String :: from( "`" ) ) ,
239
+ ( generics_sp. shrink_to_hi( ) , String :: from( "`" ) ) ,
240
+ ] ,
241
+ Applicability :: MaybeIncorrect ,
242
+ ) ;
243
+ }
244
+ diag. emit ( )
189
245
} ) ;
190
246
} ;
191
247
@@ -210,11 +266,11 @@ impl<'a, 'tcx> DocVisitor for InvalidHtmlTagsLinter<'a, 'tcx> {
210
266
let t = t. to_lowercase ( ) ;
211
267
!ALLOWED_UNCLOSED . contains ( & t. as_str ( ) )
212
268
} ) {
213
- report_diag ( & format ! ( "unclosed HTML tag `{}`" , tag) , range) ;
269
+ report_diag ( & format ! ( "unclosed HTML tag `{}`" , tag) , range, true ) ;
214
270
}
215
271
216
272
if let Some ( range) = is_in_comment {
217
- report_diag ( "Unclosed HTML comment" , & range) ;
273
+ report_diag ( "Unclosed HTML comment" , & range, false ) ;
218
274
}
219
275
}
220
276
0 commit comments