about summary refs log tree commit diff
path: root/src/librustdoc
diff options
context:
space:
mode:
authorGuillaume Gomez <guillaume1.gomez@gmail.com>2020-09-23 20:25:56 +0200
committerGuillaume Gomez <guillaume1.gomez@gmail.com>2020-10-03 14:16:23 +0200
commite6027a42e109fef10f4fc27ebede50d1b3d203f0 (patch)
treec4b918f40dcd4d015415d445b53f176cc60a0f0a /src/librustdoc
parent782013564efc06ef02614ba35a4e67dee4fcb8e7 (diff)
downloadrust-e6027a42e109fef10f4fc27ebede50d1b3d203f0.tar.gz
rust-e6027a42e109fef10f4fc27ebede50d1b3d203f0.zip
Add `unclosed_html_tags` lint
Diffstat (limited to 'src/librustdoc')
-rw-r--r--src/librustdoc/core.rs2
-rw-r--r--src/librustdoc/html/markdown.rs2
-rw-r--r--src/librustdoc/passes/html_tags.rs135
-rw-r--r--src/librustdoc/passes/mod.rs5
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.