Skip to content

Commit b537918

Browse files
authored
feat(js_biome_analyze): implement noDuplicatedSpreadProps (#8116)
1 parent 91484d1 commit b537918

File tree

15 files changed

+525
-103
lines changed

15 files changed

+525
-103
lines changed

.changeset/clean-swans-act.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added the nursery rule [`noDuplicatedSpreadProps`](https://biomejs.dev/linter/rules/no-duplicated-spread-props/). Disallow JSX prop spreading the same identifier multiple times.
6+
7+
**Invalid:**
8+
9+
```jsx
10+
<div {...props} something="else" {...props} />
11+
```

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

Lines changed: 12 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/analyzer/linter/rules.rs

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

crates/biome_configuration/src/generated/domain_selector.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.

crates/biome_diagnostics_categories/src/categories.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ define_categories! {
184184
"lint/nursery/noParametersOnlyUsedInRecursion": "https://biomejs.dev/linter/rules/no-parameters-only-used-in-recursion",
185185
"lint/nursery/noReactForwardRef": "https://biomejs.dev/linter/rules/no-react-forward-ref",
186186
"lint/nursery/noShadow": "https://biomejs.dev/linter/rules/no-shadow",
187+
"lint/nursery/noDuplicatedSpreadProps": "https://biomejs.dev/linter/rules/no-duplicated-spread-props",
187188
"lint/nursery/noSyncScripts": "https://biomejs.dev/linter/rules/no-sync-scripts",
188189
"lint/nursery/noTernary": "https://biomejs.dev/linter/rules/no-ternary",
189190
"lint/nursery/noUnknownAttribute": "https://biomejs.dev/linter/rules/no-unknown-attribute",

crates/biome_js_analyze/src/lint/nursery.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use biome_analyze::declare_lint_group;
66
pub mod no_continue;
77
pub mod no_deprecated_imports;
8+
pub mod no_duplicated_spread_props;
89
pub mod no_empty_source;
910
pub mod no_equals_to_null;
1011
pub mod no_floating_promises;
@@ -43,4 +44,4 @@ pub mod use_sorted_classes;
4344
pub mod use_spread;
4445
pub mod use_vue_define_macros_order;
4546
pub mod use_vue_multi_word_component_names;
46-
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_empty_source :: NoEmptySource , self :: no_equals_to_null :: NoEqualsToNull , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_leaked_render :: NoLeakedRender , self :: no_misused_promises :: NoMisusedPromises , self :: no_multi_str :: NoMultiStr , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_ternary :: NoTernary , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
47+
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_duplicated_spread_props :: NoDuplicatedSpreadProps , self :: no_empty_source :: NoEmptySource , self :: no_equals_to_null :: NoEqualsToNull , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_leaked_render :: NoLeakedRender , self :: no_misused_promises :: NoMisusedPromises , self :: no_multi_str :: NoMultiStr , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_ternary :: NoTernary , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use biome_analyze::{
2+
Ast, Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule,
3+
};
4+
use biome_console::markup;
5+
use biome_js_syntax::AnyJsxAttribute;
6+
use biome_js_syntax::{JsxAttributeList, JsxOpeningElement, JsxSelfClosingElement};
7+
use biome_rowan::{AstNode, declare_node_union};
8+
use biome_rule_options::no_duplicated_spread_props::NoDuplicatedSpreadPropsOptions;
9+
use std::collections::HashSet;
10+
11+
declare_lint_rule! {
12+
/// Disallow JSX prop spreading the same identifier multiple times.
13+
///
14+
/// Enforces that any unique expression is only spread once.
15+
/// Generally spreading the same expression twice is an indicator of a mistake since any attribute between the spreads may be overridden when the intent was not to.
16+
/// Even when that is not the case this will lead to unnecessary computations being performed.
17+
///
18+
/// ## Examples
19+
///
20+
/// ### Invalid
21+
///
22+
/// ```jsx,expect_diagnostic
23+
/// <div {...props} something="else" {...props} />
24+
/// ```
25+
///
26+
/// ### Valid
27+
///
28+
/// ```jsx
29+
/// <div something="else" {...props} />
30+
/// ```
31+
///
32+
pub NoDuplicatedSpreadProps {
33+
version: "next",
34+
name: "noDuplicatedSpreadProps",
35+
language: "js",
36+
recommended: false,
37+
sources: &[RuleSource::EslintReact("jsx-props-no-spread-multi").same()],
38+
domains: &[RuleDomain::React, RuleDomain::Solid],
39+
}
40+
}
41+
42+
impl Rule for NoDuplicatedSpreadProps {
43+
type Query = Ast<NoDuplicatedSpreadPropsQuery>;
44+
type State = String;
45+
type Signals = Option<Self::State>;
46+
type Options = NoDuplicatedSpreadPropsOptions;
47+
48+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
49+
let binding = ctx.query();
50+
51+
match binding {
52+
NoDuplicatedSpreadPropsQuery::JsxOpeningElement(node) => {
53+
let attributes = node.attributes();
54+
validate_attributes(&attributes)
55+
}
56+
NoDuplicatedSpreadPropsQuery::JsxSelfClosingElement(node) => {
57+
let attributes = node.attributes();
58+
validate_attributes(&attributes)
59+
}
60+
}
61+
}
62+
63+
fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
64+
let node = ctx.query();
65+
Some(
66+
RuleDiagnostic::new(
67+
rule_category!(),
68+
node.range(),
69+
markup! {
70+
"The expression "<Emphasis>{state}</Emphasis>" has spread more than once."
71+
},
72+
)
73+
.note(markup! {
74+
"Spreading an expression more than once will lead to unnecessary computations being performed. Reduce spreads of this expression down to 1."
75+
}),
76+
)
77+
}
78+
}
79+
80+
declare_node_union! {
81+
pub NoDuplicatedSpreadPropsQuery =
82+
JsxOpeningElement
83+
| JsxSelfClosingElement
84+
}
85+
86+
fn validate_attributes(list: &JsxAttributeList) -> Option<String> {
87+
let mut seen_spreads = HashSet::new();
88+
89+
for attribute in list {
90+
if let AnyJsxAttribute::JsxSpreadAttribute(spread) = attribute
91+
&& let Some(argument) = spread.argument().ok()
92+
&& let Some(express) = argument.as_js_identifier_expression()
93+
&& let Some(name) = express.name().ok()
94+
&& let Some(value_token) = name.value_token().ok()
95+
{
96+
let text = value_token.text_trimmed().to_string();
97+
if !seen_spreads.insert(text.clone()) {
98+
return Some(text);
99+
}
100+
}
101+
}
102+
103+
None
104+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const Invalid1 = () => {
2+
return <div {...props} something="else" {...props}></div>
3+
}
4+
5+
const Invalid2 = () => {
6+
return <div {...foo.bar} {...props} {...props}></div>
7+
}
8+
9+
const Invalid3 = () => {
10+
return <div {...{}} {...props} {...props}></div>
11+
}
12+
13+
const Invalid4 = () => {
14+
return <div {...props} something="else" {...props} />
15+
}
16+
17+
const Invalid5 = () => {
18+
return <div {...foo.bar} {...props} {...props} />
19+
}
20+
21+
const Invalid6 = () => {
22+
return <div {...{}} {...props} {...props} />
23+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: invalid.jsx
4+
---
5+
# Input
6+
```jsx
7+
const Invalid1 = () => {
8+
return <div {...props} something="else" {...props}></div>
9+
}
10+
11+
const Invalid2 = () => {
12+
return <div {...foo.bar} {...props} {...props}></div>
13+
}
14+
15+
const Invalid3 = () => {
16+
return <div {...{}} {...props} {...props}></div>
17+
}
18+
19+
const Invalid4 = () => {
20+
return <div {...props} something="else" {...props} />
21+
}
22+
23+
const Invalid5 = () => {
24+
return <div {...foo.bar} {...props} {...props} />
25+
}
26+
27+
const Invalid6 = () => {
28+
return <div {...{}} {...props} {...props} />
29+
}
30+
31+
```
32+
33+
# Diagnostics
34+
```
35+
invalid.jsx:2:9 lint/nursery/noDuplicatedSpreadProps ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
36+
37+
i The expression props has spread more than once.
38+
39+
1 │ const Invalid1 = () => {
40+
> 2return <div {...props} something="else" {...props}></div>
41+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
42+
3}
43+
4 │
44+
45+
i Spreading an expression more than once will lead to unnecessary computations being performed. Reduce spreads of this expression down to 1.
46+
47+
48+
```
49+
50+
```
51+
invalid.jsx:6:9 lint/nursery/noDuplicatedSpreadProps ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
52+
53+
i The expression props has spread more than once.
54+
55+
5 │ const Invalid2 = () => {
56+
> 6return <div {...foo.bar} {...props} {...props}></div>
57+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
58+
7}
59+
8 │
60+
61+
i Spreading an expression more than once will lead to unnecessary computations being performed. Reduce spreads of this expression down to 1.
62+
63+
64+
```
65+
66+
```
67+
invalid.jsx:10:9 lint/nursery/noDuplicatedSpreadProps ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
68+
69+
i The expression props has spread more than once.
70+
71+
9 │ const Invalid3 = () => {
72+
> 10return <div {...{}} {...props} {...props}></div>
73+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
74+
11}
75+
12 │
76+
77+
i Spreading an expression more than once will lead to unnecessary computations being performed. Reduce spreads of this expression down to 1.
78+
79+
80+
```
81+
82+
```
83+
invalid.jsx:14:9 lint/nursery/noDuplicatedSpreadProps ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
84+
85+
i The expression props has spread more than once.
86+
87+
13 │ const Invalid4 = () => {
88+
> 14return <div {...props} something="else" {...props} />
89+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
90+
15}
91+
16 │
92+
93+
i Spreading an expression more than once will lead to unnecessary computations being performed. Reduce spreads of this expression down to 1.
94+
95+
96+
```
97+
98+
```
99+
invalid.jsx:18:9 lint/nursery/noDuplicatedSpreadProps ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
100+
101+
i The expression props has spread more than once.
102+
103+
17 │ const Invalid5 = () => {
104+
> 18return <div {...foo.bar} {...props} {...props} />
105+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
106+
19}
107+
20 │
108+
109+
i Spreading an expression more than once will lead to unnecessary computations being performed. Reduce spreads of this expression down to 1.
110+
111+
112+
```
113+
114+
```
115+
invalid.jsx:22:9 lint/nursery/noDuplicatedSpreadProps ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
116+
117+
i The expression props has spread more than once.
118+
119+
21 │ const Invalid6 = () => {
120+
> 22return <div {...{}} {...props} {...props} />
121+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
122+
23}
123+
24 │
124+
125+
i Spreading an expression more than once will lead to unnecessary computations being performed. Reduce spreads of this expression down to 1.
126+
127+
128+
```
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/* should not generate diagnostics */
2+
const Valid1 = () => {
3+
return <div {...props} something="else"></div>
4+
}
5+
6+
const Valid2 = () => {
7+
return <div something="else" {...props}></div>
8+
}
9+
10+
const Valid3 = () => {
11+
return <div {...props} something="else" {...otherProps}></div>
12+
}
13+
14+
const Valid4 = () => {
15+
return <div {...{}} something="else" {...otherProps}></div>
16+
}
17+
18+
const Valid5 = () => {
19+
return <div {...props} something="else" />
20+
}
21+
22+
const Valid6 = () => {
23+
return <div something="else" {...props} />
24+
}
25+
26+
const Valid7 = () => {
27+
return <div {...props} something="else" {...otherProps} />
28+
}
29+
30+
const Valid8 = () => {
31+
return <div {...{}} something="else" {...otherProps} />
32+
}

0 commit comments

Comments
 (0)