Skip to content

Commit 040af8d

Browse files
committed
feat(js_biome_analyze): implement noDuplicateSpread
1 parent 803a44a commit 040af8d

File tree

15 files changed

+355
-93
lines changed

15 files changed

+355
-93
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: 113 additions & 92 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
@@ -181,6 +181,7 @@ define_categories! {
181181
"lint/nursery/noParametersOnlyUsedInRecursion": "https://biomejs.dev/linter/rules/no-parameters-only-used-in-recursion",
182182
"lint/nursery/noReactForwardRef": "https://biomejs.dev/linter/rules/no-react-forward-ref",
183183
"lint/nursery/noShadow": "https://biomejs.dev/linter/rules/no-shadow",
184+
"lint/nursery/noDuplicatedSpreadProps": "https://biomejs.dev/linter/rules/no-duplicated-spread-props",
184185
"lint/nursery/noSyncScripts": "https://biomejs.dev/linter/rules/no-sync-scripts",
185186
"lint/nursery/noUnknownAttribute": "https://biomejs.dev/linter/rules/no-unknown-attribute",
186187
"lint/nursery/noUnnecessaryConditions": "https://biomejs.dev/linter/rules/no-unnecessary-conditions",

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_floating_promises;
1011
pub mod no_for_in;
@@ -39,4 +40,4 @@ pub mod use_sorted_classes;
3940
pub mod use_spread;
4041
pub mod use_vue_define_macros_order;
4142
pub mod use_vue_multi_word_component_names;
42-
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_empty_source :: NoEmptySource , 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_misused_promises :: NoMisusedPromises , 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_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 ,] } }
43+
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_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_misused_promises :: NoMisusedPromises , 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_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+
declare_node_union! {
43+
pub NoDuplicatedSpreadPropsQuery =
44+
JsxOpeningElement
45+
| JsxSelfClosingElement
46+
}
47+
48+
fn validate_attributes(list: &JsxAttributeList) -> Option<String> {
49+
let mut seen_spreads = HashSet::new();
50+
51+
for attribute in list {
52+
if let AnyJsxAttribute::JsxSpreadAttribute(spread) = attribute
53+
&& let Some(argument) = spread.argument().ok()
54+
{
55+
let express = argument.as_js_identifier_expression()?;
56+
let name = express.name().ok()?;
57+
let value_token = name.value_token().ok()?;
58+
let text = value_token.text_trimmed().to_string();
59+
if !seen_spreads.insert(text.clone()) {
60+
return Some(text);
61+
}
62+
}
63+
}
64+
65+
None
66+
}
67+
68+
impl Rule for NoDuplicatedSpreadProps {
69+
type Query = Ast<NoDuplicatedSpreadPropsQuery>;
70+
type State = String;
71+
type Signals = Option<Self::State>;
72+
type Options = NoDuplicatedSpreadPropsOptions;
73+
74+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
75+
let binding = ctx.query();
76+
77+
match binding {
78+
NoDuplicatedSpreadPropsQuery::JsxOpeningElement(node) => {
79+
let attributes = node.attributes();
80+
validate_attributes(&attributes)
81+
}
82+
NoDuplicatedSpreadPropsQuery::JsxSelfClosingElement(node) => {
83+
let attributes = node.attributes();
84+
validate_attributes(&attributes)
85+
}
86+
}
87+
}
88+
89+
fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
90+
let node = ctx.query();
91+
Some(
92+
RuleDiagnostic::new(
93+
rule_category!(),
94+
node.range(),
95+
markup! {
96+
"The expression "<Emphasis>{state}</Emphasis>" has spread more than once."
97+
},
98+
)
99+
.note(markup! {
100+
"Spreading an expression more than once will lead to unnecessary computations being performed. Reduce spreads of this expression down to 1."
101+
}),
102+
)
103+
}
104+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const Invalid = () => {
2+
return <div {...props} something="else" {...props} />
3+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: invalid.jsx
4+
---
5+
# Input
6+
```jsx
7+
const Invalid = () => {
8+
return <div {...props} something="else" {...props} />
9+
}
10+
11+
```
12+
13+
# Diagnostics
14+
```
15+
invalid.jsx:2:9 lint/nursery/noDuplicatedSpreadProps ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
16+
17+
i The expression props has spread more than once.
18+
19+
1 │ const Invalid = () => {
20+
> 2return <div {...props} something="else" {...props} />
21+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
22+
3}
23+
4 │
24+
25+
i Spreading an expression more than once will lead to unnecessary computations being performed. Reduce spreads of this expression down to 1.
26+
27+
28+
```
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/* should not generate diagnostics */
2+
const Valid1 = () => {
3+
return <div {...props} something="else" />
4+
}
5+
6+
const Valid2 = () => {
7+
return <div something="else" {...props} />
8+
}
9+
10+
const Valid3 = () => {
11+
return <div {...props} something="else" {...otherProps} />
12+
}

0 commit comments

Comments
 (0)