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 std
::iter
::Peekable
;
9 use std
::str::CharIndices
;
11 crate const CHECK_INVALID_HTML_TAGS
: Pass
= Pass
{
12 name
: "check-invalid-html-tags",
13 run
: check_invalid_html_tags
,
14 description
: "detects invalid HTML tags in doc comments",
17 struct InvalidHtmlTagsLinter
<'a
, 'tcx
> {
18 cx
: &'a
mut DocContext
<'tcx
>,
21 crate fn check_invalid_html_tags(krate
: Crate
, cx
: &mut DocContext
<'_
>) -> Crate
{
22 if !cx
.tcx
.sess
.is_nightly_build() {
25 let mut coll
= InvalidHtmlTagsLinter { cx }
;
27 coll
.fold_crate(krate
)
31 const ALLOWED_UNCLOSED
: &[&str] = &[
32 "area", "base", "br", "col", "embed", "hr", "img", "input", "keygen", "link", "meta", "param",
33 "source", "track", "wbr",
37 tags
: &mut Vec
<(String
, Range
<usize>)>,
40 f
: &impl Fn(&str, &Range
<usize>),
42 let tag_name_low
= tag_name
.to_lowercase();
43 if let Some(pos
) = tags
.iter().rposition(|(t
, _
)| t
.to_lowercase() == tag_name_low
) {
44 // If the tag is nested inside a "<script>" or a "<style>" tag, no warning should
46 let should_not_warn
= tags
.iter().take(pos
+ 1).any(|(at
, _
)| {
47 let at
= at
.to_lowercase();
48 at
== "script" || at
== "style"
50 for (last_tag_name
, last_tag_span
) in tags
.drain(pos
+ 1..) {
54 let last_tag_name_low
= last_tag_name
.to_lowercase();
55 if ALLOWED_UNCLOSED
.iter().any(|&at
| at
== last_tag_name_low
) {
58 // `tags` is used as a queue, meaning that everything after `pos` is included inside it.
59 // So `<h2><h3></h2>` will look like `["h2", "h3"]`. So when closing `h2`, we will still
60 // have `h3`, meaning the tag wasn't closed as it should have.
61 f(&format
!("unclosed HTML tag `{}`", last_tag_name
), &last_tag_span
);
63 // Remove the `tag_name` that was originally closed
66 // It can happen for example in this case: `<h2></script></h2>` (the `h2` tag isn't required
67 // but it helps for the visualization).
68 f(&format
!("unopened HTML tag `{}`", tag_name
), &range
);
73 tags
: &mut Vec
<(String
, Range
<usize>)>,
77 iter
: &mut Peekable
<CharIndices
<'_
>>,
78 f
: &impl Fn(&str, &Range
<usize>),
80 let mut tag_name
= String
::new();
81 let mut is_closing
= false;
82 let mut prev_pos
= start_pos
;
85 let (pos
, c
) = match iter
.peek() {
86 Some((pos
, c
)) => (*pos
, *c
),
87 // In case we reached the of the doc comment, we want to check that it's an
88 // unclosed HTML tag. For example "/// <h3".
89 None
=> (prev_pos
, '
\0'
),
92 // Checking if this is a closing tag (like `</a>` for `<a>`).
93 if c
== '
/'
&& tag_name
.is_empty() {
95 } else if c
.is_ascii_alphanumeric() {
98 if !tag_name
.is_empty() {
99 let mut r
= Range { start: range.start + start_pos, end: range.start + pos }
;
101 // In case we have a tag without attribute, we can consider the span to
102 // refer to it fully.
106 // In case we have "</div >" or even "</div >".
108 if !c
.is_whitespace() {
109 // It seems like it's not a valid HTML tag.
112 let mut found
= false;
113 for (new_pos
, c
) in text
[pos
..].char_indices() {
114 if !c
.is_whitespace() {
116 r
.end
= range
.start
+ new_pos
+ 1;
126 drop_tag(tags
, tag_name
, r
, f
);
128 tags
.push((tag_name
, r
));
138 tags
: &mut Vec
<(String
, Range
<usize>)>,
141 is_in_comment
: &mut Option
<Range
<usize>>,
142 f
: &impl Fn(&str, &Range
<usize>),
144 let mut iter
= text
.char_indices().peekable();
146 while let Some((start_pos
, c
)) = iter
.next() {
147 if is_in_comment
.is_some() {
148 if text
[start_pos
..].starts_with("-->") {
149 *is_in_comment
= None
;
152 if text
[start_pos
..].starts_with("<!--") {
153 // We skip the "!--" part. (Once `advance_by` is stable, might be nice to use it!)
157 *is_in_comment
= Some(Range
{
158 start
: range
.start
+ start_pos
,
159 end
: range
.start
+ start_pos
+ 3,
162 extract_html_tag(tags
, text
, &range
, start_pos
, &mut iter
, f
);
168 impl<'a
, 'tcx
> DocFolder
for InvalidHtmlTagsLinter
<'a
, 'tcx
> {
169 fn fold_item(&mut self, item
: Item
) -> Option
<Item
> {
170 let tcx
= self.cx
.tcx
;
171 let hir_id
= match DocContext
::as_local_hir_id(tcx
, item
.def_id
) {
172 Some(hir_id
) => hir_id
,
174 // If non-local, no need to check anything.
175 return Some(self.fold_item_recur(item
));
178 let dox
= item
.attrs
.collapsed_doc_value().unwrap_or_default();
180 let report_diag
= |msg
: &str, range
: &Range
<usize>| {
181 let sp
= match super::source_span_for_markdown_range(tcx
, &dox
, range
, &item
.attrs
)
184 None
=> span_of_attrs(&item
.attrs
).unwrap_or(item
.source
.span()),
186 tcx
.struct_span_lint_hir(crate::lint
::INVALID_HTML_TAGS
, hir_id
, sp
, |lint
| {
187 lint
.build(msg
).emit()
191 let mut tags
= Vec
::new();
192 let mut is_in_comment
= None
;
193 let mut in_code_block
= false;
195 let p
= Parser
::new_ext(&dox
, opts()).into_offset_iter();
197 for (event
, range
) in p
{
199 Event
::Start(Tag
::CodeBlock(_
)) => in_code_block
= true,
200 Event
::Html(text
) | Event
::Text(text
) if !in_code_block
=> {
201 extract_tags(&mut tags
, &text
, range
, &mut is_in_comment
, &report_diag
)
203 Event
::End(Tag
::CodeBlock(_
)) => in_code_block
= false,
208 for (tag
, range
) in tags
.iter().filter(|(t
, _
)| {
209 let t
= t
.to_lowercase();
210 ALLOWED_UNCLOSED
.iter().find(|&&at
| at
== t
).is_none()
212 report_diag(&format
!("unclosed HTML tag `{}`", tag
), range
);
215 if let Some(range
) = is_in_comment
{
216 report_diag("Unclosed HTML comment", &range
);
220 Some(self.fold_item_recur(item
))