@@ -38,7 +38,7 @@ fn drop_tag(
3838 tags : & mut Vec < ( String , Range < usize > ) > ,
3939 tag_name : String ,
4040 range : Range < usize > ,
41- f : & impl Fn ( & str , & Range < usize > ) ,
41+ f : & impl Fn ( & str , & Range < usize > , bool ) ,
4242) {
4343 let tag_name_low = tag_name. to_lowercase ( ) ;
4444 if let Some ( pos) = tags. iter ( ) . rposition ( |( t, _) | t. to_lowercase ( ) == tag_name_low) {
@@ -59,14 +59,42 @@ fn drop_tag(
5959 // `tags` is used as a queue, meaning that everything after `pos` is included inside it.
6060 // So `<h2><h3></h2>` will look like `["h2", "h3"]`. So when closing `h2`, we will still
6161 // 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 ) ;
6363 }
6464 // Remove the `tag_name` that was originally closed
6565 tags. pop ( ) ;
6666 } else {
6767 // It can happen for example in this case: `<h2></script></h2>` (the `h2` tag isn't required
6868 // 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) ;
7098 }
7199}
72100
@@ -76,7 +104,7 @@ fn extract_html_tag(
76104 range : & Range < usize > ,
77105 start_pos : usize ,
78106 iter : & mut Peekable < CharIndices < ' _ > > ,
79- f : & impl Fn ( & str , & Range < usize > ) ,
107+ f : & impl Fn ( & str , & Range < usize > , bool ) ,
80108) {
81109 let mut tag_name = String :: new ( ) ;
82110 let mut is_closing = false ;
@@ -140,7 +168,7 @@ fn extract_tags(
140168 text : & str ,
141169 range : Range < usize > ,
142170 is_in_comment : & mut Option < Range < usize > > ,
143- f : & impl Fn ( & str , & Range < usize > ) ,
171+ f : & impl Fn ( & str , & Range < usize > , bool ) ,
144172) {
145173 let mut iter = text. char_indices ( ) . peekable ( ) ;
146174
@@ -178,14 +206,42 @@ impl<'a, 'tcx> DocVisitor for InvalidHtmlTagsLinter<'a, 'tcx> {
178206 } ;
179207 let dox = item. attrs . collapsed_doc_value ( ) . unwrap_or_default ( ) ;
180208 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 | {
182210 let sp = match super :: source_span_for_markdown_range ( tcx, & dox, range, & item. attrs )
183211 {
184212 Some ( sp) => sp,
185213 None => item. attr_span ( tcx) ,
186214 } ;
187215 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 ( )
189245 } ) ;
190246 } ;
191247
@@ -210,11 +266,11 @@ impl<'a, 'tcx> DocVisitor for InvalidHtmlTagsLinter<'a, 'tcx> {
210266 let t = t. to_lowercase ( ) ;
211267 !ALLOWED_UNCLOSED . contains ( & t. as_str ( ) )
212268 } ) {
213- report_diag ( & format ! ( "unclosed HTML tag `{}`" , tag) , range) ;
269+ report_diag ( & format ! ( "unclosed HTML tag `{}`" , tag) , range, true ) ;
214270 }
215271
216272 if let Some ( range) = is_in_comment {
217- report_diag ( "Unclosed HTML comment" , & range) ;
273+ report_diag ( "Unclosed HTML comment" , & range, false ) ;
218274 }
219275 }
220276
0 commit comments