mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
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, {}) }
```
This commit is contained in:
parent
5992b7575e
commit
b3c3125138
5 changed files with 663 additions and 191 deletions
|
|
@ -233,20 +233,10 @@ pub fn outermost_paren_parent<'a, 'b>(
|
||||||
node: &'b AstNode<'a>,
|
node: &'b AstNode<'a>,
|
||||||
ctx: &'b LintContext<'a>,
|
ctx: &'b LintContext<'a>,
|
||||||
) -> Option<&'b AstNode<'a>> {
|
) -> Option<&'b AstNode<'a>> {
|
||||||
let mut node = node;
|
ctx.nodes()
|
||||||
|
.iter_parents(node.id())
|
||||||
loop {
|
.skip(1)
|
||||||
if let Some(parent) = ctx.nodes().parent_node(node.id()) {
|
.find(|parent| !matches!(parent.kind(), AstKind::ParenthesizedExpression(_)))
|
||||||
if let AstKind::ParenthesizedExpression(_) = parent.kind() {
|
|
||||||
node = parent;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.nodes().parent_node(node.id())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_declaration_of_variable<'a, 'b>(
|
pub fn get_declaration_of_variable<'a, 'b>(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,232 @@
|
||||||
|
use oxc_ast::ast::{
|
||||||
|
match_expression, Argument, CallExpression, ConditionalExpression, Expression, NewExpression,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::ast_util::{is_method_call, is_new_expression};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(super) enum ValueHint {
|
||||||
|
NewObject,
|
||||||
|
NewArray,
|
||||||
|
Promise(Box<ValueHint>),
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValueHint {
|
||||||
|
pub fn r#await(self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Promise(inner) => *inner,
|
||||||
|
_ => self,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn is_object(&self) -> bool {
|
||||||
|
matches!(self, Self::NewObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn is_array(&self) -> bool {
|
||||||
|
matches!(self, Self::NewArray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::BitAnd for ValueHint {
|
||||||
|
type Output = Self;
|
||||||
|
fn bitand(self, rhs: Self) -> Self::Output {
|
||||||
|
match (self, rhs) {
|
||||||
|
(Self::NewArray, Self::NewArray) => Self::NewArray,
|
||||||
|
(Self::NewObject, Self::NewObject) => Self::NewObject,
|
||||||
|
_ => Self::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub(super) trait ConstEval {
|
||||||
|
fn const_eval(&self) -> ValueHint;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ConstEval for Expression<'a> {
|
||||||
|
fn const_eval(&self) -> ValueHint {
|
||||||
|
match self.get_inner_expression() {
|
||||||
|
Self::ArrayExpression(_) => ValueHint::NewArray,
|
||||||
|
Self::ObjectExpression(_) => ValueHint::NewObject,
|
||||||
|
Self::AwaitExpression(expr) => expr.argument.const_eval().r#await(),
|
||||||
|
Self::SequenceExpression(expr) => {
|
||||||
|
expr.expressions.last().map_or(ValueHint::Unknown, ConstEval::const_eval)
|
||||||
|
}
|
||||||
|
Self::ConditionalExpression(cond) => cond.const_eval(),
|
||||||
|
Self::CallExpression(call) => call.const_eval(),
|
||||||
|
Self::NewExpression(new) => new.const_eval(),
|
||||||
|
_ => ValueHint::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ConstEval for ConditionalExpression<'a> {
|
||||||
|
fn const_eval(&self) -> ValueHint {
|
||||||
|
self.consequent.const_eval() & self.alternate.const_eval()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ConstEval for Argument<'a> {
|
||||||
|
fn const_eval(&self) -> ValueHint {
|
||||||
|
match self {
|
||||||
|
// using a spread as an initial accumulator value creates a new
|
||||||
|
// object or array
|
||||||
|
Self::SpreadElement(spread) => spread.argument.const_eval(),
|
||||||
|
expr @ match_expression!(Argument) => expr.as_expression().unwrap().const_eval(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ConstEval for NewExpression<'a> {
|
||||||
|
fn const_eval(&self) -> ValueHint {
|
||||||
|
if is_new_array(self) || is_new_map_or_set(self) || is_new_typed_array(self) {
|
||||||
|
ValueHint::NewArray
|
||||||
|
} else if is_new_object(self) {
|
||||||
|
ValueHint::NewObject
|
||||||
|
} else {
|
||||||
|
ValueHint::Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_new_array(new_expr: &NewExpression) -> bool {
|
||||||
|
is_new_expression(new_expr, &["Array"], None, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Matches `new {Set,WeakSet,Map,WeakMap}(iterable?)`
|
||||||
|
fn is_new_map_or_set(new_expr: &NewExpression) -> bool {
|
||||||
|
is_new_expression(new_expr, &["Map", "WeakMap", "Set", "WeakSet"], None, Some(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Matches `new Object()` with any number of args.
|
||||||
|
fn is_new_object(new_expr: &NewExpression) -> bool {
|
||||||
|
is_new_expression(new_expr, &["Object"], None, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Matches `new <TypedArray>(a, [other args])` with >= 1 arg
|
||||||
|
pub fn is_new_typed_array(new_expr: &NewExpression) -> bool {
|
||||||
|
is_new_expression(
|
||||||
|
new_expr,
|
||||||
|
&[
|
||||||
|
"Int8Array",
|
||||||
|
"Uint8Array",
|
||||||
|
"Uint8ClampedArray",
|
||||||
|
"Int16Array",
|
||||||
|
"Uint16Array",
|
||||||
|
"Int32Array",
|
||||||
|
"Uint32Array",
|
||||||
|
"Float32Array",
|
||||||
|
"Float64Array",
|
||||||
|
"BigInt64Array",
|
||||||
|
"BigUint64Array",
|
||||||
|
],
|
||||||
|
Some(1),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ConstEval for CallExpression<'a> {
|
||||||
|
fn const_eval(&self) -> ValueHint {
|
||||||
|
if is_array_from(self)
|
||||||
|
|| is_split_method(self)
|
||||||
|
|| is_array_factory(self)
|
||||||
|
|| is_functional_array_method(self)
|
||||||
|
|| is_array_producing_obj_method(self)
|
||||||
|
{
|
||||||
|
ValueHint::NewArray
|
||||||
|
} else if is_array_reduce(self) {
|
||||||
|
self.arguments[1].const_eval()
|
||||||
|
} else if is_promise_array_method(self) {
|
||||||
|
ValueHint::Promise(Box::new(ValueHint::NewArray))
|
||||||
|
} else if is_obj_factory(self) {
|
||||||
|
ValueHint::NewObject
|
||||||
|
} else {
|
||||||
|
// TODO: check initial value for arr.reduce() accumulators
|
||||||
|
ValueHint::Unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// - `Array.from(x)`
|
||||||
|
/// - `Int8Array.from(x)`
|
||||||
|
/// - plus all other typed arrays
|
||||||
|
pub fn is_array_from(call_expr: &CallExpression) -> bool {
|
||||||
|
is_method_call(
|
||||||
|
call_expr,
|
||||||
|
Some(&[
|
||||||
|
"Array",
|
||||||
|
"Int8Array",
|
||||||
|
"Uint8Array",
|
||||||
|
"Uint8ClampedArray",
|
||||||
|
"Int16Array",
|
||||||
|
"Uint16Array",
|
||||||
|
"Int32Array",
|
||||||
|
"Uint32Array",
|
||||||
|
"Float32Array",
|
||||||
|
"Float64Array",
|
||||||
|
"BigInt64Array",
|
||||||
|
"BigUint64Array",
|
||||||
|
]),
|
||||||
|
Some(&["from"]),
|
||||||
|
Some(1),
|
||||||
|
Some(1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/// `<expr>.{concat,map,filter,...}`
|
||||||
|
fn is_functional_array_method(call_expr: &CallExpression) -> bool {
|
||||||
|
is_method_call(
|
||||||
|
call_expr,
|
||||||
|
None,
|
||||||
|
Some(&[
|
||||||
|
"concat",
|
||||||
|
"copyWithin",
|
||||||
|
"filter",
|
||||||
|
"flat",
|
||||||
|
"flatMap",
|
||||||
|
"map",
|
||||||
|
"slice",
|
||||||
|
"splice",
|
||||||
|
"toReversed",
|
||||||
|
"toSorted",
|
||||||
|
"toSpliced",
|
||||||
|
"with",
|
||||||
|
]),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Matches `<expr>.reduce(a, b)`, which usually looks like
|
||||||
|
/// ```ts
|
||||||
|
/// arr.reduce(reducerRn, initialAccumulator)
|
||||||
|
/// ```
|
||||||
|
fn is_array_reduce(call_expr: &CallExpression) -> bool {
|
||||||
|
is_method_call(call_expr, None, Some(&["reduce"]), Some(2), Some(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Matches `<expr>.split(...)`, which usually is `String.prototype.split(pattern)`
|
||||||
|
fn is_split_method(call_expr: &CallExpression) -> bool {
|
||||||
|
is_method_call(call_expr, None, Some(&["split"]), None, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Matches `Object.{fromEntries,create}(x)`
|
||||||
|
fn is_obj_factory(call_expr: &CallExpression) -> bool {
|
||||||
|
is_method_call(call_expr, Some(&["Object"]), Some(&["fromEntries", "create"]), Some(1), Some(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Matches `Object.{keys,values,entries}(...)`
|
||||||
|
fn is_array_producing_obj_method(call_expr: &CallExpression) -> bool {
|
||||||
|
is_method_call(call_expr, Some(&["Object"]), Some(&["keys", "values", "entries"]), None, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Matches `Array.{from,of}(...)`
|
||||||
|
fn is_array_factory(call_expr: &CallExpression) -> bool {
|
||||||
|
is_method_call(call_expr, Some(&["Array"]), Some(&["from", "of"]), None, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Matches `Promise.{all,allSettled}(x)`
|
||||||
|
fn is_promise_array_method(call_expr: &CallExpression) -> bool {
|
||||||
|
is_method_call(call_expr, Some(&["Promise"]), Some(&["all", "allSettled"]), Some(1), Some(1))
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
|
mod const_eval;
|
||||||
|
|
||||||
use oxc_ast::{
|
use oxc_ast::{
|
||||||
ast::{
|
ast::{
|
||||||
match_expression, Argument, ArrayExpression, ArrayExpressionElement, CallExpression,
|
ArrayExpression, ArrayExpressionElement, CallExpression, Expression, NewExpression,
|
||||||
Expression, SpreadElement,
|
ObjectExpression, ObjectPropertyKind, SpreadElement,
|
||||||
},
|
},
|
||||||
AstKind,
|
AstKind,
|
||||||
};
|
};
|
||||||
|
|
@ -18,6 +20,7 @@ use crate::{
|
||||||
rule::Rule,
|
rule::Rule,
|
||||||
AstNode,
|
AstNode,
|
||||||
};
|
};
|
||||||
|
use const_eval::{is_array_from, is_new_typed_array, ConstEval};
|
||||||
|
|
||||||
fn spread_in_list(span: Span, x1: &str) -> OxcDiagnostic {
|
fn spread_in_list(span: Span, x1: &str) -> OxcDiagnostic {
|
||||||
OxcDiagnostic::warn(format!("Using a spread operator here creates a new {x1} unnecessarily."))
|
OxcDiagnostic::warn(format!("Using a spread operator here creates a new {x1} unnecessarily."))
|
||||||
|
|
@ -49,10 +52,16 @@ fn iterable_to_array_in_yield_star(span: Span) -> OxcDiagnostic {
|
||||||
.with_label(span)
|
.with_label(span)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clone_array(span: Span, x1: &str) -> OxcDiagnostic {
|
fn clone(span: Span, is_array: bool, x1: Option<&str>) -> OxcDiagnostic {
|
||||||
OxcDiagnostic::warn("Using a spread operator here creates a new array unnecessarily.")
|
let noun = if is_array { "array" } else { "object" };
|
||||||
.with_help(format!("`{x1}` returns a new array. Spreading it into an array expression to create a new array is redundant."))
|
OxcDiagnostic::warn(format!("Using a spread operator here creates a new {noun} unnecessarily."))
|
||||||
.with_label(span)
|
.with_help(
|
||||||
|
if let Some(x1) = x1 {
|
||||||
|
format!("`{x1}` returns a new {noun}. Spreading it into an {noun} expression to create a new {noun} is redundant.")
|
||||||
|
} else {
|
||||||
|
|
||||||
|
format!("This expression returns a new {noun}. Spreading it into an {noun} expression to create a new {noun} is redundant.")
|
||||||
|
}).with_label(span)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
|
|
@ -133,29 +142,48 @@ declare_oxc_lint!(
|
||||||
|
|
||||||
impl Rule for NoUselessSpread {
|
impl Rule for NoUselessSpread {
|
||||||
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
|
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
|
||||||
check_useless_spread_in_list(node, ctx);
|
if !matches!(node.kind(), AstKind::ArrayExpression(_) | AstKind::ObjectExpression(_)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if let AstKind::ArrayExpression(array_expr) = node.kind() {
|
if check_useless_spread_in_list(node, ctx) {
|
||||||
check_useless_iterable_to_array(node, array_expr, ctx);
|
return;
|
||||||
check_useless_array_clone(array_expr, ctx);
|
}
|
||||||
|
|
||||||
|
match node.kind() {
|
||||||
|
AstKind::ArrayExpression(array_expr) => {
|
||||||
|
let Some(spread_elem) = as_single_array_spread(array_expr) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if check_useless_iterable_to_array(node, array_expr, spread_elem, ctx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
check_useless_clone(array_expr.span, true, spread_elem, ctx);
|
||||||
|
}
|
||||||
|
AstKind::ObjectExpression(obj_expr) => {
|
||||||
|
let Some(spread_elem) = as_single_obj_spread(obj_expr) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
check_useless_clone(obj_expr.span, false, spread_elem, ctx);
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_useless_spread_in_list<'a>(node: &AstNode<'a>, ctx: &LintContext<'a>) {
|
fn check_useless_spread_in_list<'a>(node: &AstNode<'a>, ctx: &LintContext<'a>) -> bool {
|
||||||
if !matches!(node.kind(), AstKind::ArrayExpression(_) | AstKind::ObjectExpression(_)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let Some(parent) = outermost_paren_parent(node, ctx) else {
|
let Some(parent) = outermost_paren_parent(node, ctx) else {
|
||||||
return;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// we're in ...[]
|
// we're in ...[]
|
||||||
let AstKind::SpreadElement(spread_elem) = parent.kind() else {
|
let AstKind::SpreadElement(spread_elem) = parent.kind() else {
|
||||||
return;
|
return false;
|
||||||
};
|
};
|
||||||
let Some(parent_parent) = outermost_paren_parent(parent, ctx) else {
|
let Some(parent_parent) = outermost_paren_parent(parent, ctx) else {
|
||||||
return;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
let span = Span::new(spread_elem.span.start, spread_elem.span.start + 3);
|
let span = Span::new(spread_elem.span.start, spread_elem.span.start + 3);
|
||||||
|
|
@ -164,7 +192,12 @@ fn check_useless_spread_in_list<'a>(node: &AstNode<'a>, ctx: &LintContext<'a>) {
|
||||||
AstKind::ObjectExpression(_) => {
|
AstKind::ObjectExpression(_) => {
|
||||||
// { ...{ } }
|
// { ...{ } }
|
||||||
if matches!(parent_parent.kind(), AstKind::ObjectExpression(_)) {
|
if matches!(parent_parent.kind(), AstKind::ObjectExpression(_)) {
|
||||||
ctx.diagnostic(spread_in_list(span, "object"));
|
ctx.diagnostic_with_fix(spread_in_list(span, "object"), |fixer| {
|
||||||
|
fix_by_removing_object_spread(fixer, spread_elem)
|
||||||
|
});
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AstKind::ArrayExpression(array_expr) => match parent_parent.kind() {
|
AstKind::ArrayExpression(array_expr) => match parent_parent.kind() {
|
||||||
|
|
@ -176,14 +209,16 @@ fn check_useless_spread_in_list<'a>(node: &AstNode<'a>, ctx: &LintContext<'a>) {
|
||||||
} else {
|
} else {
|
||||||
ctx.diagnostic(diagnostic);
|
ctx.diagnostic(diagnostic);
|
||||||
}
|
}
|
||||||
|
true
|
||||||
}
|
}
|
||||||
// foo(...[ ])
|
// foo(...[ ])
|
||||||
AstKind::Argument(_) => {
|
AstKind::Argument(_) => {
|
||||||
ctx.diagnostic_with_fix(spread_in_arguments(span), |fixer| {
|
ctx.diagnostic_with_fix(spread_in_arguments(span), |fixer| {
|
||||||
fix_by_removing_spread(fixer, array_expr, spread_elem)
|
fix_by_removing_array_spread(fixer, array_expr, spread_elem)
|
||||||
});
|
});
|
||||||
|
true
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => false,
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
|
|
@ -206,7 +241,7 @@ fn diagnose_array_in_array_spread<'a>(
|
||||||
0 => unreachable!(),
|
0 => unreachable!(),
|
||||||
1 => {
|
1 => {
|
||||||
ctx.diagnostic_with_fix(diagnostic, |fixer| {
|
ctx.diagnostic_with_fix(diagnostic, |fixer| {
|
||||||
fix_replace(fixer, &outer_array.span, inner_array)
|
fixer.replace_with(&outer_array.span, inner_array)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
|
@ -248,25 +283,18 @@ fn diagnose_array_in_array_spread<'a>(
|
||||||
fn check_useless_iterable_to_array<'a>(
|
fn check_useless_iterable_to_array<'a>(
|
||||||
node: &AstNode<'a>,
|
node: &AstNode<'a>,
|
||||||
array_expr: &ArrayExpression<'a>,
|
array_expr: &ArrayExpression<'a>,
|
||||||
|
spread_elem: &SpreadElement<'a>,
|
||||||
ctx: &LintContext<'a>,
|
ctx: &LintContext<'a>,
|
||||||
) {
|
) -> bool {
|
||||||
let Some(parent) = outermost_paren_parent(node, ctx) else {
|
let Some(parent) = outermost_paren_parent(node, ctx) else {
|
||||||
return;
|
return false;
|
||||||
};
|
|
||||||
|
|
||||||
if !is_single_array_spread(array_expr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let ArrayExpressionElement::SpreadElement(spread_elem) = &array_expr.elements[0] else {
|
|
||||||
return;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let span = Span::new(spread_elem.span.start, spread_elem.span.start + 3);
|
let span = Span::new(spread_elem.span.start, spread_elem.span.start + 3);
|
||||||
|
|
||||||
let parent = if let AstKind::Argument(_) = parent.kind() {
|
let parent = if let AstKind::Argument(_) = parent.kind() {
|
||||||
let Some(parent) = outermost_paren_parent(parent, ctx) else {
|
let Some(parent) = outermost_paren_parent(parent, ctx) else {
|
||||||
return;
|
return false;
|
||||||
};
|
};
|
||||||
parent
|
parent
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -277,47 +305,31 @@ fn check_useless_iterable_to_array<'a>(
|
||||||
AstKind::ForOfStatement(for_of_stmt) => {
|
AstKind::ForOfStatement(for_of_stmt) => {
|
||||||
if for_of_stmt.right.without_parenthesized().span() == array_expr.span {
|
if for_of_stmt.right.without_parenthesized().span() == array_expr.span {
|
||||||
ctx.diagnostic(iterable_to_array_in_for_of(span));
|
ctx.diagnostic(iterable_to_array_in_for_of(span));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
false
|
||||||
}
|
}
|
||||||
AstKind::YieldExpression(yield_expr) => {
|
AstKind::YieldExpression(yield_expr) => {
|
||||||
if yield_expr.delegate
|
if yield_expr.delegate
|
||||||
&& yield_expr.argument.as_ref().is_some_and(|arg| arg.span() == array_expr.span)
|
&& yield_expr.argument.as_ref().is_some_and(|arg| arg.span() == array_expr.span)
|
||||||
{
|
{
|
||||||
ctx.diagnostic(iterable_to_array_in_yield_star(span));
|
ctx.diagnostic(iterable_to_array_in_yield_star(span));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
AstKind::NewExpression(new_expr) => {
|
AstKind::NewExpression(new_expr) => {
|
||||||
if !((is_new_expression(
|
if !((is_new_map_or_set_with_iterable(new_expr) || is_new_typed_array(new_expr))
|
||||||
new_expr,
|
&& new_expr.arguments[0].span().contains_inclusive(array_expr.span))
|
||||||
&["Map", "WeakMap", "Set", "WeakSet"],
|
|
||||||
Some(1),
|
|
||||||
Some(1),
|
|
||||||
) || is_new_expression(
|
|
||||||
new_expr,
|
|
||||||
&[
|
|
||||||
"Int8Array",
|
|
||||||
"Uint8Array",
|
|
||||||
"Uint8ClampedArray",
|
|
||||||
"Int16Array",
|
|
||||||
"Uint16Array",
|
|
||||||
"Int32Array",
|
|
||||||
"Uint32Array",
|
|
||||||
"Float32Array",
|
|
||||||
"Float64Array",
|
|
||||||
"BigInt64Array",
|
|
||||||
"BigUint64Array",
|
|
||||||
],
|
|
||||||
Some(1),
|
|
||||||
None,
|
|
||||||
)) && innermost_paren_arg_span(&new_expr.arguments[0]) == array_expr.span)
|
|
||||||
{
|
{
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
ctx.diagnostic_with_fix(
|
ctx.diagnostic_with_fix(
|
||||||
iterable_to_array(span, get_new_expr_ident_name(new_expr).unwrap_or("unknown")),
|
iterable_to_array(span, get_new_expr_ident_name(new_expr).unwrap_or("unknown")),
|
||||||
|fixer| fix_by_removing_spread(fixer, &new_expr.arguments[0], spread_elem),
|
|fixer| fix_by_removing_array_spread(fixer, &new_expr.arguments[0], spread_elem),
|
||||||
);
|
);
|
||||||
|
true
|
||||||
}
|
}
|
||||||
AstKind::CallExpression(call_expr) => {
|
AstKind::CallExpression(call_expr) => {
|
||||||
if !((is_method_call(
|
if !((is_method_call(
|
||||||
|
|
@ -326,34 +338,17 @@ fn check_useless_iterable_to_array<'a>(
|
||||||
Some(&["all", "allSettled", "any", "race"]),
|
Some(&["all", "allSettled", "any", "race"]),
|
||||||
Some(1),
|
Some(1),
|
||||||
Some(1),
|
Some(1),
|
||||||
) || is_method_call(
|
) || is_array_from(call_expr)
|
||||||
call_expr,
|
|| is_method_call(
|
||||||
Some(&[
|
|
||||||
"Array",
|
|
||||||
"Int8Array",
|
|
||||||
"Uint8Array",
|
|
||||||
"Uint8ClampedArray",
|
|
||||||
"Int16Array",
|
|
||||||
"Uint16Array",
|
|
||||||
"Int32Array",
|
|
||||||
"Uint32Array",
|
|
||||||
"Float32Array",
|
|
||||||
"Float64Array",
|
|
||||||
"BigInt64Array",
|
|
||||||
"BigUint64Array",
|
|
||||||
]),
|
|
||||||
Some(&["from"]),
|
|
||||||
Some(1),
|
|
||||||
Some(1),
|
|
||||||
) || is_method_call(
|
|
||||||
call_expr,
|
call_expr,
|
||||||
Some(&["Object"]),
|
Some(&["Object"]),
|
||||||
Some(&["fromEntries"]),
|
Some(&["fromEntries"]),
|
||||||
Some(1),
|
Some(1),
|
||||||
Some(1),
|
Some(1),
|
||||||
)) && innermost_paren_arg_span(&call_expr.arguments[0]) == array_expr.span)
|
))
|
||||||
|
&& call_expr.arguments[0].span().contains_inclusive(array_expr.span))
|
||||||
{
|
{
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.diagnostic_with_fix(
|
ctx.diagnostic_with_fix(
|
||||||
|
|
@ -361,102 +356,69 @@ fn check_useless_iterable_to_array<'a>(
|
||||||
span,
|
span,
|
||||||
&get_method_name(call_expr).unwrap_or_else(|| "unknown".into()),
|
&get_method_name(call_expr).unwrap_or_else(|| "unknown".into()),
|
||||||
),
|
),
|
||||||
|fixer| fix_by_removing_spread(fixer, array_expr, spread_elem),
|
|fixer| fix_by_removing_array_spread(fixer, array_expr, spread_elem),
|
||||||
);
|
);
|
||||||
|
true
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_useless_array_clone<'a>(array_expr: &ArrayExpression<'a>, ctx: &LintContext<'a>) {
|
/// Matches `new {Set,WeakSet,Map,WeakMap}(iterable)`
|
||||||
if !is_single_array_spread(array_expr) {
|
pub fn is_new_map_or_set_with_iterable(new_expr: &NewExpression) -> bool {
|
||||||
return;
|
is_new_expression(new_expr, &["Map", "WeakMap", "Set", "WeakSet"], Some(1), Some(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
let ArrayExpressionElement::SpreadElement(spread_elem) = &array_expr.elements[0] else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
fn check_useless_clone<'a>(
|
||||||
|
array_or_obj_span: Span,
|
||||||
|
is_array: bool,
|
||||||
|
spread_elem: &SpreadElement<'a>,
|
||||||
|
ctx: &LintContext<'a>,
|
||||||
|
) -> bool {
|
||||||
let span = Span::new(spread_elem.span.start, spread_elem.span.start + 3);
|
let span = Span::new(spread_elem.span.start, spread_elem.span.start + 3);
|
||||||
|
let target = spread_elem.argument.get_inner_expression();
|
||||||
|
|
||||||
if let Expression::CallExpression(call_expr) = &spread_elem.argument {
|
// already diagnosed by first check
|
||||||
if !(is_method_call(
|
if matches!(target, Expression::ArrayExpression(_) | Expression::ObjectExpression(_)) {
|
||||||
call_expr,
|
return false;
|
||||||
None,
|
|
||||||
Some(&[
|
|
||||||
"concat",
|
|
||||||
"copyWithin",
|
|
||||||
"filter",
|
|
||||||
"flat",
|
|
||||||
"flatMap",
|
|
||||||
"map",
|
|
||||||
"slice",
|
|
||||||
"splice",
|
|
||||||
"toReversed",
|
|
||||||
"toSorted",
|
|
||||||
"toSpliced",
|
|
||||||
"with",
|
|
||||||
]),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
) || is_method_call(call_expr, None, Some(&["split"]), None, None)
|
|
||||||
|| is_method_call(call_expr, Some(&["Object"]), Some(&["keys", "values"]), None, None)
|
|
||||||
|| is_method_call(call_expr, Some(&["Array"]), Some(&["from", "of"]), None, None))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let method = call_expr.callee.get_member_expr().map_or_else(
|
let hint = target.const_eval();
|
||||||
|| "unknown".into(),
|
let hint_matches_expr = if is_array { hint.is_array() } else { hint.is_object() };
|
||||||
|method| {
|
if hint_matches_expr {
|
||||||
let object_name = if let Expression::Identifier(ident) = &method.object() {
|
let name = diagnostic_name(ctx, target);
|
||||||
ident.name.as_str()
|
|
||||||
} else {
|
|
||||||
"unknown"
|
|
||||||
};
|
|
||||||
|
|
||||||
format!("{}.{}", object_name, method.static_property_name().unwrap())
|
ctx.diagnostic_with_fix(clone(span, is_array, name), |fixer| {
|
||||||
},
|
fix_by_removing_array_spread(fixer, &array_or_obj_span, spread_elem)
|
||||||
);
|
|
||||||
|
|
||||||
ctx.diagnostic_with_fix(clone_array(span, &method), |fixer| {
|
|
||||||
fix_by_removing_spread(fixer, array_expr, spread_elem)
|
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
false
|
||||||
if let Expression::AwaitExpression(await_expr) = &spread_elem.argument {
|
|
||||||
if let Expression::CallExpression(call_expr) = &await_expr.argument {
|
|
||||||
if !is_method_call(
|
|
||||||
call_expr,
|
|
||||||
Some(&["Promise"]),
|
|
||||||
Some(&["all", "allSettled"]),
|
|
||||||
Some(1),
|
|
||||||
Some(1),
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let method_name =
|
|
||||||
call_expr.callee.get_member_expr().unwrap().static_property_name().unwrap();
|
|
||||||
|
|
||||||
ctx.diagnostic_with_fix(
|
|
||||||
clone_array(span, &format!("Promise.{method_name}")),
|
|
||||||
|fixer| fix_by_removing_spread(fixer, array_expr, spread_elem),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fix_replace<'a, T: GetSpan, U: GetSpan>(
|
fn diagnostic_name<'a>(ctx: &LintContext<'a>, expr: &Expression<'a>) -> Option<&'a str> {
|
||||||
fixer: RuleFixer<'_, 'a>,
|
fn pretty_snippet(snippet: &str) -> Option<&str> {
|
||||||
target: &T,
|
// unweildy snippets don't get included in diagnostic messages
|
||||||
replacement: &U,
|
if snippet.len() > 50 || snippet.contains('\n') {
|
||||||
) -> RuleFix<'a> {
|
None
|
||||||
let replacement = fixer.source_range(replacement.span());
|
} else {
|
||||||
fixer.replace(target.span(), replacement)
|
Some(snippet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match expr {
|
||||||
|
Expression::CallExpression(call) => diagnostic_name(ctx, &call.callee),
|
||||||
|
Expression::AwaitExpression(expr) => diagnostic_name(ctx, &expr.argument),
|
||||||
|
Expression::SequenceExpression(expr) => {
|
||||||
|
let span_with_parens = expr.span().expand(1);
|
||||||
|
pretty_snippet(ctx.source_range(span_with_parens))
|
||||||
|
}
|
||||||
|
_ => pretty_snippet(ctx.source_range(expr.span())),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a fix that replaces `[...spread]` with `spread`
|
/// Creates a fix that replaces `[...spread]` with `spread`
|
||||||
fn fix_by_removing_spread<'a, S: GetSpan>(
|
fn fix_by_removing_array_spread<'a, S: GetSpan>(
|
||||||
fixer: RuleFixer<'_, 'a>,
|
fixer: RuleFixer<'_, 'a>,
|
||||||
iterable: &S,
|
iterable: &S,
|
||||||
spread: &SpreadElement<'a>,
|
spread: &SpreadElement<'a>,
|
||||||
|
|
@ -464,15 +426,53 @@ fn fix_by_removing_spread<'a, S: GetSpan>(
|
||||||
fixer.replace(iterable.span(), fixer.source_range(spread.argument.span()))
|
fixer.replace(iterable.span(), fixer.source_range(spread.argument.span()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if `node` is `[...(expr)]`
|
/// Creates a fix that replaces `{...spread}` with `spread`, when `spread` is an
|
||||||
fn is_single_array_spread(node: &ArrayExpression) -> bool {
|
/// object literal
|
||||||
node.elements.len() == 1 && matches!(node.elements[0], ArrayExpressionElement::SpreadElement(_))
|
///
|
||||||
|
/// ## Examples
|
||||||
|
/// - `{...{ a, b, }}` -> `{ a, b }`
|
||||||
|
fn fix_by_removing_object_spread<'a>(
|
||||||
|
fixer: RuleFixer<'_, 'a>,
|
||||||
|
spread: &SpreadElement<'a>,
|
||||||
|
) -> RuleFix<'a> {
|
||||||
|
// get contents inside object brackets
|
||||||
|
// e.g. `...{ a, b, }` -> ` a, b, `
|
||||||
|
let replacement_span = &spread.argument.span().shrink(1);
|
||||||
|
|
||||||
|
// remove trailing commas to avoid syntax errors if this spread is followed
|
||||||
|
// by another property
|
||||||
|
// e.g. ` a, b, ` -> `a, b`
|
||||||
|
let mut end_shrink_amount = 0;
|
||||||
|
for c in fixer.source_range(*replacement_span).chars().rev() {
|
||||||
|
if c.is_whitespace() || c == ',' {
|
||||||
|
end_shrink_amount += 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let replacement_span = replacement_span.shrink_right(end_shrink_amount);
|
||||||
|
|
||||||
|
fixer.replace_with(&spread.span, &replacement_span)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn innermost_paren_arg_span(arg: &Argument) -> Span {
|
/// Checks if `node` is `[...(expr)]`
|
||||||
match arg {
|
fn as_single_array_spread<'a, 's>(node: &'s ArrayExpression<'a>) -> Option<&'s SpreadElement<'a>> {
|
||||||
match_expression!(Argument) => arg.to_expression().without_parenthesized().span(),
|
if node.elements.len() != 1 {
|
||||||
Argument::SpreadElement(spread_elem) => spread_elem.argument.span(),
|
return None;
|
||||||
|
}
|
||||||
|
match &node.elements[0] {
|
||||||
|
ArrayExpressionElement::SpreadElement(spread) => Some(spread.as_ref()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_single_obj_spread<'a, 's>(node: &'s ObjectExpression<'a>) -> Option<&'s SpreadElement<'a>> {
|
||||||
|
if node.properties.len() != 1 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
match &node.properties[0] {
|
||||||
|
ObjectPropertyKind::SpreadProperty(spread) => Some(spread.as_ref()),
|
||||||
|
ObjectPropertyKind::ObjectProperty(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -488,6 +488,23 @@ fn get_method_name(call_expr: &CallExpression) -> Option<String> {
|
||||||
Some(format!("{}.{}", object_name, callee.static_property_name().unwrap()))
|
Some(format!("{}.{}", object_name, callee.static_property_name().unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_debug() {
|
||||||
|
use crate::tester::Tester;
|
||||||
|
|
||||||
|
let pass = vec![];
|
||||||
|
|
||||||
|
let fail = vec![
|
||||||
|
// "[...arr.reduce(f, new Set())]",
|
||||||
|
"const obj = { a, ...{ b, c } }",
|
||||||
|
// "const promise = Promise.all([...iterable])",
|
||||||
|
// "const obj = { ...(foo ? { a: 1 } : { a: 2 }) }",
|
||||||
|
// "const array = [...[a]]",
|
||||||
|
];
|
||||||
|
|
||||||
|
Tester::new(NoUselessSpread::NAME, pass, fail).test();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test() {
|
fn test() {
|
||||||
use crate::tester::Tester;
|
use crate::tester::Tester;
|
||||||
|
|
@ -551,13 +568,19 @@ fn test() {
|
||||||
r"[...array.unknown()]",
|
r"[...array.unknown()]",
|
||||||
r"[...Object.notReturningArray(foo)]",
|
r"[...Object.notReturningArray(foo)]",
|
||||||
r"[...NotObject.keys(foo)]",
|
r"[...NotObject.keys(foo)]",
|
||||||
r"[...Int8Array.from(foo)]",
|
// NOTE (@DonIsaac) these are pathological, should really not be done,
|
||||||
r"[...Int8Array.of()]",
|
// and supporting them would add a lot of complexity to the rule's
|
||||||
r"[...new Int8Array(3)]",
|
// implementation.
|
||||||
|
// r"[...Int8Array.from(foo)]",
|
||||||
|
// r"[...Int8Array.of()]",
|
||||||
|
// r"[...new Int8Array(3)]",
|
||||||
r"[...Promise.all(foo)]",
|
r"[...Promise.all(foo)]",
|
||||||
r"[...Promise.allSettled(foo)]",
|
r"[...Promise.allSettled(foo)]",
|
||||||
r"[...await Promise.all(foo, extraArgument)]",
|
r"[...await Promise.all(foo, extraArgument)]",
|
||||||
r"[...new Array(3)]",
|
// Complex object spreads
|
||||||
|
r"const obj = { ...obj, ...(addFoo ? { foo: 'foo' } : {}) }",
|
||||||
|
r"<Button {...(isLoading ? { data: undefined } : { data: dataFromApi })} />",
|
||||||
|
r"const obj = { ...(foo ? getObjectInOpaqueManner() : { a: 2 }) }",
|
||||||
];
|
];
|
||||||
|
|
||||||
let fail = vec![
|
let fail = vec![
|
||||||
|
|
@ -647,6 +670,7 @@ fn test() {
|
||||||
r"[...foo.toSpliced(0, 1)]",
|
r"[...foo.toSpliced(0, 1)]",
|
||||||
r"[...foo.with(0, bar)]",
|
r"[...foo.with(0, bar)]",
|
||||||
r#"[...foo.split("|")]"#,
|
r#"[...foo.split("|")]"#,
|
||||||
|
r"[...new Array(3)]",
|
||||||
r"[...Object.keys(foo)]",
|
r"[...Object.keys(foo)]",
|
||||||
r"[...Object.values(foo)]",
|
r"[...Object.values(foo)]",
|
||||||
r"[...Array.from(foo)]",
|
r"[...Array.from(foo)]",
|
||||||
|
|
@ -661,20 +685,60 @@ fn test() {
|
||||||
availableAggregates.push(...['p50', 'p75', 'p95', 'p99']);
|
availableAggregates.push(...['p50', 'p75', 'p95', 'p99']);
|
||||||
}
|
}
|
||||||
",
|
",
|
||||||
|
// useless array clones with complex expressions
|
||||||
|
"[...(foo ? [1] : [2])]",
|
||||||
|
"[...(foo
|
||||||
|
? [1]
|
||||||
|
: bar
|
||||||
|
? [2]
|
||||||
|
: [2]
|
||||||
|
)]",
|
||||||
|
"[...(foo ? x.map(x => x) : await Promise.all(foo))]",
|
||||||
|
"[...((0, []))]",
|
||||||
|
"[...arr.reduce((a, b) => a.push(b), [])]",
|
||||||
|
// wait on non-promise value `v` produces `v` itself
|
||||||
|
"[...arr.reduce((a, b) => a.push(b), await [])]",
|
||||||
|
"[...arr.reduce((a, b) => a.push(b), new Array())]",
|
||||||
|
"[...arr.reduce((a, b) => a.push(b), new Array(1, 2, 3))]",
|
||||||
|
"[...arr.reduce((a, b) => a.push(b), Array.from(iter))]",
|
||||||
|
"[...arr.reduce((a, b) => a.push(b), foo.map(x => x))]",
|
||||||
|
"[...arr.reduce((a, b) => a.push(b), await Promise.all(promises))]",
|
||||||
|
"[...arr.reduce((set, b) => set.add(b), new Set())]",
|
||||||
|
"[...arr.reduce((set, b) => set.add(b), new Set(iter))]",
|
||||||
|
// useless object clones with complex expressions
|
||||||
|
r"const obj = { ...(foo ? { a: 1 } : { a: 2 }) }",
|
||||||
|
r"const obj = { ...(foo ? Object.entries(obj).reduce(fn, {}) : { a: 2 }) }",
|
||||||
];
|
];
|
||||||
|
|
||||||
let fix = vec![
|
let fix = vec![
|
||||||
|
// array literals
|
||||||
("[...[1,2,3]]", "[1,2,3]"),
|
("[...[1,2,3]]", "[1,2,3]"),
|
||||||
("[...[1,2,3], ...[4,5,6]]", "[1, 2, 3, 4, 5, 6]"),
|
("[...[1,2,3], ...[4,5,6]]", "[1, 2, 3, 4, 5, 6]"),
|
||||||
("[...[1,2,3], ...x]", "[...[1,2,3], ...x]"),
|
("[...[1,2,3], ...x]", "[...[1,2,3], ...x]"),
|
||||||
("[...[...[1,2,3]]]", "[...[1,2,3]]"),
|
("[...[...[1,2,3]]]", "[...[1,2,3]]"),
|
||||||
("[...foo.map(x => !!x)]", "foo.map(x => !!x)"),
|
("const array = [...[a, , b,]]", "const array = [a, , b,]"),
|
||||||
(r"[...await Promise.all(foo)]", r"await Promise.all(foo)"),
|
// object literals
|
||||||
|
("const obj = { a, ...{ b, c } }", "const obj = { a, b, c }"),
|
||||||
|
("const obj = { a, ...{ b, c, } }", "const obj = { a, b, c }"),
|
||||||
|
("const obj = {a, ...{b,c}}", "const obj = {a, b,c}"),
|
||||||
|
("const obj = {a, ...{b,c,}}", "const obj = {a, b,c}"),
|
||||||
|
("const obj = { a, ...{ b, c }, ...{ d } }", "const obj = { a, b, c, d }"),
|
||||||
|
// iterable spread
|
||||||
(r"const promise = Promise.any([...iterable])", r"const promise = Promise.any(iterable)"),
|
(r"const promise = Promise.any([...iterable])", r"const promise = Promise.any(iterable)"),
|
||||||
(r"[...Array.from(iterable)]", r"Array.from(iterable)"),
|
|
||||||
(r"new Map([...iterable])", r"new Map(iterable)"),
|
(r"new Map([...iterable])", r"new Map(iterable)"),
|
||||||
(r"new Map([ ...((iterable)) ])", r"new Map(((iterable)))"),
|
(r"new Map([ ...((iterable)) ])", r"new Map(((iterable)))"),
|
||||||
// (r"new Map(...[...iterable])", r"new Map(iterable)"),
|
// (r"new Map(...[...iterable])", r"new Map(iterable)"),
|
||||||
|
// useless clones - simple arrays
|
||||||
|
("[...foo.map(x => !!x)]", "foo.map(x => !!x)"),
|
||||||
|
("[...new Array()]", "new Array()"),
|
||||||
|
("[...new Array(1, 2, 3)]", "new Array(1, 2, 3)"),
|
||||||
|
// usel
|
||||||
|
// useless clones - complex
|
||||||
|
(r"[...await Promise.all(foo)]", r"await Promise.all(foo)"),
|
||||||
|
(r"[...Array.from(iterable)]", r"Array.from(iterable)"),
|
||||||
|
("[...((0, []))]", "((0, []))"),
|
||||||
|
("[...arr.reduce((a, b) => a.push(b), [])]", "arr.reduce((a, b) => a.push(b), [])"),
|
||||||
|
("[...arr.reduce((a, b) => a.push(b), [])]", "arr.reduce((a, b) => a.push(b), [])"),
|
||||||
];
|
];
|
||||||
Tester::new(NoUselessSpread::NAME, pass, fail).expect_fix(fix).test_and_snapshot();
|
Tester::new(NoUselessSpread::NAME, pass, fail).expect_fix(fix).test_and_snapshot();
|
||||||
}
|
}
|
||||||
|
|
@ -637,6 +637,13 @@ source: crates/oxc_linter/src/tester.rs
|
||||||
╰────
|
╰────
|
||||||
help: `foo.split` returns a new array. Spreading it into an array expression to create a new array is redundant.
|
help: `foo.split` returns a new array. Spreading it into an array expression to create a new array is redundant.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-unicorn(no-useless-spread): Using a spread operator here creates a new array unnecessarily.
|
||||||
|
╭─[no_useless_spread.tsx:1:2]
|
||||||
|
1 │ [...new Array(3)]
|
||||||
|
· ───
|
||||||
|
╰────
|
||||||
|
help: `new Array(3)` returns a new array. Spreading it into an array expression to create a new array is redundant.
|
||||||
|
|
||||||
⚠ eslint-plugin-unicorn(no-useless-spread): Using a spread operator here creates a new array unnecessarily.
|
⚠ eslint-plugin-unicorn(no-useless-spread): Using a spread operator here creates a new array unnecessarily.
|
||||||
╭─[no_useless_spread.tsx:1:2]
|
╭─[no_useless_spread.tsx:1:2]
|
||||||
1 │ [...Object.keys(foo)]
|
1 │ [...Object.keys(foo)]
|
||||||
|
|
@ -702,3 +709,109 @@ source: crates/oxc_linter/src/tester.rs
|
||||||
╰────
|
╰────
|
||||||
help: This function accepts a rest parameter, it's unnecessary to create a new array and then spread it. Instead, supply the arguments directly.
|
help: This function accepts a rest parameter, it's unnecessary to create a new array and then spread it. Instead, supply the arguments directly.
|
||||||
For example, replace `foo(...[1, 2, 3])` with `foo(1, 2, 3)`.
|
For example, replace `foo(...[1, 2, 3])` with `foo(1, 2, 3)`.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-unicorn(no-useless-spread): Using a spread operator here creates a new array unnecessarily.
|
||||||
|
╭─[no_useless_spread.tsx:1:2]
|
||||||
|
1 │ [...(foo ? [1] : [2])]
|
||||||
|
· ───
|
||||||
|
╰────
|
||||||
|
help: `foo ? [1] : [2]` returns a new array. Spreading it into an array expression to create a new array is redundant.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-unicorn(no-useless-spread): Using a spread operator here creates a new array unnecessarily.
|
||||||
|
╭─[no_useless_spread.tsx:1:2]
|
||||||
|
1 │ [...(foo
|
||||||
|
· ───
|
||||||
|
2 │ ? [1]
|
||||||
|
╰────
|
||||||
|
help: This expression returns a new array. Spreading it into an array expression to create a new array is redundant.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-unicorn(no-useless-spread): Using a spread operator here creates a new array unnecessarily.
|
||||||
|
╭─[no_useless_spread.tsx:1:2]
|
||||||
|
1 │ [...(foo ? x.map(x => x) : await Promise.all(foo))]
|
||||||
|
· ───
|
||||||
|
╰────
|
||||||
|
help: `foo ? x.map(x => x) : await Promise.all(foo)` returns a new array. Spreading it into an array expression to create a new array is redundant.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-unicorn(no-useless-spread): Using a spread operator here creates a new array unnecessarily.
|
||||||
|
╭─[no_useless_spread.tsx:1:2]
|
||||||
|
1 │ [...((0, []))]
|
||||||
|
· ───
|
||||||
|
╰────
|
||||||
|
help: `(0, [])` returns a new array. Spreading it into an array expression to create a new array is redundant.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-unicorn(no-useless-spread): Using a spread operator here creates a new array unnecessarily.
|
||||||
|
╭─[no_useless_spread.tsx:1:2]
|
||||||
|
1 │ [...arr.reduce((a, b) => a.push(b), [])]
|
||||||
|
· ───
|
||||||
|
╰────
|
||||||
|
help: `arr.reduce` returns a new array. Spreading it into an array expression to create a new array is redundant.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-unicorn(no-useless-spread): Using a spread operator here creates a new array unnecessarily.
|
||||||
|
╭─[no_useless_spread.tsx:1:2]
|
||||||
|
1 │ [...arr.reduce((a, b) => a.push(b), await [])]
|
||||||
|
· ───
|
||||||
|
╰────
|
||||||
|
help: `arr.reduce` returns a new array. Spreading it into an array expression to create a new array is redundant.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-unicorn(no-useless-spread): Using a spread operator here creates a new array unnecessarily.
|
||||||
|
╭─[no_useless_spread.tsx:1:2]
|
||||||
|
1 │ [...arr.reduce((a, b) => a.push(b), new Array())]
|
||||||
|
· ───
|
||||||
|
╰────
|
||||||
|
help: `arr.reduce` returns a new array. Spreading it into an array expression to create a new array is redundant.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-unicorn(no-useless-spread): Using a spread operator here creates a new array unnecessarily.
|
||||||
|
╭─[no_useless_spread.tsx:1:2]
|
||||||
|
1 │ [...arr.reduce((a, b) => a.push(b), new Array(1, 2, 3))]
|
||||||
|
· ───
|
||||||
|
╰────
|
||||||
|
help: `arr.reduce` returns a new array. Spreading it into an array expression to create a new array is redundant.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-unicorn(no-useless-spread): Using a spread operator here creates a new array unnecessarily.
|
||||||
|
╭─[no_useless_spread.tsx:1:2]
|
||||||
|
1 │ [...arr.reduce((a, b) => a.push(b), Array.from(iter))]
|
||||||
|
· ───
|
||||||
|
╰────
|
||||||
|
help: `arr.reduce` returns a new array. Spreading it into an array expression to create a new array is redundant.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-unicorn(no-useless-spread): Using a spread operator here creates a new array unnecessarily.
|
||||||
|
╭─[no_useless_spread.tsx:1:2]
|
||||||
|
1 │ [...arr.reduce((a, b) => a.push(b), foo.map(x => x))]
|
||||||
|
· ───
|
||||||
|
╰────
|
||||||
|
help: `arr.reduce` returns a new array. Spreading it into an array expression to create a new array is redundant.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-unicorn(no-useless-spread): Using a spread operator here creates a new array unnecessarily.
|
||||||
|
╭─[no_useless_spread.tsx:1:2]
|
||||||
|
1 │ [...arr.reduce((a, b) => a.push(b), await Promise.all(promises))]
|
||||||
|
· ───
|
||||||
|
╰────
|
||||||
|
help: `arr.reduce` returns a new array. Spreading it into an array expression to create a new array is redundant.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-unicorn(no-useless-spread): Using a spread operator here creates a new array unnecessarily.
|
||||||
|
╭─[no_useless_spread.tsx:1:2]
|
||||||
|
1 │ [...arr.reduce((set, b) => set.add(b), new Set())]
|
||||||
|
· ───
|
||||||
|
╰────
|
||||||
|
help: `arr.reduce` returns a new array. Spreading it into an array expression to create a new array is redundant.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-unicorn(no-useless-spread): Using a spread operator here creates a new array unnecessarily.
|
||||||
|
╭─[no_useless_spread.tsx:1:2]
|
||||||
|
1 │ [...arr.reduce((set, b) => set.add(b), new Set(iter))]
|
||||||
|
· ───
|
||||||
|
╰────
|
||||||
|
help: `arr.reduce` returns a new array. Spreading it into an array expression to create a new array is redundant.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-unicorn(no-useless-spread): Using a spread operator here creates a new object unnecessarily.
|
||||||
|
╭─[no_useless_spread.tsx:1:15]
|
||||||
|
1 │ const obj = { ...(foo ? { a: 1 } : { a: 2 }) }
|
||||||
|
· ───
|
||||||
|
╰────
|
||||||
|
help: `foo ? { a: 1 } : { a: 2 }` returns a new object. Spreading it into an object expression to create a new object is redundant.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-unicorn(no-useless-spread): Using a spread operator here creates a new object unnecessarily.
|
||||||
|
╭─[no_useless_spread.tsx:1:15]
|
||||||
|
1 │ const obj = { ...(foo ? Object.entries(obj).reduce(fn, {}) : { a: 2 }) }
|
||||||
|
· ───
|
||||||
|
╰────
|
||||||
|
help: This expression returns a new object. Spreading it into an object expression to create a new object is redundant.
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,54 @@ impl Span {
|
||||||
Self::new(self.start.min(other.start), self.end.max(other.end))
|
Self::new(self.start.min(other.start), self.end.max(other.end))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a [`Span`] that is grown by `offset` on either side.
|
||||||
|
///
|
||||||
|
/// This is equivalent to `span.expand_left(offset).expand_right(offset)`.
|
||||||
|
/// See [`expand_left`] and [`expand_right`] for more info.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// use oxc_span::Span;
|
||||||
|
///
|
||||||
|
/// let span = Span::new(3, 5);
|
||||||
|
/// assert_eq!(span.expand(1), Span::new(2, 6));
|
||||||
|
/// // start and end cannot be expanded past `0` and `u32::MAX`, respectively
|
||||||
|
/// assert_eq!(span.expand(5), Span::new(0, 10));
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [`expand_left`]: Span::expand_left
|
||||||
|
/// [`expand_right`]: Span::expand_right
|
||||||
|
#[must_use]
|
||||||
|
pub fn expand(self, offset: u32) -> Self {
|
||||||
|
Self::new(self.start.saturating_sub(offset), self.end.saturating_add(offset))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a [`Span`] that has its start and end positions shrunk by
|
||||||
|
/// `offset` amount.
|
||||||
|
///
|
||||||
|
/// It is a logical error to shrink the start of the [`Span`] past its end
|
||||||
|
/// position. This will panic in debug builds.
|
||||||
|
///
|
||||||
|
/// This is equivalent to `span.shrink_left(offset).shrink_right(offset)`.
|
||||||
|
/// See [`shrink_left`] and [`shrink_right`] for more info.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// use oxc_span::Span;
|
||||||
|
/// let span = Span::new(5, 10);
|
||||||
|
/// assert_eq!(span.shrink(2), Span::new(7, 8));
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [`shrink_left`]: Span::shrink_left
|
||||||
|
/// [`shrink_right`]: Span::shrink_right
|
||||||
|
#[must_use]
|
||||||
|
pub fn shrink(self, offset: u32) -> Self {
|
||||||
|
let start = self.start.saturating_add(offset);
|
||||||
|
let end = self.end.saturating_sub(offset);
|
||||||
|
debug_assert!(start <= end, "Cannot shrink span past zero length");
|
||||||
|
Self::new(start, end)
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a [`Span`] that has its start position moved to the left by
|
/// Create a [`Span`] that has its start position moved to the left by
|
||||||
/// `offset` bytes.
|
/// `offset` bytes.
|
||||||
///
|
///
|
||||||
|
|
@ -393,4 +441,29 @@ mod test {
|
||||||
assert!(!span.contains_inclusive(Span::new(5, 11)));
|
assert!(!span.contains_inclusive(Span::new(5, 11)));
|
||||||
assert!(!span.contains_inclusive(Span::new(4, 11)));
|
assert!(!span.contains_inclusive(Span::new(4, 11)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_expand() {
|
||||||
|
let span = Span::new(3, 5);
|
||||||
|
assert_eq!(span.expand(0), Span::new(3, 5));
|
||||||
|
assert_eq!(span.expand(1), Span::new(2, 6));
|
||||||
|
// start and end cannot be expanded past `0` and `u32::MAX`, respectively
|
||||||
|
assert_eq!(span.expand(5), Span::new(0, 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_shrink() {
|
||||||
|
let span = Span::new(4, 8);
|
||||||
|
assert_eq!(span.shrink(0), Span::new(4, 8));
|
||||||
|
assert_eq!(span.shrink(1), Span::new(5, 7));
|
||||||
|
// can be equal
|
||||||
|
assert_eq!(span.shrink(2), Span::new(6, 6));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "Cannot shrink span past zero length")]
|
||||||
|
fn test_shrink_past_start() {
|
||||||
|
let span = Span::new(5, 10);
|
||||||
|
let _ = span.shrink(5);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue