Skip to content

Commit c25909c

Browse files
committed
fix false negative in rustdoc::invalid_html_tags with tags like <img>
previously, this lint did not distinguish between `<img` and `<img>`, and since the latter should be accepted under html5, the former was also accepted.
1 parent 425a9c0 commit c25909c

File tree

1 file changed

+17
-6
lines changed

1 file changed

+17
-6
lines changed

src/librustdoc/passes/lint/html_tags.rs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,16 +132,16 @@ pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: &
132132
match event {
133133
Event::Start(Tag::CodeBlock(_)) => in_code_block = true,
134134
Event::Html(text) | Event::InlineHtml(text) if !in_code_block => {
135-
extract_tags(&mut tags, &text, range, &mut is_in_comment, &report_diag)
135+
extract_tags(&mut tags, &text, range, dox, &mut is_in_comment, &report_diag)
136136
}
137137
Event::End(TagEnd::CodeBlock) => in_code_block = false,
138138
_ => {}
139139
}
140140
}
141141

142-
for (tag, range) in tags.iter().filter(|(t, _)| {
142+
for (tag, range) in tags.iter().filter(|(t, range)| {
143143
let t = t.to_lowercase();
144-
!ALLOWED_UNCLOSED.contains(&t.as_str())
144+
!is_implicitly_self_closing(&t, range.clone(), dox)
145145
}) {
146146
report_diag(format!("unclosed HTML tag `{tag}`"), range, true);
147147
}
@@ -151,15 +151,24 @@ pub(crate) fn visit_item(cx: &DocContext<'_>, item: &Item, hir_id: HirId, dox: &
151151
}
152152
}
153153

154+
/// These tags are interpreted as self-closing if they lack an explict closing tag.
154155
const ALLOWED_UNCLOSED: &[&str] = &[
155156
"area", "base", "br", "col", "embed", "hr", "img", "input", "keygen", "link", "meta", "param",
156157
"source", "track", "wbr",
157158
];
158159

160+
/// Allows constructs like `<img>`, but not `<img`.
161+
fn is_implicitly_self_closing(tag_name: &str, range: Range<usize>, dox: &str) -> bool {
162+
ALLOWED_UNCLOSED.contains(&tag_name) &&
163+
// search in reverse order to try to exit early, since `>` appears near the end in the happy case
164+
dox[range].bytes().rev().any(|c| c == b'>')
165+
}
166+
159167
fn drop_tag(
160168
tags: &mut Vec<(String, Range<usize>)>,
161169
tag_name: String,
162170
range: Range<usize>,
171+
dox: &str,
163172
f: &impl Fn(String, &Range<usize>, bool),
164173
) {
165174
let tag_name_low = tag_name.to_lowercase();
@@ -175,7 +184,7 @@ fn drop_tag(
175184
continue;
176185
}
177186
let last_tag_name_low = last_tag_name.to_lowercase();
178-
if ALLOWED_UNCLOSED.contains(&last_tag_name_low.as_str()) {
187+
if is_implicitly_self_closing(&last_tag_name_low, last_tag_span.clone(), dox) {
179188
continue;
180189
}
181190
// `tags` is used as a queue, meaning that everything after `pos` is included inside it.
@@ -256,6 +265,7 @@ fn extract_html_tag(
256265
tags: &mut Vec<(String, Range<usize>)>,
257266
text: &str,
258267
range: &Range<usize>,
268+
dox: &str,
259269
start_pos: usize,
260270
iter: &mut Peekable<CharIndices<'_>>,
261271
f: &impl Fn(String, &Range<usize>, bool),
@@ -306,7 +316,7 @@ fn extract_html_tag(
306316
break;
307317
}
308318
}
309-
drop_tag(tags, tag_name, r, f);
319+
drop_tag(tags, tag_name, r, dox, f);
310320
} else {
311321
let mut is_self_closing = false;
312322
let mut quote_pos = None;
@@ -374,6 +384,7 @@ fn extract_tags(
374384
tags: &mut Vec<(String, Range<usize>)>,
375385
text: &str,
376386
range: Range<usize>,
387+
dox: &str,
377388
is_in_comment: &mut Option<Range<usize>>,
378389
f: &impl Fn(String, &Range<usize>, bool),
379390
) {
@@ -395,7 +406,7 @@ fn extract_tags(
395406
end: range.start + start_pos + 3,
396407
});
397408
} else {
398-
extract_html_tag(tags, text, &range, start_pos, &mut iter, f);
409+
extract_html_tag(tags, text, &range, dox, start_pos, &mut iter, f);
399410
}
400411
}
401412
}

0 commit comments

Comments
 (0)