Skip to content

Commit aa9297b

Browse files
authoredMar 11, 2024··
feat(es/lints): Add prefer-object-spread rule (#8696)
1 parent a3d877e commit aa9297b

File tree

6 files changed

+343
-0
lines changed

6 files changed

+343
-0
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"jsc": {
3+
"target": "ES2018",
4+
"lints": {
5+
"preferObjectSpread": [
6+
"error"
7+
]
8+
}
9+
}
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
Object.assign({}, foo);
2+
3+
Object.assign({}, { foo: 'bar' });
4+
5+
Object.assign({}, baz, { foo: 'bar' });
6+
7+
Object.assign({}, { foo: 'bar', baz: 'foo' });
8+
9+
Object.assign({ foo: 'bar' }, Object.assign({ bar: 'foo' }, baz))
10+
11+
Object.assign({});
12+
13+
Object['assign']({});
14+
15+
Object[`assign`]({});
16+
17+
// Valid
18+
19+
Object.assign(foo, { bar: baz });
20+
Object.assign();
21+
let a = Object.assign(a, b);
22+
Object.assign(...foo);
23+
Object.assign(foo, { bar: baz });
24+
Object.assign({}, ...objects);
25+
() => {
26+
const Object = {};
27+
Object.assign({}, foo);
28+
};
29+
Object.assign({ get a() {} }, {});
30+
Object.assign({ set a(val) {} }, {});
31+
Object.assign({ get a() {} }, foo);
32+
Object.assign({ set a(val) {} }, foo);
33+
Object.assign({ foo: 'bar', get a() {}, baz: 'quux' }, quuux);
34+
Object.assign({ foo: 'bar', set a(val) {} }, { baz: 'quux' });
35+
Object.assign({}, { get a() {} });
36+
Object.assign({}, { set a(val) {} });
37+
Object.assign({}, { foo: 'bar', get a() {} }, {});
38+
Object.assign({ foo }, bar, {}, { baz: 'quux', set a(val) {}, quuux }, {});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
2+
x "Use an object spread instead of `Object.assign` eg: `{ ...foo }`"
3+
,-[1:1]
4+
1 | Object.assign({}, foo);
5+
: ^^^^^^^^^^^^^^^^^^^^^^
6+
2 |
7+
3 | Object.assign({}, { foo: 'bar' });
8+
`----
9+
10+
x "Use an object spread instead of `Object.assign` eg: `{ ...foo }`"
11+
,-[1:1]
12+
1 | Object.assign({}, foo);
13+
2 |
14+
3 | Object.assign({}, { foo: 'bar' });
15+
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
16+
4 |
17+
5 | Object.assign({}, baz, { foo: 'bar' });
18+
`----
19+
20+
x "Use an object spread instead of `Object.assign` eg: `{ ...foo }`"
21+
,-[2:1]
22+
2 |
23+
3 | Object.assign({}, { foo: 'bar' });
24+
4 |
25+
5 | Object.assign({}, baz, { foo: 'bar' });
26+
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
27+
6 |
28+
7 | Object.assign({}, { foo: 'bar', baz: 'foo' });
29+
`----
30+
31+
x "Use an object spread instead of `Object.assign` eg: `{ ...foo }`"
32+
,-[4:1]
33+
4 |
34+
5 | Object.assign({}, baz, { foo: 'bar' });
35+
6 |
36+
7 | Object.assign({}, { foo: 'bar', baz: 'foo' });
37+
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
38+
8 |
39+
9 | Object.assign({ foo: 'bar' }, Object.assign({ bar: 'foo' }, baz))
40+
`----
41+
42+
x "Use an object spread instead of `Object.assign` eg: `{ ...foo }`"
43+
,-[6:1]
44+
6 |
45+
7 | Object.assign({}, { foo: 'bar', baz: 'foo' });
46+
8 |
47+
9 | Object.assign({ foo: 'bar' }, Object.assign({ bar: 'foo' }, baz))
48+
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
49+
10 |
50+
11 | Object.assign({});
51+
`----
52+
53+
x "Use an object spread instead of `Object.assign` eg: `{ ...foo }`"
54+
,-[6:1]
55+
6 |
56+
7 | Object.assign({}, { foo: 'bar', baz: 'foo' });
57+
8 |
58+
9 | Object.assign({ foo: 'bar' }, Object.assign({ bar: 'foo' }, baz))
59+
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
60+
10 |
61+
11 | Object.assign({});
62+
`----
63+
64+
x "Use an object literal instead of `Object.assign`. eg: `{ foo: bar }`"
65+
,-[8:1]
66+
8 |
67+
9 | Object.assign({ foo: 'bar' }, Object.assign({ bar: 'foo' }, baz))
68+
10 |
69+
11 | Object.assign({});
70+
: ^^^^^^^^^^^^^^^^^
71+
12 |
72+
13 | Object['assign']({});
73+
`----
74+
75+
x "Use an object literal instead of `Object.assign`. eg: `{ foo: bar }`"
76+
,-[10:1]
77+
10 |
78+
11 | Object.assign({});
79+
12 |
80+
13 | Object['assign']({});
81+
: ^^^^^^^^^^^^^^^^^^^^
82+
14 |
83+
15 | Object[`assign`]({});
84+
`----
85+
86+
x "Use an object literal instead of `Object.assign`. eg: `{ foo: bar }`"
87+
,-[12:1]
88+
12 |
89+
13 | Object['assign']({});
90+
14 |
91+
15 | Object[`assign`]({});
92+
: ^^^^^^^^^^^^^^^^^^^^
93+
16 |
94+
17 | // Valid
95+
`----

‎crates/swc_ecma_lints/src/config.rs

+4
Original file line numberDiff line numberDiff line change
@@ -221,4 +221,8 @@ pub struct LintConfig {
221221
pub no_prototype_builtins: RuleConfig<()>,
222222
#[serde(default, alias = "noNewObject")]
223223
pub no_new_object: RuleConfig<()>,
224+
225+
#[cfg(feature = "non_critical_lints")]
226+
#[serde(default, alias = "preferObjectSpread")]
227+
pub prefer_object_spread: RuleConfig<()>,
224228
}

‎crates/swc_ecma_lints/src/rules/mod.rs

+7
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ pub(crate) mod non_critical_lints {
4242
pub mod no_use_before_define;
4343
pub mod no_var;
4444
pub mod prefer_const;
45+
pub mod prefer_object_spread;
4546
pub mod prefer_regex_literals;
4647
pub mod quotes;
4748
pub mod radix;
@@ -205,6 +206,12 @@ pub fn all(lint_params: LintParams) -> Vec<Box<dyn Rule>> {
205206
unresolved_ctxt,
206207
&lint_config.no_new_object,
207208
));
209+
210+
rules.extend(prefer_object_spread::prefer_object_spread(
211+
&lint_config.prefer_object_spread,
212+
unresolved_ctxt,
213+
es_version,
214+
));
208215
}
209216

210217
rules
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
use swc_common::{errors::HANDLER, Span, SyntaxContext};
2+
use swc_ecma_ast::*;
3+
use swc_ecma_utils::ExprExt;
4+
use swc_ecma_visit::{noop_visit_type, Visit, VisitWith};
5+
6+
use crate::{
7+
config::{LintRuleReaction, RuleConfig},
8+
rule::{visitor_rule, Rule},
9+
};
10+
11+
const USE_SPREAD_MESSAGE: &str =
12+
r#""Use an object spread instead of `Object.assign` eg: `{ ...foo }`""#;
13+
14+
const USE_LITERAL_MESSAGE: &str =
15+
r#""Use an object literal instead of `Object.assign`. eg: `{ foo: bar }`""#;
16+
17+
pub fn prefer_object_spread(
18+
config: &RuleConfig<()>,
19+
unresolved_ctxt: SyntaxContext,
20+
es_version: EsVersion,
21+
) -> Option<Box<dyn Rule>> {
22+
if es_version < EsVersion::Es2018 {
23+
return None;
24+
}
25+
26+
let rule_reaction = config.get_rule_reaction();
27+
28+
match rule_reaction {
29+
LintRuleReaction::Off => None,
30+
_ => Some(visitor_rule(PreferObjectSpread::new(
31+
rule_reaction,
32+
unresolved_ctxt,
33+
))),
34+
}
35+
}
36+
37+
#[derive(Debug, Default)]
38+
struct PreferObjectSpread {
39+
expected_reaction: LintRuleReaction,
40+
unresolved_ctxt: SyntaxContext,
41+
}
42+
43+
#[derive(Debug)]
44+
enum ArgType {
45+
EmptyLiteralObject,
46+
LiteralObjectWithFields,
47+
LiteralObjectWithGetterOrSetter,
48+
Ident,
49+
Spread,
50+
Other,
51+
}
52+
53+
impl PreferObjectSpread {
54+
fn new(expected_reaction: LintRuleReaction, unresolved_ctxt: SyntaxContext) -> Self {
55+
Self {
56+
expected_reaction,
57+
unresolved_ctxt,
58+
}
59+
}
60+
61+
fn emit_report(&self, span: Span, message: &str) {
62+
HANDLER.with(|handler| match self.expected_reaction {
63+
LintRuleReaction::Error => {
64+
handler.struct_span_err(span, message).emit();
65+
}
66+
LintRuleReaction::Warning => {
67+
handler.struct_span_warn(span, message).emit();
68+
}
69+
_ => {}
70+
});
71+
}
72+
73+
fn recognize_expr_arg(expr: &Expr) -> ArgType {
74+
match expr {
75+
Expr::Object(obj) => {
76+
if obj.props.is_empty() {
77+
ArgType::EmptyLiteralObject
78+
} else {
79+
let has_getter_or_setter = obj.props.iter().any(|prop| {
80+
if let Some(prop) = prop.as_prop() {
81+
return matches!(prop.as_ref(), Prop::Setter(_) | Prop::Getter(_));
82+
}
83+
84+
false
85+
});
86+
87+
if has_getter_or_setter {
88+
ArgType::LiteralObjectWithGetterOrSetter
89+
} else {
90+
ArgType::LiteralObjectWithFields
91+
}
92+
}
93+
}
94+
Expr::Ident(_) => ArgType::Ident,
95+
Expr::Paren(paren) => Self::recognize_expr_arg(&paren.expr),
96+
Expr::Seq(seq) => {
97+
let last = seq.exprs.last().unwrap();
98+
99+
Self::recognize_expr_arg(last)
100+
}
101+
_ => ArgType::Other,
102+
}
103+
}
104+
105+
fn recognize_arg(expr_or_spread: &ExprOrSpread) -> ArgType {
106+
if expr_or_spread.spread.is_some() {
107+
return ArgType::Spread;
108+
}
109+
110+
Self::recognize_expr_arg(&expr_or_spread.expr)
111+
}
112+
113+
fn is_global_object(&self, expr: &Expr) -> bool {
114+
if let Expr::Ident(ident) = expr {
115+
return ident.sym == "Object" && ident.span.ctxt == self.unresolved_ctxt;
116+
}
117+
118+
false
119+
}
120+
121+
fn is_method_assign(&self, mem_prop: &MemberProp) -> bool {
122+
match mem_prop {
123+
MemberProp::Ident(ident) => ident.sym == "assign",
124+
MemberProp::Computed(computed) => match computed.expr.as_ref() {
125+
Expr::Lit(Lit::Str(lit_str)) => lit_str.value == "assign",
126+
Expr::Tpl(tlp) => {
127+
tlp.exprs.is_empty() && tlp.quasis.len() == 1 && tlp.quasis[0].raw == "assign"
128+
}
129+
_ => false,
130+
},
131+
_ => false,
132+
}
133+
}
134+
135+
fn is_object_assign_call(&self, call_expr: &CallExpr) -> bool {
136+
if let Some(callee) = call_expr.callee.as_expr() {
137+
if let Some(MemberExpr { obj, prop, .. }) = callee.as_member() {
138+
return self.is_global_object(obj.as_expr()) && self.is_method_assign(prop);
139+
}
140+
}
141+
142+
false
143+
}
144+
145+
fn check(&mut self, call_expr: &CallExpr) {
146+
if call_expr.args.is_empty() {
147+
return;
148+
}
149+
150+
if !self.is_object_assign_call(call_expr) {
151+
return;
152+
}
153+
154+
let arg: ArgType = Self::recognize_arg(&call_expr.args[0]);
155+
156+
match (call_expr.args.len(), &arg) {
157+
(1, ArgType::EmptyLiteralObject)
158+
| (1, ArgType::LiteralObjectWithFields)
159+
| (1, ArgType::LiteralObjectWithGetterOrSetter) => {
160+
self.emit_report(call_expr.span, USE_LITERAL_MESSAGE);
161+
}
162+
(_, ArgType::EmptyLiteralObject) | (_, ArgType::LiteralObjectWithFields) => {
163+
let has_spread_or_getter_setter = call_expr.args[1..].iter().any(|prop| {
164+
matches!(
165+
Self::recognize_arg(prop),
166+
ArgType::Spread | ArgType::LiteralObjectWithGetterOrSetter
167+
)
168+
});
169+
170+
if has_spread_or_getter_setter {
171+
return;
172+
}
173+
174+
self.emit_report(call_expr.span, USE_SPREAD_MESSAGE);
175+
}
176+
_ => {}
177+
}
178+
}
179+
}
180+
181+
impl Visit for PreferObjectSpread {
182+
noop_visit_type!();
183+
184+
fn visit_call_expr(&mut self, call_expr: &CallExpr) {
185+
self.check(call_expr);
186+
187+
call_expr.visit_children_with(self);
188+
}
189+
}

0 commit comments

Comments
 (0)
Please sign in to comment.