Skip to content

Commit

Permalink
feat(linter): eslint-plugin-jsx-a11y autocomplete-valid rule
Browse files Browse the repository at this point in the history
  • Loading branch information
yossydev committed Jan 10, 2024
1 parent b5f4f1e commit 60159a3
Show file tree
Hide file tree
Showing 3 changed files with 281 additions and 0 deletions.
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
232 changes: 232 additions & 0 deletions crates/oxc_linter/src/rules/jsx_a11y/autocomplete_valid.rs
Original file line number Diff line number Diff line change
@@ -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
/// <input autocomplete="invalid-value" />
///
/// // Good
/// <input autocomplete="name" />
/// ```
AutocompleteValid,
correctness
);

static VALID_AUTOCOMPLETE_VALUES: Lazy<HashSet<&'static str>> = 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<HashMap<&'static str, HashSet<&'static str>>> =
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![
("<input type='text' />;", None, None, None),
("<input type='text' autocomplete='name' />;", None, None, None),
("<input type='text' autocomplete='off' />", None, None, None),
("<input type='text' autocomplete='on' />;", None, None, None),
("<input type='text' autocomplete='shipping street-address' />;", None, None, None),
("<input type='text' autocomplete />;", None, None, None),
("<input type='text' autocomplete={autocompl} />;", None, None, None),
("<input type='text' autocomplete={autocompl || 'name'} />;", None, None, None),
("<input type='text' autocomplete={autocompl || 'foo'} />;", None, None, None),
("<input type={isEmail ? 'email' : 'text'} autocomplete='off' />;", None, None, None),
("<Input type='text' autocomplete='name' />", None, Some(settings()), None),
];

let fail = vec![
("<input type='text' autocomplete='foo' />;", None, None, None),
("<input type='text' autocomplete='name invalid' />;", None, None, None),
("<input type='text' autocomplete='invalid name' />;", None, None, None),
("<input type='text' autocomplete='home url' />;", Some(config()), None, None),
("<Bar autocomplete='baz'></Bar>;", None, None, None),
("<Input type='text' autocomplete='baz' />;", None, Some(settings()), None),
];

Tester::new_with_settings(AutocompleteValid::NAME, pass, fail).test_and_snapshot();
}
47 changes: 47 additions & 0 deletions crates/oxc_linter/src/snapshots/autocomplete_valid.snap
Original file line number Diff line number Diff line change
@@ -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<input type='text' autocomplete='foo' />;
· ──────────────────
╰────
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<input type='text' autocomplete='name invalid' />;
· ───────────────────────────
╰────
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<input type='text' autocomplete='invalid name' />;
· ───────────────────────────
╰────
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<input type='text' autocomplete='home url' />;
· ───────────────────────
╰────
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<Bar autocomplete='baz'></Bar>;
· ──────────────────
╰────
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<Input type='text' autocomplete='baz' />;
· ──────────────────
╰────
help: Change `baz` to a valid value for autocomplete.


0 comments on commit 60159a3

Please sign in to comment.