Skip to content

Commit 4863080

Browse files
committed
feat(lint): implement useObjectSpread rule
1 parent f66cebd commit 4863080

File tree

13 files changed

+367
-13
lines changed

13 files changed

+367
-13
lines changed

.changeset/puny-lands-drop.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
"@biomejs/biome": minor
3+
---
4+
5+
Added the new rule [`useObjectSpread`](https://biomejs.dev/linter/rules/use-object-spread), which prefers object spread syntax over `Object.assign()` when constructing new objects.
6+
7+
**Example (Invalid): Using Object.assign with an empty object:**
8+
9+
```js
10+
Object.assign({}, foo);
11+
Object.assign({}, { foo: 'bar' });
12+
```
13+
14+
**Example (Invalid): Using Object.assign with object literal as first argument:**
15+
16+
```js
17+
Object.assign({ foo: 'bar' }, baz);
18+
Object.assign({}, baz, { foo: 'bar' });
19+
```
20+
21+
**Example (Valid): Using object spread syntax:**
22+
23+
```js
24+
({ ...foo });
25+
({ ...baz, foo: 'bar' });
26+
```
27+
28+
**Example (Valid): Modifying existing objects is allowed:**
29+
30+
```js
31+
Object.assign(foo, { bar: baz });
32+
Object.assign(foo, bar, baz);
33+
```

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

Lines changed: 11 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: 36 additions & 12 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
@@ -202,6 +202,7 @@ define_categories! {
202202
"lint/nursery/useNamedOperation": "https://biomejs.dev/linter/rules/use-named-operation",
203203
"lint/nursery/useNamingConvention": "https://biomejs.dev/linter/rules/use-naming-convention",
204204
"lint/nursery/useNumericSeparators": "https://biomejs.dev/linter/rules/use-numeric-separators",
205+
"lint/nursery/useObjectSpread": "https://biomejs.dev/linter/rules/use-object-spread",
205206
"lint/nursery/useParseIntRadix": "https://biomejs.dev/linter/rules/use-parse-int-radix",
206207
"lint/nursery/useSingleJsDocAsterisk": "https://biomejs.dev/linter/rules/use-single-js-doc-asterisk",
207208
"lint/nursery/useSortedClasses": "https://biomejs.dev/linter/rules/use-sorted-classes",

crates/biome_js_analyze/src/lint/nursery.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@ pub mod use_for_component;
3232
pub mod use_google_font_preconnect;
3333
pub mod use_iterable_callback_return;
3434
pub mod use_numeric_separators;
35+
pub mod use_object_spread;
3536
pub mod use_parse_int_radix;
3637
pub mod use_single_js_doc_asterisk;
3738
pub mod use_sorted_classes;
3839
pub mod use_symbol_description;
3940
pub mod use_unique_element_ids;
40-
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_await_in_loop :: NoAwaitInLoop , self :: no_bitwise_operators :: NoBitwiseOperators , self :: no_constant_binary_expression :: NoConstantBinaryExpression , self :: no_destructured_props :: NoDestructuredProps , self :: no_floating_promises :: NoFloatingPromises , self :: no_global_dirname_filename :: NoGlobalDirnameFilename , self :: no_import_cycles :: NoImportCycles , self :: no_nested_component_definitions :: NoNestedComponentDefinitions , self :: no_noninteractive_element_interactions :: NoNoninteractiveElementInteractions , self :: no_process_global :: NoProcessGlobal , self :: no_restricted_elements :: NoRestrictedElements , self :: no_secrets :: NoSecrets , self :: no_shadow :: NoShadow , self :: no_ts_ignore :: NoTsIgnore , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unwanted_polyfillio :: NoUnwantedPolyfillio , self :: no_useless_backref_in_regex :: NoUselessBackrefInRegex , self :: no_useless_escape_in_string :: NoUselessEscapeInString , self :: no_useless_undefined :: NoUselessUndefined , self :: use_adjacent_getter_setter :: UseAdjacentGetterSetter , self :: use_consistent_object_definition :: UseConsistentObjectDefinition , self :: use_consistent_response :: UseConsistentResponse , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_exports_last :: UseExportsLast , self :: use_for_component :: UseForComponent , self :: use_google_font_preconnect :: UseGoogleFontPreconnect , self :: use_iterable_callback_return :: UseIterableCallbackReturn , self :: use_numeric_separators :: UseNumericSeparators , self :: use_parse_int_radix :: UseParseIntRadix , self :: use_single_js_doc_asterisk :: UseSingleJsDocAsterisk , self :: use_sorted_classes :: UseSortedClasses , self :: use_symbol_description :: UseSymbolDescription , self :: use_unique_element_ids :: UseUniqueElementIds ,] } }
41+
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_await_in_loop :: NoAwaitInLoop , self :: no_bitwise_operators :: NoBitwiseOperators , self :: no_constant_binary_expression :: NoConstantBinaryExpression , self :: no_destructured_props :: NoDestructuredProps , self :: no_floating_promises :: NoFloatingPromises , self :: no_global_dirname_filename :: NoGlobalDirnameFilename , self :: no_import_cycles :: NoImportCycles , self :: no_nested_component_definitions :: NoNestedComponentDefinitions , self :: no_noninteractive_element_interactions :: NoNoninteractiveElementInteractions , self :: no_process_global :: NoProcessGlobal , self :: no_restricted_elements :: NoRestrictedElements , self :: no_secrets :: NoSecrets , self :: no_shadow :: NoShadow , self :: no_ts_ignore :: NoTsIgnore , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unwanted_polyfillio :: NoUnwantedPolyfillio , self :: no_useless_backref_in_regex :: NoUselessBackrefInRegex , self :: no_useless_escape_in_string :: NoUselessEscapeInString , self :: no_useless_undefined :: NoUselessUndefined , self :: use_adjacent_getter_setter :: UseAdjacentGetterSetter , self :: use_consistent_object_definition :: UseConsistentObjectDefinition , self :: use_consistent_response :: UseConsistentResponse , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_exports_last :: UseExportsLast , self :: use_for_component :: UseForComponent , self :: use_google_font_preconnect :: UseGoogleFontPreconnect , self :: use_iterable_callback_return :: UseIterableCallbackReturn , self :: use_numeric_separators :: UseNumericSeparators , self :: use_object_spread :: UseObjectSpread , self :: use_parse_int_radix :: UseParseIntRadix , self :: use_single_js_doc_asterisk :: UseSingleJsDocAsterisk , self :: use_sorted_classes :: UseSortedClasses , self :: use_symbol_description :: UseSymbolDescription , self :: use_unique_element_ids :: UseUniqueElementIds ,] } }
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
use crate::services::semantic::Semantic;
2+
use biome_analyze::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule};
3+
use biome_console::markup;
4+
use biome_js_syntax::{JsCallExpression, global_identifier};
5+
use biome_rowan::{AstNode, AstSeparatedList, TextRange};
6+
7+
declare_lint_rule! {
8+
/// Prefer object spread over `Object.assign()` when constructing new objects.
9+
///
10+
/// Object spread syntax is more concise, more readable, and performs better
11+
/// than `Object.assign()` when creating a new object from existing objects.
12+
/// It also has better TypeScript integration.
13+
///
14+
/// ## Examples
15+
///
16+
/// ### Invalid
17+
///
18+
/// ```js,expect_diagnostic
19+
/// Object.assign({}, foo);
20+
/// ```
21+
///
22+
/// ```js,expect_diagnostic
23+
/// Object.assign({}, { foo: 'bar' });
24+
/// ```
25+
///
26+
/// ```js,expect_diagnostic
27+
/// Object.assign({ foo: 'bar' }, baz);
28+
/// ```
29+
///
30+
/// ```js,expect_diagnostic
31+
/// Object.assign({}, baz, { foo: 'bar' });
32+
/// ```
33+
///
34+
/// ### Valid
35+
///
36+
/// ```js
37+
/// ({ ...foo });
38+
/// ```
39+
///
40+
/// ```js
41+
/// ({ ...baz, foo: 'bar' });
42+
/// ```
43+
///
44+
/// Modifying an existing object is allowed:
45+
/// ```js
46+
/// Object.assign(foo, { bar: baz });
47+
/// ```
48+
///
49+
pub UseObjectSpread {
50+
version: "2.0.0",
51+
name: "useObjectSpread",
52+
language: "js",
53+
sources: &[
54+
RuleSource::Eslint("prefer-object-spread"),
55+
],
56+
recommended: false,
57+
}
58+
}
59+
60+
impl Rule for UseObjectSpread {
61+
type Query = Semantic<JsCallExpression>;
62+
type State = TextRange;
63+
type Signals = Option<Self::State>;
64+
type Options = ();
65+
66+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
67+
let node = ctx.query();
68+
69+
let callee = node.callee().ok()?;
70+
let member_expr = callee.as_js_static_member_expression()?;
71+
72+
let obj = member_expr.object().ok()?;
73+
let (reference, obj_name) = global_identifier(&obj)?;
74+
if obj_name.text() != "Object" || ctx.model().binding(&reference).is_some() {
75+
return None;
76+
}
77+
78+
let method = member_expr.member().ok()?;
79+
if method.value_token().ok()?.text() != "assign" {
80+
return None;
81+
}
82+
83+
let args = node.arguments().ok()?;
84+
let first_arg = args.args().first()?.ok()?;
85+
let expression = first_arg.as_any_js_expression()?;
86+
87+
expression
88+
.as_js_object_expression()
89+
.and(Some(member_expr.range()))
90+
}
91+
92+
fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
93+
Some(
94+
RuleDiagnostic::new(
95+
rule_category!(),
96+
state,
97+
markup! {
98+
"Object spread should be used instead of "<Emphasis>"Object.assign"</Emphasis>
99+
" when constructing new objects."
100+
},
101+
)
102+
.note(markup! {
103+
"Replace "<Emphasis>"Object.assign({...}, <object>)"</Emphasis>
104+
" with "<Emphasis>"{ ...<object> }"</Emphasis>"."
105+
}),
106+
)
107+
}
108+
}

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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Object.assign({}, foo);
2+
Object.assign({}, {foo: 'bar'});
3+
Object.assign({ foo: 'bar'}, baz);
4+
Object.assign({}, baz, { foo: 'bar' });
5+
Object.assign({}, { ...baz });
6+
Object.assign({});
7+
Object.assign({ foo: bar });

0 commit comments

Comments
 (0)