diff options
| author | Guillaume Gomez <guillaume1.gomez@gmail.com> | 2020-09-23 20:25:56 +0200 |
|---|---|---|
| committer | Guillaume Gomez <guillaume1.gomez@gmail.com> | 2020-10-03 14:16:23 +0200 |
| commit | e6027a42e109fef10f4fc27ebede50d1b3d203f0 (patch) | |
| tree | c4b918f40dcd4d015415d445b53f176cc60a0f0a /src/librustdoc | |
| parent | 782013564efc06ef02614ba35a4e67dee4fcb8e7 (diff) | |
| download | rust-e6027a42e109fef10f4fc27ebede50d1b3d203f0.tar.gz rust-e6027a42e109fef10f4fc27ebede50d1b3d203f0.zip | |
Add `unclosed_html_tags` lint
Diffstat (limited to 'src/librustdoc')
| -rw-r--r-- | src/librustdoc/core.rs | 2 | ||||
| -rw-r--r-- | src/librustdoc/html/markdown.rs | 2 | ||||
| -rw-r--r-- | src/librustdoc/passes/html_tags.rs | 135 | ||||
| -rw-r--r-- | src/librustdoc/passes/mod.rs | 5 |
4 files changed, 143 insertions, 1 deletions
diff --git a/src/librustdoc/core.rs b/src/librustdoc/core.rs index 391859050e8..45a84c4fb30 100644 --- a/src/librustdoc/core.rs +++ b/src/librustdoc/core.rs @@ -328,6 +328,7 @@ pub fn run_core( let private_doc_tests = rustc_lint::builtin::PRIVATE_DOC_TESTS.name; let no_crate_level_docs = rustc_lint::builtin::MISSING_CRATE_LEVEL_DOCS.name; let invalid_codeblock_attributes_name = rustc_lint::builtin::INVALID_CODEBLOCK_ATTRIBUTES.name; + let invalid_html_tags = rustc_lint::builtin::INVALID_HTML_TAGS.name; let renamed_and_removed_lints = rustc_lint::builtin::RENAMED_AND_REMOVED_LINTS.name; let unknown_lints = rustc_lint::builtin::UNKNOWN_LINTS.name; @@ -340,6 +341,7 @@ pub fn run_core( private_doc_tests.to_owned(), no_crate_level_docs.to_owned(), invalid_codeblock_attributes_name.to_owned(), + invalid_html_tags.to_owned(), renamed_and_removed_lints.to_owned(), unknown_lints.to_owned(), ]; diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index 6c0f1c02ac6..2fd06d7e573 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -43,7 +43,7 @@ use pulldown_cmark::{html, BrokenLink, CodeBlockKind, CowStr, Event, Options, Pa #[cfg(test)] mod tests; -fn opts() -> Options { +pub(crate) fn opts() -> Options { Options::ENABLE_TABLES | Options::ENABLE_FOOTNOTES | Options::ENABLE_STRIKETHROUGH } diff --git a/src/librustdoc/passes/html_tags.rs b/src/librustdoc/passes/html_tags.rs new file mode 100644 index 00000000000..b177eaeeb73 --- /dev/null +++ b/src/librustdoc/passes/html_tags.rs @@ -0,0 +1,135 @@ +use super::{span_of_attrs, Pass}; +use crate::clean::*; +use crate::core::DocContext; +use crate::fold::DocFolder; +use crate::html::markdown::opts; +use pulldown_cmark::{Event, Parser}; +use rustc_hir::hir_id::HirId; +use rustc_session::lint; +use rustc_span::Span; + +pub const CHECK_INVALID_HTML_TAGS: Pass = Pass { + name: "check-invalid-html-tags", + run: check_invalid_html_tags, + description: "detects invalid HTML tags in doc comments", +}; + +struct InvalidHtmlTagsLinter<'a, 'tcx> { + cx: &'a DocContext<'tcx>, +} + +impl<'a, 'tcx> InvalidHtmlTagsLinter<'a, 'tcx> { + fn new(cx: &'a DocContext<'tcx>) -> Self { + InvalidHtmlTagsLinter { cx } + } +} + +pub fn check_invalid_html_tags(krate: Crate, cx: &DocContext<'_>) -> Crate { + let mut coll = InvalidHtmlTagsLinter::new(cx); + + coll.fold_crate(krate) +} + +const ALLOWED_UNCLOSED: &[&str] = &[ + "area", "base", "br", "col", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", + "source", "track", "wbr", +]; + +fn drop_tag( + cx: &DocContext<'_>, + tags: &mut Vec<String>, + tag_name: String, + hir_id: HirId, + sp: Span, +) { + if let Some(pos) = tags.iter().position(|t| *t == tag_name) { + for _ in pos + 1..tags.len() { + if ALLOWED_UNCLOSED.iter().find(|&at| at == &tags[pos + 1]).is_some() { + continue; + } + // `tags` is used as a queue, meaning that everything after `pos` is included inside it. + // So `<h2><h3></h2>` will look like `["h2", "h3"]`. So when closing `h2`, we will still + // have `h3`, meaning the tag wasn't closed as it should have. + cx.tcx.struct_span_lint_hir(lint::builtin::INVALID_HTML_TAGS, hir_id, sp, |lint| { + lint.build(&format!("unclosed HTML tag `{}`", tags[pos + 1])).emit() + }); + tags.remove(pos + 1); + } + tags.remove(pos); + } else { + // It can happen for example in this case: `<h2></script></h2>` (the `h2` tag isn't required + // but it helps for the visualization). + cx.tcx.struct_span_lint_hir(lint::builtin::INVALID_HTML_TAGS, hir_id, sp, |lint| { + lint.build(&format!("unopened HTML tag `{}`", tag_name)).emit() + }); + } +} + +fn extract_tag(cx: &DocContext<'_>, tags: &mut Vec<String>, text: &str, hir_id: HirId, sp: Span) { + let mut iter = text.chars().peekable(); + + while let Some(c) = iter.next() { + if c == '<' { + let mut tag_name = String::new(); + let mut is_closing = false; + while let Some(&c) = iter.peek() { + // </tag> + if c == '/' && tag_name.is_empty() { + is_closing = true; + } else if c.is_ascii_alphanumeric() && !c.is_ascii_uppercase() { + tag_name.push(c); + } else { + break; + } + iter.next(); + } + if tag_name.is_empty() { + // Not an HTML tag presumably... + continue; + } + if is_closing { + drop_tag(cx, tags, tag_name, hir_id, sp); + } else { + tags.push(tag_name); + } + } + } +} + +impl<'a, 'tcx> DocFolder for InvalidHtmlTagsLinter<'a, 'tcx> { + fn fold_item(&mut self, item: Item) -> Option<Item> { + let hir_id = match self.cx.as_local_hir_id(item.def_id) { + Some(hir_id) => hir_id, + None => { + // If non-local, no need to check anything. + return None; + } + }; + let dox = item.attrs.collapsed_doc_value().unwrap_or_default(); + if !dox.is_empty() { + let sp = span_of_attrs(&item.attrs).unwrap_or(item.source.span()); + let mut tags = Vec::new(); + + let p = Parser::new_ext(&dox, opts()); + + for event in p { + match event { + Event::Html(text) => extract_tag(self.cx, &mut tags, &text, hir_id, sp), + _ => {} + } + } + + for tag in tags.iter().filter(|t| ALLOWED_UNCLOSED.iter().find(|at| at == t).is_none()) + { + self.cx.tcx.struct_span_lint_hir( + lint::builtin::INVALID_HTML_TAGS, + hir_id, + sp, + |lint| lint.build(&format!("unclosed HTML tag `{}`", tag)).emit(), + ); + } + } + + self.fold_item_recur(item) + } +} diff --git a/src/librustdoc/passes/mod.rs b/src/librustdoc/passes/mod.rs index 75a65966667..3819aaee560 100644 --- a/src/librustdoc/passes/mod.rs +++ b/src/librustdoc/passes/mod.rs @@ -45,6 +45,9 @@ pub use self::check_code_block_syntax::CHECK_CODE_BLOCK_SYNTAX; mod calculate_doc_coverage; pub use self::calculate_doc_coverage::CALCULATE_DOC_COVERAGE; +mod html_tags; +pub use self::html_tags::CHECK_INVALID_HTML_TAGS; + /// A single pass over the cleaned documentation. /// /// Runs in the compiler context, so it has access to types and traits and the like. @@ -87,6 +90,7 @@ pub const PASSES: &[Pass] = &[ CHECK_CODE_BLOCK_SYNTAX, COLLECT_TRAIT_IMPLS, CALCULATE_DOC_COVERAGE, + CHECK_INVALID_HTML_TAGS, ]; /// The list of passes run by default. @@ -101,6 +105,7 @@ pub const DEFAULT_PASSES: &[ConditionalPass] = &[ ConditionalPass::always(COLLECT_INTRA_DOC_LINKS), ConditionalPass::always(CHECK_CODE_BLOCK_SYNTAX), ConditionalPass::always(PROPAGATE_DOC_CFG), + ConditionalPass::always(CHECK_INVALID_HTML_TAGS), ]; /// The list of default passes run when `--doc-coverage` is passed to rustdoc. |
