Skip to content

Commit b3c3125

Browse files
committed
feat(linter): overhaul unicorn/no-useless-spread (#4791)
I got tired of seeing useless spreads on ternaries and `arr.reduce()` within my company's internal codebase so I overhauled this rule. ## Changes - add fixer for object spreads ```js const before = { a, ...{ b, c }, d } const after = { a, b, c, d } // fixer does not dedupe spaces before `b` ``` - recursively check for useless clones on complex expressions. This rule now catches and auto-fixes the following cases: ```js // ternaries when both branches create a new array or object const obj = { ...(foo ? { a: 1 } : { b: 2 }) } // recursive, so this can support complex cases const arr = [ ...(foo ? a.map(fn) : bar ? Array.from(iter) : await Promise.all(bar)) ] // reduce functions where the initial accumulator creates a new object or array const obj = { ...(arr.reduce(fn, {}) } ```
1 parent 5992b75 commit b3c3125

File tree

5 files changed

+659
-187
lines changed

5 files changed

+659
-187
lines changed

crates/oxc_linter/src/ast_util.rs

+4-14
Original file line numberDiff line numberDiff line change
@@ -233,20 +233,10 @@ pub fn outermost_paren_parent<'a, 'b>(
233233
node: &'b AstNode<'a>,
234234
ctx: &'b LintContext<'a>,
235235
) -> Option<&'b AstNode<'a>> {
236-
let mut node = node;
237-
238-
loop {
239-
if let Some(parent) = ctx.nodes().parent_node(node.id()) {
240-
if let AstKind::ParenthesizedExpression(_) = parent.kind() {
241-
node = parent;
242-
continue;
243-
}
244-
}
245-
246-
break;
247-
}
248-
249-
ctx.nodes().parent_node(node.id())
236+
ctx.nodes()
237+
.iter_parents(node.id())
238+
.skip(1)
239+
.find(|parent| !matches!(parent.kind(), AstKind::ParenthesizedExpression(_)))
250240
}
251241

252242
pub fn get_declaration_of_variable<'a, 'b>(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
use oxc_ast::ast::{
2+
match_expression, Argument, CallExpression, ConditionalExpression, Expression, NewExpression,
3+
};
4+
5+
use crate::ast_util::{is_method_call, is_new_expression};
6+
7+
#[derive(Debug, Clone)]
8+
pub(super) enum ValueHint {
9+
NewObject,
10+
NewArray,
11+
Promise(Box<ValueHint>),
12+
Unknown,
13+
}
14+
15+
impl ValueHint {
16+
pub fn r#await(self) -> Self {
17+
match self {
18+
Self::Promise(inner) => *inner,
19+
_ => self,
20+
}
21+
}
22+
23+
#[inline]
24+
pub fn is_object(&self) -> bool {
25+
matches!(self, Self::NewObject)
26+
}
27+
28+
#[inline]
29+
pub fn is_array(&self) -> bool {
30+
matches!(self, Self::NewArray)
31+
}
32+
}
33+
34+
impl std::ops::BitAnd for ValueHint {
35+
type Output = Self;
36+
fn bitand(self, rhs: Self) -> Self::Output {
37+
match (self, rhs) {
38+
(Self::NewArray, Self::NewArray) => Self::NewArray,
39+
(Self::NewObject, Self::NewObject) => Self::NewObject,
40+
_ => Self::Unknown,
41+
}
42+
}
43+
}
44+
pub(super) trait ConstEval {
45+
fn const_eval(&self) -> ValueHint;
46+
}
47+
48+
impl<'a> ConstEval for Expression<'a> {
49+
fn const_eval(&self) -> ValueHint {
50+
match self.get_inner_expression() {
51+
Self::ArrayExpression(_) => ValueHint::NewArray,
52+
Self::ObjectExpression(_) => ValueHint::NewObject,
53+
Self::AwaitExpression(expr) => expr.argument.const_eval().r#await(),
54+
Self::SequenceExpression(expr) => {
55+
expr.expressions.last().map_or(ValueHint::Unknown, ConstEval::const_eval)
56+
}
57+
Self::ConditionalExpression(cond) => cond.const_eval(),
58+
Self::CallExpression(call) => call.const_eval(),
59+
Self::NewExpression(new) => new.const_eval(),
60+
_ => ValueHint::Unknown,
61+
}
62+
}
63+
}
64+
65+
impl<'a> ConstEval for ConditionalExpression<'a> {
66+
fn const_eval(&self) -> ValueHint {
67+
self.consequent.const_eval() & self.alternate.const_eval()
68+
}
69+
}
70+
71+
impl<'a> ConstEval for Argument<'a> {
72+
fn const_eval(&self) -> ValueHint {
73+
match self {
74+
// using a spread as an initial accumulator value creates a new
75+
// object or array
76+
Self::SpreadElement(spread) => spread.argument.const_eval(),
77+
expr @ match_expression!(Argument) => expr.as_expression().unwrap().const_eval(),
78+
}
79+
}
80+
}
81+
82+
impl<'a> ConstEval for NewExpression<'a> {
83+
fn const_eval(&self) -> ValueHint {
84+
if is_new_array(self) || is_new_map_or_set(self) || is_new_typed_array(self) {
85+
ValueHint::NewArray
86+
} else if is_new_object(self) {
87+
ValueHint::NewObject
88+
} else {
89+
ValueHint::Unknown
90+
}
91+
}
92+
}
93+
94+
fn is_new_array(new_expr: &NewExpression) -> bool {
95+
is_new_expression(new_expr, &["Array"], None, None)
96+
}
97+
98+
/// Matches `new {Set,WeakSet,Map,WeakMap}(iterable?)`
99+
fn is_new_map_or_set(new_expr: &NewExpression) -> bool {
100+
is_new_expression(new_expr, &["Map", "WeakMap", "Set", "WeakSet"], None, Some(1))
101+
}
102+
103+
/// Matches `new Object()` with any number of args.
104+
fn is_new_object(new_expr: &NewExpression) -> bool {
105+
is_new_expression(new_expr, &["Object"], None, None)
106+
}
107+
108+
/// Matches `new <TypedArray>(a, [other args])` with >= 1 arg
109+
pub fn is_new_typed_array(new_expr: &NewExpression) -> bool {
110+
is_new_expression(
111+
new_expr,
112+
&[
113+
"Int8Array",
114+
"Uint8Array",
115+
"Uint8ClampedArray",
116+
"Int16Array",
117+
"Uint16Array",
118+
"Int32Array",
119+
"Uint32Array",
120+
"Float32Array",
121+
"Float64Array",
122+
"BigInt64Array",
123+
"BigUint64Array",
124+
],
125+
Some(1),
126+
None,
127+
)
128+
}
129+
130+
impl<'a> ConstEval for CallExpression<'a> {
131+
fn const_eval(&self) -> ValueHint {
132+
if is_array_from(self)
133+
|| is_split_method(self)
134+
|| is_array_factory(self)
135+
|| is_functional_array_method(self)
136+
|| is_array_producing_obj_method(self)
137+
{
138+
ValueHint::NewArray
139+
} else if is_array_reduce(self) {
140+
self.arguments[1].const_eval()
141+
} else if is_promise_array_method(self) {
142+
ValueHint::Promise(Box::new(ValueHint::NewArray))
143+
} else if is_obj_factory(self) {
144+
ValueHint::NewObject
145+
} else {
146+
// TODO: check initial value for arr.reduce() accumulators
147+
ValueHint::Unknown
148+
}
149+
}
150+
}
151+
152+
/// - `Array.from(x)`
153+
/// - `Int8Array.from(x)`
154+
/// - plus all other typed arrays
155+
pub fn is_array_from(call_expr: &CallExpression) -> bool {
156+
is_method_call(
157+
call_expr,
158+
Some(&[
159+
"Array",
160+
"Int8Array",
161+
"Uint8Array",
162+
"Uint8ClampedArray",
163+
"Int16Array",
164+
"Uint16Array",
165+
"Int32Array",
166+
"Uint32Array",
167+
"Float32Array",
168+
"Float64Array",
169+
"BigInt64Array",
170+
"BigUint64Array",
171+
]),
172+
Some(&["from"]),
173+
Some(1),
174+
Some(1),
175+
)
176+
}
177+
/// `<expr>.{concat,map,filter,...}`
178+
fn is_functional_array_method(call_expr: &CallExpression) -> bool {
179+
is_method_call(
180+
call_expr,
181+
None,
182+
Some(&[
183+
"concat",
184+
"copyWithin",
185+
"filter",
186+
"flat",
187+
"flatMap",
188+
"map",
189+
"slice",
190+
"splice",
191+
"toReversed",
192+
"toSorted",
193+
"toSpliced",
194+
"with",
195+
]),
196+
None,
197+
None,
198+
)
199+
}
200+
201+
/// Matches `<expr>.reduce(a, b)`, which usually looks like
202+
/// ```ts
203+
/// arr.reduce(reducerRn, initialAccumulator)
204+
/// ```
205+
fn is_array_reduce(call_expr: &CallExpression) -> bool {
206+
is_method_call(call_expr, None, Some(&["reduce"]), Some(2), Some(2))
207+
}
208+
209+
/// Matches `<expr>.split(...)`, which usually is `String.prototype.split(pattern)`
210+
fn is_split_method(call_expr: &CallExpression) -> bool {
211+
is_method_call(call_expr, None, Some(&["split"]), None, None)
212+
}
213+
214+
/// Matches `Object.{fromEntries,create}(x)`
215+
fn is_obj_factory(call_expr: &CallExpression) -> bool {
216+
is_method_call(call_expr, Some(&["Object"]), Some(&["fromEntries", "create"]), Some(1), Some(1))
217+
}
218+
219+
/// Matches `Object.{keys,values,entries}(...)`
220+
fn is_array_producing_obj_method(call_expr: &CallExpression) -> bool {
221+
is_method_call(call_expr, Some(&["Object"]), Some(&["keys", "values", "entries"]), None, None)
222+
}
223+
224+
/// Matches `Array.{from,of}(...)`
225+
fn is_array_factory(call_expr: &CallExpression) -> bool {
226+
is_method_call(call_expr, Some(&["Array"]), Some(&["from", "of"]), None, None)
227+
}
228+
229+
/// Matches `Promise.{all,allSettled}(x)`
230+
fn is_promise_array_method(call_expr: &CallExpression) -> bool {
231+
is_method_call(call_expr, Some(&["Promise"]), Some(&["all", "allSettled"]), Some(1), Some(1))
232+
}

0 commit comments

Comments
 (0)