use crate::context::LintContext;
use crate::diagnostic::{LintDiagnostic, Severity};
use crate::rule::{Rule, RuleCategory, RuleMeta};
use vize_carton::FxHashMap;
use vize_carton::String;
use vize_carton::ToCompactString;
use vize_relief::ast::{ElementNode, PropNode, RootNode, SourceLocation, TemplateChildNode};
static META: RuleMeta = RuleMeta {
name: "html/id-duplication",
description: "Disallow duplicate element IDs",
category: RuleCategory::HtmlConformance,
fixable: false,
default_severity: Severity::Error,
};
#[derive(Default)]
pub struct IdDuplication;
struct IdEntry {
value: String,
loc: LocInfo,
}
#[derive(Clone)]
struct LocInfo {
start: u32,
end: u32,
}
impl Rule for IdDuplication {
fn meta(&self) -> &'static RuleMeta {
&META
}
fn run_on_template<'a>(&self, ctx: &mut LintContext<'a>, root: &RootNode<'a>) {
let mut ids: Vec<IdEntry> = Vec::new();
collect_static_ids(&root.children, &mut ids);
let mut seen: FxHashMap<&str, &LocInfo> = FxHashMap::default();
for entry in &ids {
if let Some(first_loc) = seen.get(entry.value.as_str()) {
let message = ctx.t_fmt(
"html/id-duplication.message",
&[("id", entry.value.as_str())],
);
let help = ctx.t("html/id-duplication.help");
let diag =
LintDiagnostic::error(META.name, message, entry.loc.start, entry.loc.end)
.with_help(help.into_owned())
.with_label(
"first defined here".to_compact_string(),
first_loc.start,
first_loc.end,
);
ctx.report(diag);
} else {
seen.insert(&entry.value, &entry.loc);
}
}
}
}
fn collect_static_ids<'a>(children: &[TemplateChildNode<'a>], ids: &mut Vec<IdEntry>) {
for child in children {
match child {
TemplateChildNode::Element(el) => {
collect_element_id(el, ids);
collect_static_ids(&el.children, ids);
}
TemplateChildNode::If(if_node) => {
for branch in if_node.branches.iter() {
collect_static_ids(&branch.children, ids);
}
}
TemplateChildNode::For(for_node) => {
collect_static_ids(&for_node.children, ids);
}
_ => {}
}
}
}
fn collect_element_id(element: &ElementNode, ids: &mut Vec<IdEntry>) {
for prop in &element.props {
if let PropNode::Attribute(attr) = prop {
if attr.name == "id" {
if let Some(value) = &attr.value {
ids.push(IdEntry {
value: value.content.to_compact_string(),
loc: loc_info(&attr.loc),
});
}
}
}
}
}
fn loc_info(loc: &SourceLocation) -> LocInfo {
LocInfo {
start: loc.start.offset,
end: loc.end.offset,
}
}
#[cfg(test)]
mod tests {
use super::IdDuplication;
use crate::linter::Linter;
use crate::rule::RuleRegistry;
fn create_linter() -> Linter {
let mut registry = RuleRegistry::new();
registry.register(Box::new(IdDuplication));
Linter::with_registry(registry)
}
#[test]
fn test_valid_unique_ids() {
let linter = create_linter();
let result = linter.lint_template(r#"<div id="a">A</div><div id="b">B</div>"#, "test.vue");
assert_eq!(result.error_count, 0);
}
#[test]
fn test_valid_no_ids() {
let linter = create_linter();
let result = linter.lint_template(r#"<div>A</div><div>B</div>"#, "test.vue");
assert_eq!(result.error_count, 0);
}
#[test]
fn test_valid_dynamic_ids() {
let linter = create_linter();
let result = linter.lint_template(
r#"<div :id="id1">A</div><div :id="id2">B</div>"#,
"test.vue",
);
assert_eq!(result.error_count, 0);
}
#[test]
fn test_invalid_duplicate_ids() {
let linter = create_linter();
let result = linter.lint_template(
r#"<div id="content">A</div><div id="content">B</div>"#,
"test.vue",
);
assert_eq!(result.error_count, 1);
}
#[test]
fn test_invalid_triple_duplicate() {
let linter = create_linter();
let result = linter.lint_template(
r#"<div id="x">A</div><div id="x">B</div><div id="x">C</div>"#,
"test.vue",
);
assert_eq!(result.error_count, 2);
}
#[test]
fn test_invalid_nested_duplicate() {
let linter = create_linter();
let result = linter.lint_template(
r#"<div id="foo"><span id="foo">text</span></div>"#,
"test.vue",
);
assert_eq!(result.error_count, 1);
}
}