Skip to content

Commit c434f01

Browse files
sterliakovematipicodyc3
authored
feat(linter): add excludedComponents option to useUniqueElementIds (#6723)
Co-authored-by: Emanuele Stoppa <my.burning@gmail.com> Co-authored-by: ematipico <602478+ematipico@users.noreply.github.com> Co-authored-by: dyc3 <1808807+dyc3@users.noreply.github.com>
1 parent 51bf430 commit c434f01

File tree

12 files changed

+331
-11
lines changed

12 files changed

+331
-11
lines changed

.changeset/vast-tables-dance.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
`useUniqueElementIds` now has an `excludedComponents` option to support elements using `id` prop for reasons not related to DOM element id. Fixed [#6722](https://github.com/biomejs/biome/issues/6722).

crates/biome_js_analyze/src/lint/nursery/use_unique_element_ids.rs

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use biome_js_syntax::{
66
AnyJsExpression, AnyJsxAttributeValue, JsCallExpression, JsPropertyObjectMember, JsxAttribute,
77
jsx_ext::AnyJsxElement,
88
};
9-
use biome_rowan::{AstNode, declare_node_union};
9+
use biome_rowan::{AstNode, TokenText, declare_node_union};
1010
use biome_rule_options::use_unique_element_ids::UseUniqueElementIdsOptions;
1111

1212
use crate::react::{ReactApiCall, ReactCreateElementCall};
@@ -44,6 +44,37 @@ declare_lint_rule! {
4444
/// React.createElement("div", { id });
4545
/// ```
4646
///
47+
/// ## Options
48+
///
49+
/// The following option is available
50+
///
51+
/// ### `excludedComponents`
52+
///
53+
/// List of unqualified component names to ignore.
54+
/// Use it to list components expecting an `id` attribute that does not represent
55+
/// a DOM element ID.
56+
///
57+
/// **Default**: empty list.
58+
///
59+
/// ```json,options
60+
/// {
61+
/// "options": {
62+
/// "excludedComponents": [
63+
/// "FormattedMessage"
64+
/// ]
65+
/// }
66+
/// }
67+
/// ```
68+
///
69+
/// ```jsx,use_options
70+
/// <FormattedMessage id="static" />
71+
/// ```
72+
///
73+
/// ```jsx,use_options
74+
/// <Library.FormattedMessage id="static" />
75+
/// ```
76+
///
77+
///
4778
pub UseUniqueElementIds {
4879
version: "2.0.0",
4980
name: "useUniqueElementIds",
@@ -63,16 +94,37 @@ declare_node_union! {
6394
}
6495

6596
impl UseUniqueElementIdsQuery {
66-
fn find_id_attribute(&self, model: &SemanticModel) -> Option<IdProp> {
97+
fn create_element_call(&self, model: &SemanticModel) -> Option<ReactCreateElementCall> {
6798
match self {
68-
Self::AnyJsxElement(jsx) => jsx.find_attribute_by_name("id").map(IdProp::from),
6999
Self::JsCallExpression(expression) => {
70-
let react_create_element =
71-
ReactCreateElementCall::from_call_expression(expression, model)?;
72-
react_create_element
73-
.find_prop_by_name("id")
74-
.map(IdProp::from)
100+
ReactCreateElementCall::from_call_expression(expression, model)
75101
}
102+
&Self::AnyJsxElement(_) => None,
103+
}
104+
}
105+
106+
fn element_name(&self, model: &SemanticModel) -> Option<TokenText> {
107+
match self {
108+
Self::AnyJsxElement(jsx) => jsx
109+
.name_value_token()
110+
.ok()
111+
.map(|tok| tok.token_text_trimmed()),
112+
Self::JsCallExpression(_) => self
113+
.create_element_call(model)?
114+
.element_type
115+
.as_any_js_expression()?
116+
.get_callee_member_name()
117+
.map(|tok| tok.token_text_trimmed()),
118+
}
119+
}
120+
121+
fn find_id_attribute(&self, model: &SemanticModel) -> Option<IdProp> {
122+
match self {
123+
Self::AnyJsxElement(jsx) => jsx.find_attribute_by_name("id").map(IdProp::from),
124+
Self::JsCallExpression(_) => self
125+
.create_element_call(model)?
126+
.find_prop_by_name("id")
127+
.map(IdProp::from),
76128
}
77129
}
78130
}
@@ -86,6 +138,13 @@ impl Rule for UseUniqueElementIds {
86138
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
87139
let node = ctx.query();
88140
let model = ctx.model();
141+
let options = ctx.options();
142+
if node
143+
.element_name(model)
144+
.is_some_and(|name| options.excluded_components.contains(name.text()))
145+
{
146+
return None;
147+
}
89148
let id_attribute = node.find_id_attribute(model)?;
90149

91150
match id_attribute {

crates/biome_js_analyze/src/react/components.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ impl ReactComponentInfo {
198198

199199
/// Creates a `ReactComponentInfo` from an expression.
200200
/// It is not guaranteed that the expression is a React component,
201-
/// but if any reqiuirements are not met, it will return `None`.
201+
/// but if any requirements are not met, it will return `None`.
202202
/// Never returns a name, can only return a name hint.
203203
pub(crate) fn from_expression(syntax: &SyntaxNode<JsLanguage>) -> Option<Self> {
204204
let any_expression = AnyJsExpression::cast_ref(syntax)?;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// allowed
2+
function WithJsx() {
3+
return <FormattedMessage id="abc"></FormattedMessage>
4+
}
5+
6+
function WithJsxSelfClosing() {
7+
return <FormattedMessage id="abc"/>
8+
}
9+
10+
function WithJsxNamespaced() {
11+
return <Library.FormattedMessage id="abc"/>
12+
}
13+
14+
function WithCreateElement() {
15+
return React.createElement(FormattedMessage, {id: "abc"})
16+
}
17+
18+
function WithCreateElement2() {
19+
return React.createElement(Library.FormattedMessage, {id: "abc"})
20+
}
21+
22+
// denied
23+
function WithJsxOther() {
24+
return <OtherFormattedMessage id="abc"></OtherFormattedMessage>
25+
}
26+
27+
function WithCreateElementOther() {
28+
return React.createElement(OtherFormattedMessage, {id: "abc"})
29+
}
30+
31+
function WithCreateElementWronglyQuoted() {
32+
return React.createElement("FormattedMessage", {id: "abc"})
33+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: allowlist.jsx
4+
---
5+
# Input
6+
```jsx
7+
// allowed
8+
function WithJsx() {
9+
return <FormattedMessage id="abc"></FormattedMessage>
10+
}
11+
12+
function WithJsxSelfClosing() {
13+
return <FormattedMessage id="abc"/>
14+
}
15+
16+
function WithJsxNamespaced() {
17+
return <Library.FormattedMessage id="abc"/>
18+
}
19+
20+
function WithCreateElement() {
21+
return React.createElement(FormattedMessage, {id: "abc"})
22+
}
23+
24+
function WithCreateElement2() {
25+
return React.createElement(Library.FormattedMessage, {id: "abc"})
26+
}
27+
28+
// denied
29+
function WithJsxOther() {
30+
return <OtherFormattedMessage id="abc"></OtherFormattedMessage>
31+
}
32+
33+
function WithCreateElementOther() {
34+
return React.createElement(OtherFormattedMessage, {id: "abc"})
35+
}
36+
37+
function WithCreateElementWronglyQuoted() {
38+
return React.createElement("FormattedMessage", {id: "abc"})
39+
}
40+
41+
```
42+
43+
# Diagnostics
44+
```
45+
allowlist.jsx:24:9 lint/nursery/useUniqueElementIds ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
46+
47+
× id attribute should not be a static string literal. Generate unique IDs using useId().
48+
49+
22 │ // denied
50+
23 │ function WithJsxOther() {
51+
> 24return <OtherFormattedMessage id="abc"></OtherFormattedMessage>
52+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
53+
25}
54+
26 │
55+
56+
i In React, if you hardcode IDs and use the component multiple times, it can lead to duplicate IDs in the DOM. Instead, generate unique IDs using useId().
57+
58+
59+
```
60+
61+
```
62+
allowlist.jsx:28:9 lint/nursery/useUniqueElementIds ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
63+
64+
× id attribute should not be a static string literal. Generate unique IDs using useId().
65+
66+
27 │ function WithCreateElementOther() {
67+
> 28return React.createElement(OtherFormattedMessage, {id: "abc"})
68+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
69+
29}
70+
30 │
71+
72+
i In React, if you hardcode IDs and use the component multiple times, it can lead to duplicate IDs in the DOM. Instead, generate unique IDs using useId().
73+
74+
75+
```
76+
77+
```
78+
allowlist.jsx:32:9 lint/nursery/useUniqueElementIds ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
79+
80+
× id attribute should not be a static string literal. Generate unique IDs using useId().
81+
82+
31 │ function WithCreateElementWronglyQuoted() {
83+
> 32return React.createElement("FormattedMessage", {id: "abc"})
84+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
85+
33}
86+
34 │
87+
88+
i In React, if you hardcode IDs and use the component multiple times, it can lead to duplicate IDs in the DOM. Instead, generate unique IDs using useId().
89+
90+
91+
```
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json",
3+
"linter": {
4+
"rules": {
5+
"nursery": {
6+
"useUniqueElementIds": {
7+
"level": "error",
8+
"options": {
9+
"excludedComponents": ["FormattedMessage"]
10+
}
11+
}
12+
}
13+
}
14+
}
15+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
function WithJsxNamespaced() {
2+
return <Library.FormattedMessage id="abc"/>
3+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: badAllowlist.jsx
4+
---
5+
# Input
6+
```jsx
7+
function WithJsxNamespaced() {
8+
return <Library.FormattedMessage id="abc"/>
9+
}
10+
11+
```
12+
13+
# Diagnostics
14+
```
15+
badAllowlist.options:8:32 deserialize ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
16+
17+
× 'excludedComponents' does not accept values with dots.
18+
19+
6 │ "useUniqueElementIds": {
20+
7"level": "error",
21+
> 8"options": {
22+
│ ^
23+
> 9 │ "excludedComponents": ["Library.FormattedMessage"]
24+
> 10 │ }
25+
^
26+
11}
27+
12 │ }
28+
29+
30+
```
31+
32+
```
33+
badAllowlist.jsx:2:9 lint/nursery/useUniqueElementIds ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
34+
35+
× id attribute should not be a static string literal. Generate unique IDs using useId().
36+
37+
1 │ function WithJsxNamespaced() {
38+
> 2return <Library.FormattedMessage id="abc"/>
39+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
40+
3}
41+
4 │
42+
43+
i In React, if you hardcode IDs and use the component multiple times, it can lead to duplicate IDs in the DOM. Instead, generate unique IDs using useId().
44+
45+
46+
```
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json",
3+
"linter": {
4+
"rules": {
5+
"nursery": {
6+
"useUniqueElementIds": {
7+
"level": "error",
8+
"options": {
9+
"excludedComponents": ["Library.FormattedMessage"]
10+
}
11+
}
12+
}
13+
}
14+
}
15+
}
Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,45 @@
1+
use biome_console::markup;
2+
use biome_deserialize::{
3+
DeserializableValidator, DeserializationContext, DeserializationDiagnostic, TextRange,
4+
};
15
use biome_deserialize_macros::Deserializable;
6+
use rustc_hash::FxHashSet;
27
use serde::{Deserialize, Serialize};
8+
39
#[derive(Default, Clone, Debug, Deserialize, Deserializable, Eq, PartialEq, Serialize)]
410
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
511
#[serde(rename_all = "camelCase", deny_unknown_fields, default)]
6-
pub struct UseUniqueElementIdsOptions {}
12+
#[deserializable(with_validator)]
13+
pub struct UseUniqueElementIdsOptions {
14+
/// Component names that accept an `id` prop that does not translate
15+
/// to a DOM element id.
16+
pub excluded_components: FxHashSet<Box<str>>,
17+
}
18+
19+
impl DeserializableValidator for UseUniqueElementIdsOptions {
20+
fn validate(
21+
&mut self,
22+
ctx: &mut impl DeserializationContext,
23+
_name: &str,
24+
range: TextRange,
25+
) -> bool {
26+
for name in &self.excluded_components {
27+
let msg = if name.is_empty() {
28+
"empty values"
29+
} else if name.contains('.') {
30+
"values with dots"
31+
} else {
32+
continue;
33+
};
34+
ctx.report(
35+
DeserializationDiagnostic::new(markup!(
36+
<Emphasis>"'excludedComponents'"</Emphasis>" does not accept "{msg}"."
37+
))
38+
.with_range(range),
39+
);
40+
return false;
41+
}
42+
43+
true
44+
}
45+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy