diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs
index 02f80e5d981710..c146eec98deab3 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 00000000000000..178b69ed3e2dc6
--- /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 00000000000000..21503f10e39896
--- /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.
+
+