Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(linter): eslint-plugin-jsx-a11y autocomplete-valid #1901

Merged
merged 2 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
228 changes: 228 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,228 @@
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(|| {
camc314 marked this conversation as resolved.
Show resolved Hide resolved
[
"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()
.copied()
.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 => VALID_AUTOCOMPLETE_COMBINATIONS
.get(parts[0])
.map_or(false, |valid_suffixes| valid_suffixes.contains(parts[1])),
_ => 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,
});
}
}
}
}

#[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.