Skip to content

Commit 0d9b60a

Browse files
authored
feat(lint/useValidAutocomplete): add rule (#3143)
1 parent 177d2c7 commit 0d9b60a

File tree

15 files changed

+583
-0
lines changed

15 files changed

+583
-0
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b
3333

3434
### Linter
3535

36+
37+
#### New features
38+
39+
- Add [nursery/useValidAutocomplete](https://biomejs.dev/linter/rules/use-valid-autocomplete/). Contributed by @unvalley
40+
3641
#### Bug fixes
3742

3843
- [useImportExtensions](https://biomejs.dev/linter/rules/use-import-extensions/) now suggests a correct fix for `import '.'` and `import './.'`. Contributed by @minht11

crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_configuration/src/linter/rules.rs

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_diagnostics_categories/src/categories.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ define_categories! {
164164
"lint/nursery/useThrowNewError": "https://biomejs.dev/linter/rules/use-throw-new-error",
165165
"lint/nursery/useThrowOnlyError": "https://biomejs.dev/linter/rules/use-throw-only-error",
166166
"lint/nursery/useTopLevelRegex": "https://biomejs.dev/linter/rules/use-top-level-regex",
167+
"lint/nursery/useValidAutocomplete": "https://biomejs.dev/linter/rules/use-valid-autocomplete",
167168
"lint/performance/noAccumulatingSpread": "https://biomejs.dev/linter/rules/no-accumulating-spread",
168169
"lint/performance/noBarrelFile": "https://biomejs.dev/linter/rules/no-barrel-file",
169170
"lint/performance/noDelete": "https://biomejs.dev/linter/rules/no-delete",

crates/biome_js_analyze/src/lint/nursery.rs

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
use biome_analyze::{context::RuleContext, declare_rule, Rule, RuleDiagnostic, RuleSource};
2+
use biome_console::markup;
3+
use biome_deserialize_macros::Deserializable;
4+
use biome_js_syntax::{JsxOpeningElement, JsxSelfClosingElement};
5+
use biome_rowan::{declare_node_union, AstNode, TextRange};
6+
use serde::{Deserialize, Serialize};
7+
8+
use crate::services::aria::Aria;
9+
10+
declare_rule! {
11+
/// Use valid values for the `autocomplete` attribute on `input` elements.
12+
///
13+
/// The HTML autocomplete attribute only accepts specific predefined values.
14+
/// This allows for more detailed purpose definitions compared to the `type` attribute.
15+
/// Using these predefined values, user agents and assistive technologies can present input purposes to users in different ways.
16+
///
17+
/// ## Examples
18+
///
19+
/// ### Invalid
20+
///
21+
/// ```jsx,expect_diagnostic
22+
/// <input type="text" autocomplete="incorrect" />
23+
/// ```
24+
///
25+
/// ### Valid
26+
///
27+
/// ```jsx
28+
/// <>
29+
/// <input type="text" autocomplete="name" />
30+
/// <MyInput autocomplete="incorrect" />
31+
/// </>
32+
/// ```
33+
///
34+
/// ## Options
35+
///
36+
/// ```json
37+
/// {
38+
/// "//": "...",
39+
/// "options": {
40+
/// "inputComponents": ["MyInput"]
41+
/// }
42+
/// }
43+
/// ```
44+
///
45+
/// ## Accessibility guidelines
46+
/// - [WCAG 1.3.5](https://www.w3.org/WAI/WCAG21/Understanding/identify-input-purpose)
47+
///
48+
/// ### Resources
49+
/// - [HTML Living Standard autofill](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill)
50+
/// - [HTML attribute: autocomplete - HTML: HyperText Markup Language | MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete)
51+
///
52+
pub UseValidAutocomplete {
53+
version: "next",
54+
name: "useValidAutocomplete",
55+
language: "js",
56+
sources: &[RuleSource::EslintJsxA11y("autocomplete-valid")],
57+
recommended: false,
58+
}
59+
}
60+
61+
declare_node_union! {
62+
pub UseValidAutocompleteQuery = JsxSelfClosingElement | JsxOpeningElement
63+
}
64+
65+
// Sorted for binary search
66+
const VALID_AUTOCOMPLETE_VALUES: [&str; 55] = [
67+
"additional-name",
68+
"address-level1",
69+
"address-level2",
70+
"address-level3",
71+
"address-level4",
72+
"address-line1",
73+
"address-line2",
74+
"address-line3",
75+
"bday",
76+
"bday-day",
77+
"bday-month",
78+
"bday-year",
79+
"cc-additional-name",
80+
"cc-csc",
81+
"cc-exp",
82+
"cc-exp-month",
83+
"cc-exp-year",
84+
"cc-family-name",
85+
"cc-given-name",
86+
"cc-name",
87+
"cc-number",
88+
"cc-type",
89+
"country",
90+
"country-name",
91+
"current-password",
92+
"email",
93+
"family-name",
94+
"given-name",
95+
"honorific-prefix",
96+
"honorific-suffix",
97+
"impp",
98+
"language",
99+
"name",
100+
"new-password",
101+
"nickname",
102+
"off",
103+
"on",
104+
"one-time-code",
105+
"organization",
106+
"organization-title",
107+
"photo",
108+
"postal-code",
109+
"sex",
110+
"street-address",
111+
"tel",
112+
"tel-area-code",
113+
"tel-country-code",
114+
"tel-extension",
115+
"tel-local",
116+
"tel-national",
117+
"transaction-amount",
118+
"transaction-currency",
119+
"url",
120+
"username",
121+
"webauthn",
122+
];
123+
124+
// Sorted for binary search
125+
const BILLING_AND_SHIPPING_ADDRESS: &[&str; 11] = &[
126+
"address-level1",
127+
"address-level2",
128+
"address-level3",
129+
"address-level4",
130+
"address-line1",
131+
"address-line2",
132+
"address-line3",
133+
"country",
134+
"country-name",
135+
"postal-code",
136+
"street-address",
137+
];
138+
139+
#[derive(Clone, Debug, Default, Deserialize, Deserializable, Eq, PartialEq, Serialize)]
140+
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
141+
#[serde(rename_all = "camelCase", deny_unknown_fields)]
142+
pub struct UseValidAutocompleteOptions {
143+
/// `input` like custom components that should be checked.
144+
pub input_components: Vec<String>,
145+
}
146+
147+
impl Rule for UseValidAutocomplete {
148+
type Query = Aria<UseValidAutocompleteQuery>;
149+
type State = TextRange;
150+
type Signals = Option<Self::State>;
151+
type Options = Box<UseValidAutocompleteOptions>;
152+
153+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
154+
let options = ctx.options();
155+
let input_components = &options.input_components;
156+
match ctx.query() {
157+
UseValidAutocompleteQuery::JsxOpeningElement(elem) => {
158+
let elem_name = elem.name().ok()?.name_value_token()?;
159+
let elem_name = elem_name.text_trimmed();
160+
if !(elem_name == "input" || input_components.contains(&elem_name.to_string())) {
161+
return None;
162+
}
163+
let attributes = elem.attributes();
164+
let autocomplete = attributes.find_by_name("autocomplete").ok()??;
165+
let _initializer = autocomplete.initializer()?;
166+
let extract_attrs = ctx.extract_attributes(&attributes)?;
167+
let autocomplete_values = extract_attrs.get("autocomplete")?;
168+
if is_valid_autocomplete(autocomplete_values)? {
169+
return None;
170+
}
171+
Some(autocomplete.range())
172+
}
173+
UseValidAutocompleteQuery::JsxSelfClosingElement(elem) => {
174+
let elem_name = elem.name().ok()?.name_value_token()?;
175+
let elem_name = elem_name.text_trimmed();
176+
if !(elem_name == "input" || input_components.contains(&elem_name.to_string())) {
177+
return None;
178+
}
179+
let attributes = elem.attributes();
180+
let autocomplete = attributes.find_by_name("autocomplete").ok()??;
181+
let _initializer = autocomplete.initializer()?;
182+
let extract_attrs = ctx.extract_attributes(&attributes)?;
183+
let autocomplete_values = extract_attrs.get("autocomplete")?;
184+
if is_valid_autocomplete(autocomplete_values)? {
185+
return None;
186+
}
187+
Some(autocomplete.range())
188+
}
189+
}
190+
}
191+
192+
fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
193+
Some(
194+
RuleDiagnostic::new(
195+
rule_category!(),
196+
state,
197+
markup! {
198+
"Use valid values for the "<Emphasis>"autocomplete"</Emphasis>" attribute."
199+
},
200+
)
201+
.note(markup! {
202+
"The autocomplete attribute only accepts a certain number of specific fixed values."
203+
}).note(markup!{
204+
"Follow the links for more information,
205+
"<Hyperlink href="https://www.w3.org/WAI/WCAG21/Understanding/identify-input-purpose">"WCAG 1.3.5"</Hyperlink>"
206+
"<Hyperlink href="https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill">"HTML Living Standard autofill"</Hyperlink>"
207+
"<Hyperlink href="https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete">"HTML attribute: autocomplete - HTML: HyperText Markup Language | MDN"</Hyperlink>""
208+
})
209+
)
210+
}
211+
}
212+
213+
/// Checks if the autocomplete attribute values are valid
214+
fn is_valid_autocomplete(autocomplete_values: &[String]) -> Option<bool> {
215+
let is_valid = match autocomplete_values.len() {
216+
0 => true,
217+
1 => {
218+
let first = autocomplete_values.first()?.as_str();
219+
first.is_empty()
220+
| first.starts_with("section-")
221+
| VALID_AUTOCOMPLETE_VALUES.binary_search(&first).is_ok()
222+
}
223+
_ => {
224+
let first = autocomplete_values.first()?.as_str();
225+
let second = autocomplete_values.get(1)?.as_str();
226+
first.starts_with("section-")
227+
|| ["billing", "shipping"].contains(&first)
228+
&& (BILLING_AND_SHIPPING_ADDRESS.binary_search(&second).is_ok()
229+
|| VALID_AUTOCOMPLETE_VALUES.binary_search(&second).is_ok())
230+
|| autocomplete_values.iter().all(|val| {
231+
VALID_AUTOCOMPLETE_VALUES
232+
.binary_search(&val.as_str())
233+
.is_ok()
234+
})
235+
}
236+
};
237+
Some(is_valid)
238+
}
239+
240+
#[test]
241+
fn test_order() {
242+
for items in VALID_AUTOCOMPLETE_VALUES.windows(2) {
243+
assert!(items[0] < items[1], "{} < {}", items[0], items[1]);
244+
}
245+
for items in BILLING_AND_SHIPPING_ADDRESS.windows(2) {
246+
assert!(items[0] < items[1], "{} < {}", items[0], items[1]);
247+
}
248+
}

crates/biome_js_analyze/src/options.rs

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<>
2+
<input type="text" autocomplete="foo" />
3+
<input type="text" autocomplete="name invalid" />
4+
<input type="text" autocomplete="invalid name" />
5+
<input type="text" autocomplete="home url" />
6+
<Bar autocomplete="baz"></Bar>
7+
<Input type="text" autocomplete="baz" />
8+
</>

0 commit comments

Comments
 (0)