use crate::context::LintContext;
use crate::diagnostic::Severity;
use crate::rule::{Rule, RuleCategory, RuleMeta};
use vize_relief::ast::{ElementNode, PropNode, SourceLocation};
static META: RuleMeta = RuleMeta {
name: "vue/use-unique-element-ids",
description: "Enforce unique element IDs using useId() instead of static literals",
category: RuleCategory::Accessibility,
fixable: false,
default_severity: Severity::Warning,
};
const ID_REFERENCE_ATTRIBUTES: &[&str] = &[
"for", "aria-labelledby", "aria-describedby", "aria-controls", "aria-owns", "aria-activedescendant",
"aria-flowto",
"aria-details",
"aria-errormessage",
"headers", "list", "form", "popovertarget",
"anchor",
];
#[derive(Default)]
pub struct UseUniqueElementIds {
pub allow_static: bool,
}
impl UseUniqueElementIds {
#[inline]
fn is_id_reference_attr(name: &str) -> bool {
ID_REFERENCE_ATTRIBUTES.contains(&name)
}
}
impl Rule for UseUniqueElementIds {
fn meta(&self) -> &'static RuleMeta {
&META
}
fn enter_element<'a>(&self, ctx: &mut LintContext<'a>, element: &ElementNode<'a>) {
if self.allow_static {
return;
}
for prop in &element.props {
if let PropNode::Attribute(attr) = prop {
let name = attr.name.as_str();
if name == "id" {
if let Some(value) = &attr.value {
self.report_static_id(ctx, &attr.loc, value.content.as_str(), false);
}
}
else if Self::is_id_reference_attr(name) {
if let Some(value) = &attr.value {
self.report_static_id(ctx, &attr.loc, value.content.as_str(), true);
}
}
}
}
}
}
impl UseUniqueElementIds {
fn report_static_id(
&self,
ctx: &mut LintContext<'_>,
loc: &SourceLocation,
value: &str,
is_reference: bool,
) {
let has_use_id = ctx
.analysis()
.map(|a| a.bindings.contains("useId"))
.unwrap_or(false);
let message = if is_reference {
ctx.t_fmt(
"vue/use-unique-element-ids.message_reference",
&[("value", value)],
)
} else {
ctx.t_fmt("vue/use-unique-element-ids.message", &[("value", value)])
};
let help = if has_use_id {
ctx.t("vue/use-unique-element-ids.help_has_use_id")
} else {
ctx.t("vue/use-unique-element-ids.help")
};
ctx.warn_with_help(&message, loc, 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(UseUniqueElementIds::default()));
Linter::with_registry(registry)
}
#[test]
fn test_valid_dynamic_id() {
let linter = create_linter();
let result = linter.lint_template(r#"<div :id="id">content</div>"#, "test.vue");
assert_eq!(result.warning_count, 0);
}
#[test]
fn test_valid_no_id() {
let linter = create_linter();
let result = linter.lint_template(r#"<div class="test">content</div>"#, "test.vue");
assert_eq!(result.warning_count, 0);
}
#[test]
fn test_valid_dynamic_for() {
let linter = create_linter();
let result = linter.lint_template(
r#"<label :for="inputId">Name</label><input :id="inputId" />"#,
"test.vue",
);
assert_eq!(result.warning_count, 0);
}
#[test]
fn test_invalid_static_id() {
let linter = create_linter();
let result = linter.lint_template(r#"<div id="foo">content</div>"#, "test.vue");
assert_eq!(result.warning_count, 1);
}
#[test]
fn test_invalid_static_for() {
let linter = create_linter();
let result = linter.lint_template(r#"<label for="input">Name</label>"#, "test.vue");
assert_eq!(result.warning_count, 1);
}
#[test]
fn test_invalid_static_aria_labelledby() {
let linter = create_linter();
let result = linter.lint_template(
r#"<span role="checkbox" aria-labelledby="tac"></span>"#,
"test.vue",
);
assert_eq!(result.warning_count, 1);
}
#[test]
fn test_invalid_static_aria_describedby() {
let linter = create_linter();
let result = linter.lint_template(r#"<input aria-describedby="hint" />"#, "test.vue");
assert_eq!(result.warning_count, 1);
}
#[test]
fn test_invalid_multiple_static_ids() {
let linter = create_linter();
let result = linter.lint_template(
r#"<div id="foo"><label for="bar">Label</label><input id="bar" /></div>"#,
"test.vue",
);
assert_eq!(result.warning_count, 3);
}
#[test]
fn test_is_id_reference_attr() {
assert!(UseUniqueElementIds::is_id_reference_attr("for"));
assert!(UseUniqueElementIds::is_id_reference_attr("aria-labelledby"));
assert!(UseUniqueElementIds::is_id_reference_attr(
"aria-describedby"
));
assert!(!UseUniqueElementIds::is_id_reference_attr("id"));
assert!(!UseUniqueElementIds::is_id_reference_attr("class"));
}
}