1 use super::{span_of_attrs, Pass}
;
3 use crate::core
::DocContext
;
4 use crate::fold
::DocFolder
;
5 use crate::html
::markdown
::opts
;
7 use pulldown_cmark
::{Event, Parser, Tag}
;
8 use rustc_session
::lint
;
9 use std
::iter
::Peekable
;
10 use std
::str::CharIndices
;
12 crate const CHECK_INVALID_HTML_TAGS
: Pass
= Pass
{
13 name
: "check-invalid-html-tags",
14 run
: check_invalid_html_tags
,
15 description
: "detects invalid HTML tags in doc comments",
18 struct InvalidHtmlTagsLinter
<'a
, 'tcx
> {
19 cx
: &'a DocContext
<'tcx
>,
22 impl<'a
, 'tcx
> InvalidHtmlTagsLinter
<'a
, 'tcx
> {
23 fn new(cx
: &'a DocContext
<'tcx
>) -> Self {
24 InvalidHtmlTagsLinter { cx }
28 crate fn check_invalid_html_tags(krate
: Crate
, cx
: &DocContext
<'_
>) -> Crate
{
29 if !cx
.tcx
.sess
.is_nightly_build() {
32 let mut coll
= InvalidHtmlTagsLinter
::new(cx
);
34 coll
.fold_crate(krate
)
38 const ALLOWED_UNCLOSED
: &[&str] = &[
39 "area", "base", "br", "col", "embed", "hr", "img", "input", "keygen", "link", "meta", "param",
40 "source", "track", "wbr",
44 tags
: &mut Vec
<(String
, Range
<usize>)>,
47 f
: &impl Fn(&str, &Range
<usize>),
49 let tag_name_low
= tag_name
.to_lowercase();
50 if let Some(pos
) = tags
.iter().rposition(|(t
, _
)| t
.to_lowercase() == tag_name_low
) {
51 // If the tag is nested inside a "<script>" or a "<style>" tag, no warning should
53 let should_not_warn
= tags
.iter().take(pos
+ 1).any(|(at
, _
)| {
54 let at
= at
.to_lowercase();
55 at
== "script" || at
== "style"
57 for (last_tag_name
, last_tag_span
) in tags
.drain(pos
+ 1..) {
61 let last_tag_name_low
= last_tag_name
.to_lowercase();
62 if ALLOWED_UNCLOSED
.iter().any(|&at
| at
== &last_tag_name_low
) {
65 // `tags` is used as a queue, meaning that everything after `pos` is included inside it.
66 // So `<h2><h3></h2>` will look like `["h2", "h3"]`. So when closing `h2`, we will still
67 // have `h3`, meaning the tag wasn't closed as it should have.
68 f(&format
!("unclosed HTML tag `{}`", last_tag_name
), &last_tag_span
);
70 // Remove the `tag_name` that was originally closed
73 // It can happen for example in this case: `<h2></script></h2>` (the `h2` tag isn't required
74 // but it helps for the visualization).
75 f(&format
!("unopened HTML tag `{}`", tag_name
), &range
);
80 tags
: &mut Vec
<(String
, Range
<usize>)>,
84 iter
: &mut Peekable
<CharIndices
<'_
>>,
85 f
: &impl Fn(&str, &Range
<usize>),
87 let mut tag_name
= String
::new();
88 let mut is_closing
= false;
89 let mut prev_pos
= start_pos
;
92 let (pos
, c
) = match iter
.peek() {
93 Some((pos
, c
)) => (*pos
, *c
),
94 // In case we reached the of the doc comment, we want to check that it's an
95 // unclosed HTML tag. For example "/// <h3".
96 None
=> (prev_pos
, '
\0'
),
99 // Checking if this is a closing tag (like `</a>` for `<a>`).
100 if c
== '
/'
&& tag_name
.is_empty() {
102 } else if c
.is_ascii_alphanumeric() {
105 if !tag_name
.is_empty() {
106 let mut r
= Range { start: range.start + start_pos, end: range.start + pos }
;
108 // In case we have a tag without attribute, we can consider the span to
109 // refer to it fully.
113 // In case we have "</div >" or even "</div >".
115 if !c
.is_whitespace() {
116 // It seems like it's not a valid HTML tag.
119 let mut found
= false;
120 for (new_pos
, c
) in text
[pos
..].char_indices() {
121 if !c
.is_whitespace() {
123 r
.end
= range
.start
+ new_pos
+ 1;
133 drop_tag(tags
, tag_name
, r
, f
);
135 tags
.push((tag_name
, r
));
145 tags
: &mut Vec
<(String
, Range
<usize>)>,
148 is_in_comment
: &mut Option
<Range
<usize>>,
149 f
: &impl Fn(&str, &Range
<usize>),
151 let mut iter
= text
.char_indices().peekable();
153 while let Some((start_pos
, c
)) = iter
.next() {
154 if is_in_comment
.is_some() {
155 if text
[start_pos
..].starts_with("-->") {
156 *is_in_comment
= None
;
159 if text
[start_pos
..].starts_with("<!--") {
160 // We skip the "!--" part. (Once `advance_by` is stable, might be nice to use it!)
164 *is_in_comment
= Some(Range
{
165 start
: range
.start
+ start_pos
,
166 end
: range
.start
+ start_pos
+ 3,
169 extract_html_tag(tags
, text
, &range
, start_pos
, &mut iter
, f
);
175 impl<'a
, 'tcx
> DocFolder
for InvalidHtmlTagsLinter
<'a
, 'tcx
> {
176 fn fold_item(&mut self, item
: Item
) -> Option
<Item
> {
177 let hir_id
= match self.cx
.as_local_hir_id(item
.def_id
) {
178 Some(hir_id
) => hir_id
,
180 // If non-local, no need to check anything.
181 return Some(self.fold_item_recur(item
));
184 let dox
= item
.attrs
.collapsed_doc_value().unwrap_or_default();
187 let report_diag
= |msg
: &str, range
: &Range
<usize>| {
188 let sp
= match super::source_span_for_markdown_range(cx
, &dox
, range
, &item
.attrs
) {
190 None
=> span_of_attrs(&item
.attrs
).unwrap_or(item
.source
.span()),
192 cx
.tcx
.struct_span_lint_hir(lint
::builtin
::INVALID_HTML_TAGS
, hir_id
, sp
, |lint
| {
193 lint
.build(msg
).emit()
197 let mut tags
= Vec
::new();
198 let mut is_in_comment
= None
;
199 let mut in_code_block
= false;
201 let p
= Parser
::new_ext(&dox
, opts()).into_offset_iter();
203 for (event
, range
) in p
{
205 Event
::Start(Tag
::CodeBlock(_
)) => in_code_block
= true,
206 Event
::Html(text
) | Event
::Text(text
) if !in_code_block
=> {
207 extract_tags(&mut tags
, &text
, range
, &mut is_in_comment
, &report_diag
)
209 Event
::End(Tag
::CodeBlock(_
)) => in_code_block
= false,
214 for (tag
, range
) in tags
.iter().filter(|(t
, _
)| {
215 let t
= t
.to_lowercase();
216 ALLOWED_UNCLOSED
.iter().find(|&&at
| at
== t
).is_none()
218 report_diag(&format
!("unclosed HTML tag `{}`", tag
), range
);
221 if let Some(range
) = is_in_comment
{
222 report_diag("Unclosed HTML comment", &range
);
226 Some(self.fold_item_recur(item
))