Skip to content

Commit 8ee6c0b

Browse files
committed
chore: add fix action, improve diagnostics, add more tests
1 parent 8c03833 commit 8ee6c0b

File tree

14 files changed

+829
-85
lines changed

14 files changed

+829
-85
lines changed

crates/biome_configuration/src/analyzer/linter/rules.rs

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 109 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1+
use crate::JsRuleAction;
12
use crate::services::semantic::Semantic;
2-
use biome_analyze::{Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule};
3+
use biome_analyze::{
4+
FixKind, Rule, RuleAction, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule,
5+
};
36
use biome_console::markup;
4-
use biome_js_syntax::{JsCallExpression, global_identifier};
5-
use biome_rowan::{AstNode, AstSeparatedList, TextRange};
7+
use biome_js_factory::make;
8+
use biome_js_syntax::{
9+
AnyJsCallArgument, AnyJsExpression, AnyJsObjectMember, JsCallArgumentList, JsCallExpression,
10+
JsLanguage, JsSyntaxKind, T, global_identifier,
11+
};
12+
use biome_rowan::{AstNode, AstSeparatedList, BatchMutationExt, TextRange};
613

714
declare_lint_rule! {
815
/// Prefer object spread over `Object.assign()` when constructing new objects.
@@ -54,12 +61,13 @@ declare_lint_rule! {
5461
RuleSource::Eslint("prefer-object-spread"),
5562
],
5663
recommended: false,
64+
fix_kind: FixKind::Safe,
5765
}
5866
}
5967

6068
impl Rule for UseObjectSpread {
6169
type Query = Semantic<JsCallExpression>;
62-
type State = TextRange;
70+
type State = RuleState;
6371
type Signals = Option<Self::State>;
6472
type Options = ();
6573

@@ -76,33 +84,112 @@ impl Rule for UseObjectSpread {
7684
}
7785

7886
let method = member_expr.member().ok()?;
79-
if method.value_token().ok()?.text() != "assign" {
87+
if method.value_token().ok()?.text_trimmed() != "assign" {
8088
return None;
8189
}
8290

83-
let args = node.arguments().ok()?;
84-
let first_arg = args.args().first()?.ok()?;
91+
let args = node.arguments().ok()?.args();
92+
93+
let first_arg = args.first()?.ok()?;
94+
8595
let expression = first_arg.as_any_js_expression()?;
96+
if !matches!(expression, AnyJsExpression::JsObjectExpression(_)) {
97+
return None;
98+
}
8699

87-
expression
88-
.as_js_object_expression()
89-
.and(Some(member_expr.range()))
100+
if args
101+
.iter()
102+
.skip(1)
103+
.any(|arg| matches!(arg, Ok(AnyJsCallArgument::JsSpread(_))))
104+
{
105+
// If there are any spread arguments, we cannot convert to object spread
106+
return None;
107+
}
108+
109+
Some(RuleState {
110+
member_expr_range: member_expr.range(),
111+
args,
112+
})
90113
}
91114

92115
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! {
116+
Some(RuleDiagnostic::new(
117+
rule_category!(),
118+
state.member_expr_range,
119+
markup! {
120+
"Object spread syntax is more concise, readable, and performs better"
121+
" than "<Emphasis>"Object.assign"</Emphasis>"."
122+
},
123+
))
124+
}
125+
126+
fn action(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleAction<JsLanguage>> {
127+
let args = &state.args;
128+
let mut object_members = Vec::new();
129+
let mut object_member_separators = Vec::new();
130+
131+
for arg in args.iter().flatten() {
132+
let AnyJsCallArgument::AnyJsExpression(expression) = arg else {
133+
return None;
134+
};
135+
match expression {
136+
// Flatten nested object expressions.
137+
AnyJsExpression::JsObjectExpression(object_expr) => {
138+
let object_member_list = object_expr.members();
139+
let mut separators = object_member_list
140+
.separators()
141+
.flatten()
142+
.collect::<Vec<_>>();
143+
let members = object_member_list.iter().flatten().collect::<Vec<_>>();
144+
// Keep original separators to preserve comments and whitespace.
145+
separators.resize_with(members.len(), || make::token(T![,]));
146+
object_member_separators.extend(separators);
147+
object_members.extend(members);
148+
}
149+
// All the other expressions will be spreaded.
150+
_ => {
151+
object_members.push(AnyJsObjectMember::JsSpread(make::js_spread(
152+
make::token(JsSyntaxKind::DOT3),
153+
expression,
154+
)));
155+
object_member_separators.push(make::token(T![,]));
156+
}
157+
}
158+
}
159+
let mut mutation = ctx.root().begin();
160+
// Building the final object expression.
161+
// Formatter should be able to remove unnecessary trailing comma depending on configuration.
162+
let result_object = make::js_object_expression(
163+
make::token(T!['{']),
164+
make::js_object_member_list(object_members, object_member_separators),
165+
make::token(T!['}']),
166+
);
167+
168+
mutation.replace_node(
169+
AnyJsExpression::JsCallExpression(ctx.query().clone()),
170+
// Wrap into parens in case we are in a statement expression or arrow function body.
171+
// Formatter should be able to remove unnecessary parens.
172+
AnyJsExpression::JsParenthesizedExpression(make::js_parenthesized_expression(
173+
make::token(T!['(']),
174+
AnyJsExpression::JsObjectExpression(result_object.clone()),
175+
make::token(T![')']),
176+
)),
177+
);
178+
179+
Some(JsRuleAction::new(
180+
ctx.metadata().action_category(ctx.category(), ctx.group()),
181+
ctx.metadata().applicability(),
182+
markup! {
103183
"Replace "<Emphasis>"Object.assign({...}, <object>)"</Emphasis>
104184
" with "<Emphasis>"{ ...<object> }"</Emphasis>"."
105-
}),
106-
)
185+
}
186+
.to_owned(),
187+
mutation,
188+
))
107189
}
108190
}
191+
192+
pub struct RuleState {
193+
member_expr_range: TextRange,
194+
args: JsCallArgumentList,
195+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,44 @@
11
Object.assign({}, foo);
2+
Object.assign ({}, foo);
3+
Object .assign ({}, foo);
24
Object.assign({}, {foo: 'bar'});
35
Object.assign({ foo: 'bar'}, baz);
46
Object.assign({}, baz, { foo: 'bar' });
7+
Object.assign({}, { foo: 'bar', baz: 'foo' });
58
Object.assign({}, { ...baz });
69
Object.assign({});
710
Object.assign({ foo: bar });
11+
Object.assign({ foo: 'bar' }, cats, dogs, trees, birds);
12+
Object.assign({ foo: 'bar' }, Object.assign({ bar: 'foo' }, baz));
13+
({foo: 'bar', ...Object.assign({ bar: 'foo' }, baz)});
14+
Object.assign({ foo: 'bar' }, Object.assign({ bar: 'foo' }, Object.assign({}, { superNested: 'butwhy' })));
15+
Object.assign({foo: 'bar', ...bar}, baz);
16+
Object.assign({}, { foo, bar, baz });
17+
Object.assign({}, { [bar]: 'foo' });
18+
Object.assign({ ...bar }, { ...baz });
19+
Object.assign({ ...bar }, {
20+
// this is a bar
21+
foo: 'bar',
22+
baz: "cats"
23+
});
24+
Object.assign({
25+
boo: "lol",
26+
// I'm a comment
27+
dog: "cat"
28+
}, {
29+
// this is a bar
30+
foo: 'bar',
31+
baz: "cats"
32+
});
33+
const test1 = Object.assign({ ...bar }, {
34+
foo: 'bar', // inline comment
35+
baz: "cats"
36+
});
37+
const test2 = Object.assign({ ...bar }, {
38+
/**
39+
* foo
40+
*/
41+
foo: 'bar',
42+
baz: "cats"
43+
});
44+
globalThis.Object.assign({}, foo);

0 commit comments

Comments
 (0)