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: "a11y/aria-props",
description: "Disallow invalid ARIA attributes",
category: RuleCategory::Accessibility,
fixable: false,
default_severity: Severity::Error,
};
const VALID_ARIA_ATTRIBUTES: &[&str] = &[
"aria-atomic",
"aria-busy",
"aria-controls",
"aria-current",
"aria-describedby",
"aria-description", "aria-details",
"aria-disabled",
"aria-dropeffect", "aria-errormessage",
"aria-flowto",
"aria-grabbed", "aria-haspopup",
"aria-hidden",
"aria-invalid",
"aria-keyshortcuts",
"aria-label",
"aria-labelledby",
"aria-live",
"aria-owns",
"aria-relevant",
"aria-roledescription",
"aria-autocomplete",
"aria-checked",
"aria-expanded",
"aria-level",
"aria-modal",
"aria-multiline",
"aria-multiselectable",
"aria-orientation",
"aria-placeholder",
"aria-pressed",
"aria-readonly",
"aria-required",
"aria-selected",
"aria-sort",
"aria-valuemax",
"aria-valuemin",
"aria-valuenow",
"aria-valuetext",
"aria-activedescendant",
"aria-colcount",
"aria-colindex",
"aria-colindextext", "aria-colspan",
"aria-posinset",
"aria-rowcount",
"aria-rowindex",
"aria-rowindextext", "aria-rowspan",
"aria-setsize",
"aria-braillelabel",
"aria-brailleroledescription",
];
#[derive(Default)]
pub struct AriaProps;
impl AriaProps {
#[inline]
fn is_valid_aria_attr(name: &str) -> bool {
VALID_ARIA_ATTRIBUTES.contains(&name)
}
#[inline]
fn is_aria_attr(name: &str) -> bool {
name.starts_with("aria-")
}
fn find_similar(invalid: &str) -> Option<&'static str> {
let typo_fixes: &[(&str, &str)] = &[
("aria-labeledby", "aria-labelledby"),
("aria-describeby", "aria-describedby"),
("aria-role", "role"),
("aria-labelled-by", "aria-labelledby"),
("aria-described-by", "aria-describedby"),
("aria-labelleby", "aria-labelledby"),
("aria-lable", "aria-label"),
("aria-lablledby", "aria-labelledby"),
("aria-hiiden", "aria-hidden"),
("aria-hdden", "aria-hidden"),
("aria-disbled", "aria-disabled"),
("aria-exanded", "aria-expanded"),
("aria-expandd", "aria-expanded"),
];
for (typo, fix) in typo_fixes {
if *typo == invalid {
return Some(fix);
}
}
let invalid_lower = invalid.to_ascii_lowercase();
VALID_ARIA_ATTRIBUTES
.iter()
.find(|valid| {
let valid_lower = valid.to_ascii_lowercase();
let len_diff = (invalid_lower.len() as i32 - valid_lower.len() as i32).abs();
if len_diff > 2 {
return false;
}
let matches = invalid_lower
.chars()
.zip(valid_lower.chars())
.filter(|(a, b)| a == b)
.count();
matches >= invalid_lower.len().saturating_sub(2)
})
.copied()
}
}
impl Rule for AriaProps {
fn meta(&self) -> &'static RuleMeta {
&META
}
fn enter_element<'a>(&self, ctx: &mut LintContext<'a>, element: &ElementNode<'a>) {
for prop in &element.props {
match prop {
PropNode::Attribute(attr) => {
let name = attr.name.as_str();
if Self::is_aria_attr(name) && !Self::is_valid_aria_attr(name) {
self.report_invalid_aria(ctx, name, &attr.loc);
}
}
PropNode::Directive(dir) => {
if dir.name == "bind" {
if let Some(vize_relief::ast::ExpressionNode::Simple(arg)) = &dir.arg {
let name = arg.content.as_str();
if Self::is_aria_attr(name) && !Self::is_valid_aria_attr(name) {
self.report_invalid_aria(ctx, name, &dir.loc);
}
}
}
}
}
}
}
}
impl AriaProps {
fn report_invalid_aria(
&self,
ctx: &mut LintContext<'_>,
invalid_attr: &str,
loc: &SourceLocation,
) {
let message = ctx.t_fmt("a11y/aria-props.message", &[("attr", invalid_attr)]);
if let Some(suggestion) = Self::find_similar(invalid_attr) {
let help = ctx.t_fmt(
"a11y/aria-props.help_suggestion",
&[("invalid", invalid_attr), ("valid", suggestion)],
);
ctx.error_with_help(message.clone(), loc, help);
} else {
ctx.error_with_help(message, loc, ctx.t("a11y/aria-props.help"));
}
}
}
#[cfg(test)]
mod tests {
use super::AriaProps;
use crate::linter::Linter;
use crate::rule::RuleRegistry;
fn create_linter() -> Linter {
let mut registry = RuleRegistry::new();
registry.register(Box::new(AriaProps));
Linter::with_registry(registry)
}
#[test]
fn test_valid_aria_labelledby() {
let linter = create_linter();
let result = linter.lint_template(r#"<input aria-labelledby="address" />"#, "test.vue");
assert_eq!(result.error_count, 0);
}
#[test]
fn test_valid_aria_label() {
let linter = create_linter();
let result = linter.lint_template(r#"<button aria-label="Close">X</button>"#, "test.vue");
assert_eq!(result.error_count, 0);
}
#[test]
fn test_valid_aria_hidden() {
let linter = create_linter();
let result = linter.lint_template(r#"<div aria-hidden="true"></div>"#, "test.vue");
assert_eq!(result.error_count, 0);
}
#[test]
fn test_valid_aria_expanded() {
let linter = create_linter();
let result =
linter.lint_template(r#"<button aria-expanded="false">Menu</button>"#, "test.vue");
assert_eq!(result.error_count, 0);
}
#[test]
fn test_valid_multiple_aria_attrs() {
let linter = create_linter();
let result = linter.lint_template(
r#"<div role="dialog" aria-labelledby="title" aria-describedby="desc" aria-modal="true"></div>"#,
"test.vue",
);
assert_eq!(result.error_count, 0);
}
#[test]
fn test_valid_dynamic_aria() {
let linter = create_linter();
let result = linter.lint_template(
r#"<button :aria-expanded="isOpen">Toggle</button>"#,
"test.vue",
);
assert_eq!(result.error_count, 0);
}
#[test]
fn test_invalid_aria_labeledby() {
let linter = create_linter();
let result = linter.lint_template(r#"<input aria-labeledby="address" />"#, "test.vue");
assert_eq!(result.error_count, 1);
}
#[test]
fn test_invalid_aria_describeby() {
let linter = create_linter();
let result = linter.lint_template(r#"<div aria-describeby="desc"></div>"#, "test.vue");
assert_eq!(result.error_count, 1);
}
#[test]
fn test_invalid_made_up_aria() {
let linter = create_linter();
let result = linter.lint_template(r#"<div aria-foobar="test"></div>"#, "test.vue");
assert_eq!(result.error_count, 1);
}
#[test]
fn test_invalid_dynamic_aria() {
let linter = create_linter();
let result = linter.lint_template(r#"<input :aria-labeledby="labelId" />"#, "test.vue");
assert_eq!(result.error_count, 1);
}
#[test]
fn test_find_similar_typos() {
assert_eq!(
AriaProps::find_similar("aria-labeledby"),
Some("aria-labelledby")
);
assert_eq!(
AriaProps::find_similar("aria-describeby"),
Some("aria-describedby")
);
}
#[test]
fn test_is_valid_aria_attr() {
assert!(AriaProps::is_valid_aria_attr("aria-label"));
assert!(AriaProps::is_valid_aria_attr("aria-labelledby"));
assert!(AriaProps::is_valid_aria_attr("aria-hidden"));
assert!(!AriaProps::is_valid_aria_attr("aria-labeledby"));
assert!(!AriaProps::is_valid_aria_attr("aria-foobar"));
}
}