Skip to content

Commit 214ce57

Browse files
committed
rustdoc: correct unclosed HTML tags as generics
1 parent ad88831 commit 214ce57

File tree

7 files changed

+281
-9
lines changed

7 files changed

+281
-9
lines changed

src/librustdoc/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#![feature(rustc_private)]
66
#![feature(array_methods)]
77
#![feature(assert_matches)]
8+
#![feature(bool_to_option)]
89
#![feature(box_patterns)]
910
#![feature(control_flow_enum)]
1011
#![feature(box_syntax)]

src/librustdoc/passes/html_tags.rs

+72-9
Original file line numberDiff line numberDiff line change
@@ -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 - 2..current_pos] == "::" {
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,49 @@ 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' 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 - 1..range.end] == ">")
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+
if let Ok(generics_snippet) =
235+
tcx.sess.source_map().span_to_snippet(generics_sp)
236+
{
237+
// short form is chosen here because ``Vec<i32>`` would be confusing.
238+
diag.span_suggestion_short(
239+
generics_sp,
240+
"try marking as source code with `backticks`",
241+
format!("`{}`", generics_snippet),
242+
Applicability::MachineApplicable,
243+
);
244+
} else {
245+
diag.span_help(
246+
generics_sp,
247+
"try marking as source code with `backticks`",
248+
);
249+
}
250+
}
251+
diag.emit()
189252
});
190253
};
191254

@@ -210,11 +273,11 @@ impl<'a, 'tcx> DocVisitor for InvalidHtmlTagsLinter<'a, 'tcx> {
210273
let t = t.to_lowercase();
211274
!ALLOWED_UNCLOSED.contains(&t.as_str())
212275
}) {
213-
report_diag(&format!("unclosed HTML tag `{}`", tag), range);
276+
report_diag(&format!("unclosed HTML tag `{}`", tag), range, true);
214277
}
215278

216279
if let Some(range) = is_in_comment {
217-
report_diag("Unclosed HTML comment", &range);
280+
report_diag("Unclosed HTML comment", &range, false);
218281
}
219282
}
220283

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#![deny(rustdoc::invalid_html_tags)]
2+
3+
/// This Vec<32> thing!
4+
// Numbers aren't valid HTML tags, so no error.
5+
pub struct ConstGeneric;
6+
7+
/// This Vec<i32, i32> thing!
8+
// HTML tags cannot contain commas, so no error.
9+
pub struct MultipleGenerics;
10+
11+
/// This Vec<i32 class="test"> thing!
12+
//~^ERROR unclosed HTML tag `i32`
13+
// HTML attributes shouldn't be treated as Rust syntax, so no suggestions.
14+
pub struct TagWithAttributes;
15+
16+
/// This Vec<i32></i32> thing!
17+
// There should be no error, and no suggestion, since the tags are balanced.
18+
pub struct DoNotWarnOnMatchingTags;
19+
20+
/// This Vec</i32> thing!
21+
//~^ERROR unopened HTML tag `i32`
22+
// This should produce an error, but no suggestion.
23+
pub struct EndTagsAreNotValidRustSyntax;
24+
25+
/// This 123<i32> thing!
26+
//~^ERROR unclosed HTML tag `i32`
27+
// This should produce an error, but no suggestion.
28+
pub struct NumbersAreNotPaths;
29+
30+
/// This Vec:<i32> thing!
31+
//~^ERROR unclosed HTML tag `i32`
32+
// This should produce an error, but no suggestion.
33+
pub struct InvalidTurbofish;
34+
35+
/// This [link](https://rust-lang.org)<i32> thing!
36+
//~^ERROR unclosed HTML tag `i32`
37+
// This should produce an error, but no suggestion.
38+
pub struct BareTurbofish;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
error: unclosed HTML tag `i32`
2+
--> $DIR/html-as-generics-no-suggestions.rs:11:13
3+
|
4+
LL | /// This Vec<i32 class="test"> thing!
5+
| ^^^^
6+
|
7+
note: the lint level is defined here
8+
--> $DIR/html-as-generics-no-suggestions.rs:1:9
9+
|
10+
LL | #![deny(rustdoc::invalid_html_tags)]
11+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
12+
13+
error: unopened HTML tag `i32`
14+
--> $DIR/html-as-generics-no-suggestions.rs:20:13
15+
|
16+
LL | /// This Vec</i32> thing!
17+
| ^^^^^^
18+
19+
error: unclosed HTML tag `i32`
20+
--> $DIR/html-as-generics-no-suggestions.rs:25:13
21+
|
22+
LL | /// This 123<i32> thing!
23+
| ^^^^^
24+
25+
error: unclosed HTML tag `i32`
26+
--> $DIR/html-as-generics-no-suggestions.rs:30:14
27+
|
28+
LL | /// This Vec:<i32> thing!
29+
| ^^^^^
30+
31+
error: unclosed HTML tag `i32`
32+
--> $DIR/html-as-generics-no-suggestions.rs:35:39
33+
|
34+
LL | /// This [link](https://rust-lang.org)<i32> thing!
35+
| ^^^^^
36+
37+
error: aborting due to 5 previous errors
38+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// run-rustfix
2+
#![deny(rustdoc::invalid_html_tags)]
3+
4+
/// This `Vec<i32>` thing!
5+
//~^ERROR unclosed HTML tag `i32`
6+
//~|HELP try marking as source
7+
//~|SUGGESTION `Vec<i32>`
8+
pub struct Generic;
9+
10+
/// This `vec::Vec<i32>` thing!
11+
//~^ERROR unclosed HTML tag `i32`
12+
//~|HELP try marking as source
13+
//~|SUGGESTION `vec::Vec<i32>`
14+
pub struct GenericPath;
15+
16+
/// This `i32<i32>` thing!
17+
//~^ERROR unclosed HTML tag `i32`
18+
//~|HELP try marking as source
19+
//~|SUGGESTION `i32<i32>`
20+
pub struct PathsCanContainTrailingNumbers;
21+
22+
/// This `Vec::<i32>` thing!
23+
//~^ERROR unclosed HTML tag `i32`
24+
//~|HELP try marking as source
25+
//~|SUGGESTION `Vec::<i32>`
26+
pub struct Turbofish;
27+
28+
/// This [link](https://rust-lang.org)`::<i32>` thing!
29+
//~^ERROR unclosed HTML tag `i32`
30+
//~|HELP try marking as source
31+
//~|SUGGESTION `::<i32>`
32+
pub struct BareTurbofish;
33+
34+
/// This <span>`Vec::<i32>`</span> thing!
35+
//~^ERROR unclosed HTML tag `i32`
36+
//~|HELP try marking as source
37+
//~|SUGGESTION `Vec::<i32>`
38+
pub struct Nested;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// run-rustfix
2+
#![deny(rustdoc::invalid_html_tags)]
3+
4+
/// This Vec<i32> thing!
5+
//~^ERROR unclosed HTML tag `i32`
6+
//~|HELP try marking as source
7+
//~|SUGGESTION `Vec<i32>`
8+
pub struct Generic;
9+
10+
/// This vec::Vec<i32> thing!
11+
//~^ERROR unclosed HTML tag `i32`
12+
//~|HELP try marking as source
13+
//~|SUGGESTION `vec::Vec<i32>`
14+
pub struct GenericPath;
15+
16+
/// This i32<i32> thing!
17+
//~^ERROR unclosed HTML tag `i32`
18+
//~|HELP try marking as source
19+
//~|SUGGESTION `i32<i32>`
20+
pub struct PathsCanContainTrailingNumbers;
21+
22+
/// This Vec::<i32> thing!
23+
//~^ERROR unclosed HTML tag `i32`
24+
//~|HELP try marking as source
25+
//~|SUGGESTION `Vec::<i32>`
26+
pub struct Turbofish;
27+
28+
/// This [link](https://rust-lang.org)::<i32> thing!
29+
//~^ERROR unclosed HTML tag `i32`
30+
//~|HELP try marking as source
31+
//~|SUGGESTION `::<i32>`
32+
pub struct BareTurbofish;
33+
34+
/// This <span>Vec::<i32></span> thing!
35+
//~^ERROR unclosed HTML tag `i32`
36+
//~|HELP try marking as source
37+
//~|SUGGESTION `Vec::<i32>`
38+
pub struct Nested;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
error: unclosed HTML tag `i32`
2+
--> $DIR/html-as-generics.rs:4:13
3+
|
4+
LL | /// This Vec<i32> thing!
5+
| ---^^^^^
6+
| |
7+
| help: try marking as source code with `backticks`
8+
|
9+
note: the lint level is defined here
10+
--> $DIR/html-as-generics.rs:2:9
11+
|
12+
LL | #![deny(rustdoc::invalid_html_tags)]
13+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
14+
15+
error: unclosed HTML tag `i32`
16+
--> $DIR/html-as-generics.rs:10:18
17+
|
18+
LL | /// This vec::Vec<i32> thing!
19+
| --------^^^^^
20+
| |
21+
| help: try marking as source code with `backticks`
22+
23+
error: unclosed HTML tag `i32`
24+
--> $DIR/html-as-generics.rs:16:13
25+
|
26+
LL | /// This i32<i32> thing!
27+
| ---^^^^^
28+
| |
29+
| help: try marking as source code with `backticks`
30+
31+
error: unclosed HTML tag `i32`
32+
--> $DIR/html-as-generics.rs:22:15
33+
|
34+
LL | /// This Vec::<i32> thing!
35+
| -----^^^^^
36+
| |
37+
| help: try marking as source code with `backticks`
38+
39+
error: unclosed HTML tag `i32`
40+
--> $DIR/html-as-generics.rs:28:41
41+
|
42+
LL | /// This [link](https://rust-lang.org)::<i32> thing!
43+
| --^^^^^
44+
| |
45+
| help: try marking as source code with `backticks`
46+
47+
error: unclosed HTML tag `i32`
48+
--> $DIR/html-as-generics.rs:34:21
49+
|
50+
LL | /// This <span>Vec::<i32></span> thing!
51+
| -----^^^^^
52+
| |
53+
| help: try marking as source code with `backticks`
54+
55+
error: aborting due to 6 previous errors
56+

0 commit comments

Comments
 (0)