From 60159a31ff246d55ed8e96bb617bbd6c1283e2eb Mon Sep 17 00:00:00 2001 From: yossydev Date: Thu, 4 Jan 2024 00:03:10 +0900 Subject: [PATCH] feat(linter): eslint-plugin-jsx-a11y autocomplete-valid rule --- crates/oxc_linter/src/rules.rs | 2 + .../src/rules/jsx_a11y/autocomplete_valid.rs | 232 ++++++++++++++++++ .../src/snapshots/autocomplete_valid.snap | 47 ++++ 3 files changed, 281 insertions(+) create mode 100644 crates/oxc_linter/src/rules/jsx_a11y/autocomplete_valid.rs create mode 100644 crates/oxc_linter/src/snapshots/autocomplete_valid.snap diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 02f80e5d98171..c146eec98deab 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -243,6 +243,7 @@ mod jsx_a11y { pub mod aria_props; pub mod aria_role; pub mod aria_unsupported_elements; + pub mod autocomplete_valid; pub mod heading_has_content; pub mod html_has_lang; pub mod iframe_has_title; @@ -518,6 +519,7 @@ oxc_macros::declare_all_lint_rules! { jsx_a11y::aria_role, jsx_a11y::no_distracting_elements, jsx_a11y::role_support_aria_props, + jsx_a11y::autocomplete_valid, oxc::approx_constant, oxc::const_comparisons, oxc::double_comparisons, diff --git a/crates/oxc_linter/src/rules/jsx_a11y/autocomplete_valid.rs b/crates/oxc_linter/src/rules/jsx_a11y/autocomplete_valid.rs new file mode 100644 index 0000000000000..178b69ed3e2dc --- /dev/null +++ b/crates/oxc_linter/src/rules/jsx_a11y/autocomplete_valid.rs @@ -0,0 +1,232 @@ +use std::collections::{HashMap, HashSet}; + +use crate::{context::LintContext, rule::Rule, utils::has_jsx_prop_lowercase, AstNode}; +use once_cell::sync::Lazy; +use oxc_ast::{ + ast::{JSXAttributeItem, JSXAttributeValue}, + AstKind, +}; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::{self, Error}, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; +#[derive(Debug, Error, Diagnostic)] +#[error( + "eslint-plugin-jsx-a11y(autocomplete-valid): `{autocomplete}` is not a valid value for autocomplete." +)] +#[diagnostic(severity(warning), help("Change `{autocomplete}` to a valid value for autocomplete."))] +struct AutocompleteValidDiagnostic { + #[label] + pub span: Span, + pub autocomplete: String, +} + +#[derive(Debug, Default, Clone)] +pub struct AutocompleteValid; +declare_oxc_lint!( + /// ### What it does + /// Enforces that an element's autocomplete attribute must be a valid value. + /// + /// ### Why is this bad? + /// Incorrectly using the autocomplete attribute may decrease the accessibility of the website for users. + /// + /// ### Example + /// ```javascript + /// // Bad + /// + /// + /// // Good + /// + /// ``` + AutocompleteValid, + correctness +); + +static VALID_AUTOCOMPLETE_VALUES: Lazy> = Lazy::new(|| { + [ + "on", + "name", + "email", + "username", + "new-password", + "current-password", + "one-time-code", + "off", + "organization-title", + "organization", + "street-address", + "address-line1", + "address-line2", + "address-line3", + "address-level4", + "address-level3", + "address-level2", + "address-level1", + "country", + "country-name", + "postal-code", + "cc-name", + "cc-given-name", + "cc-additional-name", + "cc-family-name", + "cc-number", + "cc-exp", + "cc-exp-month", + "cc-exp-year", + "cc-csc", + "cc-type", + "transaction-currency", + "transaction-amount", + "language", + "bday", + "bday-day", + "bday-month", + "bday-year", + "sex", + "tel", + "tel-country-code", + "tel-national", + "tel-area-code", + "tel-local", + "tel-extension", + "impp", + "url", + "photo", + "webauthn", + ] + .iter() + .cloned() + .collect() +}); + +static VALID_AUTOCOMPLETE_COMBINATIONS: Lazy>> = + Lazy::new(|| { + let mut m = HashMap::new(); + m.insert( + "billing", + vec![ + "street-address", + "address-line1", + "address-line2", + "address-line3", + "address-level4", + "address-level3", + "address-level2", + "address-level1", + "country", + "country-name", + "postal-code", + ] + .into_iter() + .collect(), + ); + m.insert( + "shipping", + vec![ + "street-address", + "address-line1", + "address-line2", + "address-line3", + "address-level4", + "address-level3", + "address-level2", + "address-level1", + "country", + "country-name", + "postal-code", + ] + .into_iter() + .collect(), + ); + m + }); + +fn is_valid_autocomplete_value(value: &str) -> bool { + let parts: Vec<&str> = value.split_whitespace().collect(); + match parts.len() { + 1 => VALID_AUTOCOMPLETE_VALUES.contains(parts[0]), + 2 => { + if let Some(valid_suffixes) = VALID_AUTOCOMPLETE_COMBINATIONS.get(parts[0]) { + valid_suffixes.contains(parts[1]) + } else { + false + } + } + _ => false, + } +} + +impl Rule for AutocompleteValid { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + if let AstKind::JSXOpeningElement(jsx_el) = node.kind() { + let autocomplete_prop = match has_jsx_prop_lowercase(jsx_el, "autocomplete") { + Some(autocomplete_prop) => autocomplete_prop, + None => return, + }; + let attr = match autocomplete_prop { + JSXAttributeItem::Attribute(attr) => attr, + JSXAttributeItem::SpreadAttribute(_) => return, + }; + let autocomplete_values = match &attr.value { + Some(JSXAttributeValue::StringLiteral(autocomplete_values)) => autocomplete_values, + _ => return, + }; + let value = autocomplete_values.value.to_string(); + if !is_valid_autocomplete_value(&value) { + ctx.diagnostic(AutocompleteValidDiagnostic { + span: attr.span, + autocomplete: value.into(), + }); + } + } + } +} + +#[test] +fn test() { + use crate::rules::AutocompleteValid; + use crate::tester::Tester; + + fn config() -> serde_json::Value { + serde_json::json!([{ + "inputComponents": [ "Bar" ] + }]) + } + + fn settings() -> serde_json::Value { + serde_json::json!({ + "jsx-a11y": { + "components": { + "Input": "input", + } + } + }) + } + + let pass = vec![ + (";", None, None, None), + (";", None, None, None), + ("", None, None, None), + (";", None, None, None), + (";", None, None, None), + (";", None, None, None), + (";", None, None, None), + (";", None, None, None), + (";", None, None, None), + (";", None, None, None), + ("", None, Some(settings()), None), + ]; + + let fail = vec![ + (";", None, None, None), + (";", None, None, None), + (";", None, None, None), + (";", Some(config()), None, None), + (";", None, None, None), + (";", None, Some(settings()), None), + ]; + + Tester::new_with_settings(AutocompleteValid::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/autocomplete_valid.snap b/crates/oxc_linter/src/snapshots/autocomplete_valid.snap new file mode 100644 index 0000000000000..21503f10e3989 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/autocomplete_valid.snap @@ -0,0 +1,47 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: autocomplete_valid +--- + ⚠ eslint-plugin-jsx-a11y(autocomplete-valid): `foo` is not a valid value for autocomplete. + ╭─[autocomplete_valid.tsx:1:1] + 1 │ ; + · ────────────────── + ╰──── + help: Change `foo` to a valid value for autocomplete. + + ⚠ eslint-plugin-jsx-a11y(autocomplete-valid): `name invalid` is not a valid value for autocomplete. + ╭─[autocomplete_valid.tsx:1:1] + 1 │ ; + · ─────────────────────────── + ╰──── + help: Change `name invalid` to a valid value for autocomplete. + + ⚠ eslint-plugin-jsx-a11y(autocomplete-valid): `invalid name` is not a valid value for autocomplete. + ╭─[autocomplete_valid.tsx:1:1] + 1 │ ; + · ─────────────────────────── + ╰──── + help: Change `invalid name` to a valid value for autocomplete. + + ⚠ eslint-plugin-jsx-a11y(autocomplete-valid): `home url` is not a valid value for autocomplete. + ╭─[autocomplete_valid.tsx:1:1] + 1 │ ; + · ─────────────────────── + ╰──── + help: Change `home url` to a valid value for autocomplete. + + ⚠ eslint-plugin-jsx-a11y(autocomplete-valid): `baz` is not a valid value for autocomplete. + ╭─[autocomplete_valid.tsx:1:1] + 1 │ ; + · ────────────────── + ╰──── + help: Change `baz` to a valid value for autocomplete. + + ⚠ eslint-plugin-jsx-a11y(autocomplete-valid): `baz` is not a valid value for autocomplete. + ╭─[autocomplete_valid.tsx:1:1] + 1 │ ; + · ────────────────── + ╰──── + help: Change `baz` to a valid value for autocomplete. + +