use crate::context::LintContext;
use crate::diagnostic::Severity;
use crate::rule::{Rule, RuleCategory, RuleMeta};
use vize_relief::ast::ElementNode;
static META: RuleMeta = RuleMeta {
name: "vue/html-self-closing",
description: "Enforce self-closing style",
category: RuleCategory::StronglyRecommended,
fixable: true,
default_severity: Severity::Warning,
};
const VOID_ELEMENTS: &[&str] = &[
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source",
"track", "wbr",
];
const SVG_ELEMENTS: &[&str] = &[
"circle",
"ellipse",
"line",
"path",
"polygon",
"polyline",
"rect",
"use",
"image",
"animate",
"animateMotion",
"animateTransform",
"set",
"stop",
"symbol",
"defs",
"g",
"marker",
"mask",
"pattern",
"linearGradient",
"radialGradient",
"clipPath",
"filter",
"foreignObject",
];
const MATHML_ELEMENTS: &[&str] = &[
"math",
"mrow",
"mi",
"mn",
"mo",
"ms",
"mtext",
"mspace",
"msqrt",
"mroot",
"mfrac",
"msup",
"msub",
"msubsup",
"munder",
"mover",
"munderover",
"mtable",
"mtr",
"mtd",
];
#[derive(Default)]
pub struct HtmlSelfClosing;
impl Rule for HtmlSelfClosing {
fn meta(&self) -> &'static RuleMeta {
&META
}
fn enter_element<'a>(&self, ctx: &mut LintContext<'a>, element: &ElementNode<'a>) {
let tag = element.tag.as_str();
let is_void = VOID_ELEMENTS.contains(&tag);
let is_svg = SVG_ELEMENTS.contains(&tag);
let is_mathml = MATHML_ELEMENTS.contains(&tag);
let is_component =
tag.contains('-') || tag.chars().next().is_some_and(|c| c.is_uppercase());
let has_children = !element.children.is_empty();
let is_self_closing = element.is_self_closing;
if is_void && !is_self_closing {
ctx.warn_with_help(
ctx.t("vue/html-self-closing.void"),
&element.loc,
ctx.t("vue/html-self-closing.help"),
);
return;
}
if (is_svg || is_mathml) && !has_children && !is_self_closing {
ctx.warn_with_help(
ctx.t("vue/html-self-closing.empty"),
&element.loc,
ctx.t("vue/html-self-closing.help"),
);
return;
}
if is_component && !has_children && !is_self_closing {
ctx.warn_with_help(
ctx.t("vue/html-self-closing.component"),
&element.loc,
ctx.t("vue/html-self-closing.help"),
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::linter::Linter;
use crate::rule::RuleRegistry;
fn create_linter() -> Linter {
let mut registry = RuleRegistry::new();
registry.register(Box::new(HtmlSelfClosing));
Linter::with_registry(registry)
}
#[test]
fn test_valid_self_closing_void() {
let linter = create_linter();
let result = linter.lint_template(r#"<img />"#, "test.vue");
assert_eq!(result.warning_count, 0);
}
#[test]
fn test_invalid_void_not_self_closing() {
let linter = create_linter();
let result = linter.lint_template(r#"<img>"#, "test.vue");
assert_eq!(result.warning_count, 1);
}
#[test]
fn test_valid_component_self_closing() {
let linter = create_linter();
let result = linter.lint_template(r#"<MyComponent />"#, "test.vue");
assert_eq!(result.warning_count, 0);
}
#[test]
fn test_invalid_empty_component() {
let linter = create_linter();
let result = linter.lint_template(r#"<MyComponent></MyComponent>"#, "test.vue");
assert_eq!(result.warning_count, 1);
}
#[test]
fn test_valid_component_with_content() {
let linter = create_linter();
let result = linter.lint_template(r#"<MyComponent>content</MyComponent>"#, "test.vue");
assert_eq!(result.warning_count, 0);
}
}