Add unclosed_html_tags
lint
This commit is contained in:
parent
782013564e
commit
e6027a42e1
compiler
src
librustdoc
test/rustdoc-ui
@ -63,8 +63,8 @@ use rustc_middle::ty::query::Providers;
|
||||
use rustc_middle::ty::TyCtxt;
|
||||
use rustc_session::lint::builtin::{
|
||||
BARE_TRAIT_OBJECTS, BROKEN_INTRA_DOC_LINKS, ELIDED_LIFETIMES_IN_PATHS,
|
||||
EXPLICIT_OUTLIVES_REQUIREMENTS, INVALID_CODEBLOCK_ATTRIBUTES, MISSING_DOC_CODE_EXAMPLES,
|
||||
PRIVATE_DOC_TESTS,
|
||||
EXPLICIT_OUTLIVES_REQUIREMENTS, INVALID_CODEBLOCK_ATTRIBUTES, INVALID_HTML_TAGS,
|
||||
MISSING_DOC_CODE_EXAMPLES, PRIVATE_DOC_TESTS,
|
||||
};
|
||||
use rustc_span::symbol::{Ident, Symbol};
|
||||
use rustc_span::Span;
|
||||
@ -308,7 +308,8 @@ fn register_builtins(store: &mut LintStore, no_interleave_lints: bool) {
|
||||
PRIVATE_INTRA_DOC_LINKS,
|
||||
INVALID_CODEBLOCK_ATTRIBUTES,
|
||||
MISSING_DOC_CODE_EXAMPLES,
|
||||
PRIVATE_DOC_TESTS
|
||||
PRIVATE_DOC_TESTS,
|
||||
INVALID_HTML_TAGS
|
||||
);
|
||||
|
||||
// Register renamed and removed lints.
|
||||
|
@ -1881,6 +1881,16 @@ declare_lint! {
|
||||
"detects code samples in docs of private items not documented by rustdoc"
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// The `invalid_html_tags` lint detects invalid HTML tags. This is a
|
||||
/// `rustdoc` only lint, see the documentation in the [rustdoc book].
|
||||
///
|
||||
/// [rustdoc book]: ../../../rustdoc/lints.html#invalid_html_tags
|
||||
pub INVALID_HTML_TAGS,
|
||||
Warn,
|
||||
"detects invalid HTML tags in doc comments"
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// The `where_clauses_object_safety` lint detects for [object safety] of
|
||||
/// [where clauses].
|
||||
@ -2699,6 +2709,7 @@ declare_lint_pass! {
|
||||
INVALID_CODEBLOCK_ATTRIBUTES,
|
||||
MISSING_CRATE_LEVEL_DOCS,
|
||||
MISSING_DOC_CODE_EXAMPLES,
|
||||
INVALID_HTML_TAGS,
|
||||
PRIVATE_DOC_TESTS,
|
||||
WHERE_CLAUSES_OBJECT_SAFETY,
|
||||
PROC_MACRO_DERIVE_RESOLUTION_FALLBACK,
|
||||
|
@ -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(),
|
||||
];
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
135
src/librustdoc/passes/html_tags.rs
Normal file
135
src/librustdoc/passes/html_tags.rs
Normal file
@ -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)
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -1,3 +1,4 @@
|
||||
#![allow(unclosed_html_tags)]
|
||||
#![deny(broken_intra_doc_links)]
|
||||
//~^ NOTE lint level is defined
|
||||
|
||||
|
@ -1,35 +1,40 @@
|
||||
error: unresolved link to `path::to::nonexistent::module`
|
||||
--> $DIR/intra-link-errors.rs:7:6
|
||||
--> $DIR/intra-link-errors.rs:8:6
|
||||
|
|
||||
LL | /// [path::to::nonexistent::module]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the module `intra_link_errors` contains no item named `path`
|
||||
|
|
||||
note: the lint level is defined here
|
||||
--> $DIR/intra-link-errors.rs:1:9
|
||||
--> $DIR/intra-link-errors.rs:2:9
|
||||
|
|
||||
LL | #![deny(broken_intra_doc_links)]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: unresolved link to `path::to::nonexistent::macro`
|
||||
--> $DIR/intra-link-errors.rs:11:6
|
||||
--> $DIR/intra-link-errors.rs:12:6
|
||||
|
|
||||
LL | /// [path::to::nonexistent::macro!]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the module `intra_link_errors` contains no item named `path`
|
||||
|
||||
error: unresolved link to `path::to::nonexistent::type`
|
||||
--> $DIR/intra-link-errors.rs:15:6
|
||||
--> $DIR/intra-link-errors.rs:16:6
|
||||
|
|
||||
LL | /// [type@path::to::nonexistent::type]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the module `intra_link_errors` contains no item named `path`
|
||||
|
||||
error: unresolved link to `std::io::not::here`
|
||||
--> $DIR/intra-link-errors.rs:19:6
|
||||
--> $DIR/intra-link-errors.rs:20:6
|
||||
|
|
||||
LL | /// [std::io::not::here]
|
||||
| ^^^^^^^^^^^^^^^^^^ the module `io` contains no item named `not`
|
||||
|
||||
<<<<<<< HEAD
|
||||
error: unresolved link to `std::io::not::here`
|
||||
--> $DIR/intra-link-errors.rs:23:6
|
||||
=======
|
||||
error: unresolved link to `std::io::Error::x`
|
||||
--> $DIR/intra-link-errors.rs:24:6
|
||||
>>>>>>> Add `unclosed_html_tags` lint
|
||||
|
|
||||
LL | /// [type@std::io::not::here]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ the module `io` contains no item named `not`
|
||||
@ -41,13 +46,21 @@ LL | /// [std::io::Error::x]
|
||||
| ^^^^^^^^^^^^^^^^^ the struct `Error` has no field or associated item named `x`
|
||||
|
||||
error: unresolved link to `std::io::ErrorKind::x`
|
||||
<<<<<<< HEAD
|
||||
--> $DIR/intra-link-errors.rs:31:6
|
||||
=======
|
||||
--> $DIR/intra-link-errors.rs:28:6
|
||||
>>>>>>> Add `unclosed_html_tags` lint
|
||||
|
|
||||
LL | /// [std::io::ErrorKind::x]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ the enum `ErrorKind` has no variant or associated item named `x`
|
||||
|
||||
error: unresolved link to `f::A`
|
||||
<<<<<<< HEAD
|
||||
--> $DIR/intra-link-errors.rs:35:6
|
||||
=======
|
||||
--> $DIR/intra-link-errors.rs:32:6
|
||||
>>>>>>> Add `unclosed_html_tags` lint
|
||||
|
|
||||
LL | /// [f::A]
|
||||
| ^^^^ `f` is a function, not a module or type, and cannot have associated items
|
||||
@ -59,25 +72,41 @@ LL | /// [f::A!]
|
||||
| ^^^^^ `f` is a function, not a module or type, and cannot have associated items
|
||||
|
||||
error: unresolved link to `S::A`
|
||||
<<<<<<< HEAD
|
||||
--> $DIR/intra-link-errors.rs:43:6
|
||||
=======
|
||||
--> $DIR/intra-link-errors.rs:36:6
|
||||
>>>>>>> Add `unclosed_html_tags` lint
|
||||
|
|
||||
LL | /// [S::A]
|
||||
| ^^^^ the struct `S` has no field or associated item named `A`
|
||||
|
||||
error: unresolved link to `S::fmt`
|
||||
<<<<<<< HEAD
|
||||
--> $DIR/intra-link-errors.rs:47:6
|
||||
=======
|
||||
--> $DIR/intra-link-errors.rs:40:6
|
||||
>>>>>>> Add `unclosed_html_tags` lint
|
||||
|
|
||||
LL | /// [S::fmt]
|
||||
| ^^^^^^ the struct `S` has no field or associated item named `fmt`
|
||||
|
||||
error: unresolved link to `E::D`
|
||||
<<<<<<< HEAD
|
||||
--> $DIR/intra-link-errors.rs:51:6
|
||||
=======
|
||||
--> $DIR/intra-link-errors.rs:44:6
|
||||
>>>>>>> Add `unclosed_html_tags` lint
|
||||
|
|
||||
LL | /// [E::D]
|
||||
| ^^^^ the enum `E` has no variant or associated item named `D`
|
||||
|
||||
error: unresolved link to `u8::not_found`
|
||||
<<<<<<< HEAD
|
||||
--> $DIR/intra-link-errors.rs:55:6
|
||||
=======
|
||||
--> $DIR/intra-link-errors.rs:48:6
|
||||
>>>>>>> Add `unclosed_html_tags` lint
|
||||
|
|
||||
LL | /// [u8::not_found]
|
||||
| ^^^^^^^^^^^^^ the builtin type `u8` has no associated item named `not_found`
|
||||
@ -98,7 +127,11 @@ LL | /// [type@Vec::into_iter]
|
||||
| help: to link to the associated function, add parentheses: `Vec::into_iter()`
|
||||
|
||||
error: unresolved link to `S`
|
||||
<<<<<<< HEAD
|
||||
--> $DIR/intra-link-errors.rs:68:6
|
||||
=======
|
||||
--> $DIR/intra-link-errors.rs:52:6
|
||||
>>>>>>> Add `unclosed_html_tags` lint
|
||||
|
|
||||
LL | /// [S!]
|
||||
| ^^
|
||||
@ -107,7 +140,11 @@ LL | /// [S!]
|
||||
| help: to link to the struct, prefix with `struct@`: `struct@S`
|
||||
|
||||
error: unresolved link to `T::g`
|
||||
<<<<<<< HEAD
|
||||
--> $DIR/intra-link-errors.rs:86:6
|
||||
=======
|
||||
--> $DIR/intra-link-errors.rs:70:6
|
||||
>>>>>>> Add `unclosed_html_tags` lint
|
||||
|
|
||||
LL | /// [type@T::g]
|
||||
| ^^^^^^^^^
|
||||
@ -116,13 +153,21 @@ LL | /// [type@T::g]
|
||||
| help: to link to the associated function, add parentheses: `T::g()`
|
||||
|
||||
error: unresolved link to `T::h`
|
||||
<<<<<<< HEAD
|
||||
--> $DIR/intra-link-errors.rs:91:6
|
||||
=======
|
||||
--> $DIR/intra-link-errors.rs:75:6
|
||||
>>>>>>> Add `unclosed_html_tags` lint
|
||||
|
|
||||
LL | /// [T::h!]
|
||||
| ^^^^^ the trait `T` has no macro named `h`
|
||||
|
||||
error: unresolved link to `S::h`
|
||||
<<<<<<< HEAD
|
||||
--> $DIR/intra-link-errors.rs:78:6
|
||||
=======
|
||||
--> $DIR/intra-link-errors.rs:62:6
|
||||
>>>>>>> Add `unclosed_html_tags` lint
|
||||
|
|
||||
LL | /// [type@S::h]
|
||||
| ^^^^^^^^^
|
||||
@ -131,7 +176,11 @@ LL | /// [type@S::h]
|
||||
| help: to link to the associated function, add parentheses: `S::h()`
|
||||
|
||||
error: unresolved link to `m`
|
||||
<<<<<<< HEAD
|
||||
--> $DIR/intra-link-errors.rs:98:6
|
||||
=======
|
||||
--> $DIR/intra-link-errors.rs:82:6
|
||||
>>>>>>> Add `unclosed_html_tags` lint
|
||||
|
|
||||
LL | /// [m()]
|
||||
| ^^^
|
||||
|
Loading…
x
Reference in New Issue
Block a user