1+ use crate :: JsRuleAction ;
12use 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+ } ;
36use 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
714declare_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
6068impl 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+ }
0 commit comments