mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 04:08:41 +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>,
|
||||
ctx: &'b LintContext<'a>,
|
||||
) -> Option<&'b AstNode<'a>> {
|
||||
let mut node = node;
|
||||
|
||||
loop {
|
||||
if let Some(parent) = ctx.nodes().parent_node(node.id()) {
|
||||
if let AstKind::ParenthesizedExpression(_) = parent.kind() {
|
||||
node = parent;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
ctx.nodes().parent_node(node.id())
|
||||
ctx.nodes()
|
||||
.iter_parents(node.id())
|
||||
.skip(1)
|
||||
.find(|parent| !matches!(parent.kind(), AstKind::ParenthesizedExpression(_)))
|
||||
}
|
||||
|
||||
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::{
|
||||
ast::{
|
||||
match_expression, Argument, ArrayExpression, ArrayExpressionElement, CallExpression,
|
||||
Expression, SpreadElement,
|
||||
ArrayExpression, ArrayExpressionElement, CallExpression, Expression, NewExpression,
|
||||
ObjectExpression, ObjectPropertyKind, SpreadElement,
|
||||
},
|
||||
AstKind,
|
||||
};
|
||||
|
|
@ -18,6 +20,7 @@ use crate::{
|
|||
rule::Rule,
|
||||
AstNode,
|
||||
};
|
||||
use const_eval::{is_array_from, is_new_typed_array, ConstEval};
|
||||
|
||||
fn spread_in_list(span: Span, x1: &str) -> OxcDiagnostic {
|
||||
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)
|
||||
}
|
||||
|
||||
fn clone_array(span: Span, x1: &str) -> OxcDiagnostic {
|
||||
OxcDiagnostic::warn("Using a spread operator here creates a new array unnecessarily.")
|
||||
.with_help(format!("`{x1}` returns a new array. Spreading it into an array expression to create a new array is redundant."))
|
||||
.with_label(span)
|
||||
fn clone(span: Span, is_array: bool, x1: Option<&str>) -> OxcDiagnostic {
|
||||
let noun = if is_array { "array" } else { "object" };
|
||||
OxcDiagnostic::warn(format!("Using a spread operator here creates a new {noun} unnecessarily."))
|
||||
.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)]
|
||||
|
|
@ -133,29 +142,48 @@ declare_oxc_lint!(
|
|||
|
||||
impl Rule for NoUselessSpread {
|
||||
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() {
|
||||
check_useless_iterable_to_array(node, array_expr, ctx);
|
||||
check_useless_array_clone(array_expr, ctx);
|
||||
if check_useless_spread_in_list(node, ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
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>) {
|
||||
if !matches!(node.kind(), AstKind::ArrayExpression(_) | AstKind::ObjectExpression(_)) {
|
||||
return;
|
||||
}
|
||||
fn check_useless_spread_in_list<'a>(node: &AstNode<'a>, ctx: &LintContext<'a>) -> bool {
|
||||
let Some(parent) = outermost_paren_parent(node, ctx) else {
|
||||
return;
|
||||
return false;
|
||||
};
|
||||
|
||||
// we're in ...[]
|
||||
let AstKind::SpreadElement(spread_elem) = parent.kind() else {
|
||||
return;
|
||||
return false;
|
||||
};
|
||||
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);
|
||||
|
|
@ -164,7 +192,12 @@ fn check_useless_spread_in_list<'a>(node: &AstNode<'a>, ctx: &LintContext<'a>) {
|
|||
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() {
|
||||
|
|
@ -176,14 +209,16 @@ fn check_useless_spread_in_list<'a>(node: &AstNode<'a>, ctx: &LintContext<'a>) {
|
|||
} else {
|
||||
ctx.diagnostic(diagnostic);
|
||||
}
|
||||
true
|
||||
}
|
||||
// foo(...[ ])
|
||||
AstKind::Argument(_) => {
|
||||
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!()
|
||||
|
|
@ -206,7 +241,7 @@ fn diagnose_array_in_array_spread<'a>(
|
|||
0 => unreachable!(),
|
||||
1 => {
|
||||
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>(
|
||||
node: &AstNode<'a>,
|
||||
array_expr: &ArrayExpression<'a>,
|
||||
spread_elem: &SpreadElement<'a>,
|
||||
ctx: &LintContext<'a>,
|
||||
) {
|
||||
) -> bool {
|
||||
let Some(parent) = outermost_paren_parent(node, ctx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !is_single_array_spread(array_expr) {
|
||||
return;
|
||||
}
|
||||
|
||||
let ArrayExpressionElement::SpreadElement(spread_elem) = &array_expr.elements[0] else {
|
||||
return;
|
||||
return false;
|
||||
};
|
||||
|
||||
let span = Span::new(spread_elem.span.start, spread_elem.span.start + 3);
|
||||
|
||||
let parent = if let AstKind::Argument(_) = parent.kind() {
|
||||
let Some(parent) = outermost_paren_parent(parent, ctx) else {
|
||||
return;
|
||||
return false;
|
||||
};
|
||||
parent
|
||||
} else {
|
||||
|
|
@ -277,47 +305,31 @@ fn check_useless_iterable_to_array<'a>(
|
|||
AstKind::ForOfStatement(for_of_stmt) => {
|
||||
if for_of_stmt.right.without_parenthesized().span() == array_expr.span {
|
||||
ctx.diagnostic(iterable_to_array_in_for_of(span));
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
AstKind::YieldExpression(yield_expr) => {
|
||||
if yield_expr.delegate
|
||||
&& yield_expr.argument.as_ref().is_some_and(|arg| arg.span() == array_expr.span)
|
||||
{
|
||||
ctx.diagnostic(iterable_to_array_in_yield_star(span));
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
AstKind::NewExpression(new_expr) => {
|
||||
if !((is_new_expression(
|
||||
new_expr,
|
||||
&["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)
|
||||
if !((is_new_map_or_set_with_iterable(new_expr) || is_new_typed_array(new_expr))
|
||||
&& new_expr.arguments[0].span().contains_inclusive(array_expr.span))
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
ctx.diagnostic_with_fix(
|
||||
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) => {
|
||||
if !((is_method_call(
|
||||
|
|
@ -326,34 +338,17 @@ fn check_useless_iterable_to_array<'a>(
|
|||
Some(&["all", "allSettled", "any", "race"]),
|
||||
Some(1),
|
||||
Some(1),
|
||||
) || is_method_call(
|
||||
call_expr,
|
||||
Some(&[
|
||||
"Array",
|
||||
"Int8Array",
|
||||
"Uint8Array",
|
||||
"Uint8ClampedArray",
|
||||
"Int16Array",
|
||||
"Uint16Array",
|
||||
"Int32Array",
|
||||
"Uint32Array",
|
||||
"Float32Array",
|
||||
"Float64Array",
|
||||
"BigInt64Array",
|
||||
"BigUint64Array",
|
||||
]),
|
||||
Some(&["from"]),
|
||||
Some(1),
|
||||
Some(1),
|
||||
) || is_method_call(
|
||||
call_expr,
|
||||
Some(&["Object"]),
|
||||
Some(&["fromEntries"]),
|
||||
Some(1),
|
||||
Some(1),
|
||||
)) && innermost_paren_arg_span(&call_expr.arguments[0]) == array_expr.span)
|
||||
) || is_array_from(call_expr)
|
||||
|| is_method_call(
|
||||
call_expr,
|
||||
Some(&["Object"]),
|
||||
Some(&["fromEntries"]),
|
||||
Some(1),
|
||||
Some(1),
|
||||
))
|
||||
&& call_expr.arguments[0].span().contains_inclusive(array_expr.span))
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
ctx.diagnostic_with_fix(
|
||||
|
|
@ -361,102 +356,69 @@ fn check_useless_iterable_to_array<'a>(
|
|||
span,
|
||||
&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>) {
|
||||
if !is_single_array_spread(array_expr) {
|
||||
return;
|
||||
}
|
||||
|
||||
let ArrayExpressionElement::SpreadElement(spread_elem) = &array_expr.elements[0] else {
|
||||
return;
|
||||
};
|
||||
/// Matches `new {Set,WeakSet,Map,WeakMap}(iterable)`
|
||||
pub fn is_new_map_or_set_with_iterable(new_expr: &NewExpression) -> bool {
|
||||
is_new_expression(new_expr, &["Map", "WeakMap", "Set", "WeakSet"], Some(1), Some(1))
|
||||
}
|
||||
|
||||
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 target = spread_elem.argument.get_inner_expression();
|
||||
|
||||
if let Expression::CallExpression(call_expr) = &spread_elem.argument {
|
||||
if !(is_method_call(
|
||||
call_expr,
|
||||
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;
|
||||
}
|
||||
// already diagnosed by first check
|
||||
if matches!(target, Expression::ArrayExpression(_) | Expression::ObjectExpression(_)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let method = call_expr.callee.get_member_expr().map_or_else(
|
||||
|| "unknown".into(),
|
||||
|method| {
|
||||
let object_name = if let Expression::Identifier(ident) = &method.object() {
|
||||
ident.name.as_str()
|
||||
} else {
|
||||
"unknown"
|
||||
};
|
||||
let hint = target.const_eval();
|
||||
let hint_matches_expr = if is_array { hint.is_array() } else { hint.is_object() };
|
||||
if hint_matches_expr {
|
||||
let name = diagnostic_name(ctx, target);
|
||||
|
||||
format!("{}.{}", object_name, method.static_property_name().unwrap())
|
||||
},
|
||||
);
|
||||
|
||||
ctx.diagnostic_with_fix(clone_array(span, &method), |fixer| {
|
||||
fix_by_removing_spread(fixer, array_expr, spread_elem)
|
||||
ctx.diagnostic_with_fix(clone(span, is_array, name), |fixer| {
|
||||
fix_by_removing_array_spread(fixer, &array_or_obj_span, spread_elem)
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn fix_replace<'a, T: GetSpan, U: GetSpan>(
|
||||
fixer: RuleFixer<'_, 'a>,
|
||||
target: &T,
|
||||
replacement: &U,
|
||||
) -> RuleFix<'a> {
|
||||
let replacement = fixer.source_range(replacement.span());
|
||||
fixer.replace(target.span(), replacement)
|
||||
fn diagnostic_name<'a>(ctx: &LintContext<'a>, expr: &Expression<'a>) -> Option<&'a str> {
|
||||
fn pretty_snippet(snippet: &str) -> Option<&str> {
|
||||
// unweildy snippets don't get included in diagnostic messages
|
||||
if snippet.len() > 50 || snippet.contains('\n') {
|
||||
None
|
||||
} else {
|
||||
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`
|
||||
fn fix_by_removing_spread<'a, S: GetSpan>(
|
||||
fn fix_by_removing_array_spread<'a, S: GetSpan>(
|
||||
fixer: RuleFixer<'_, 'a>,
|
||||
iterable: &S,
|
||||
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()))
|
||||
}
|
||||
|
||||
/// Checks if `node` is `[...(expr)]`
|
||||
fn is_single_array_spread(node: &ArrayExpression) -> bool {
|
||||
node.elements.len() == 1 && matches!(node.elements[0], ArrayExpressionElement::SpreadElement(_))
|
||||
/// Creates a fix that replaces `{...spread}` with `spread`, when `spread` is an
|
||||
/// object literal
|
||||
///
|
||||
/// ## 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 {
|
||||
match arg {
|
||||
match_expression!(Argument) => arg.to_expression().without_parenthesized().span(),
|
||||
Argument::SpreadElement(spread_elem) => spread_elem.argument.span(),
|
||||
/// Checks if `node` is `[...(expr)]`
|
||||
fn as_single_array_spread<'a, 's>(node: &'s ArrayExpression<'a>) -> Option<&'s SpreadElement<'a>> {
|
||||
if node.elements.len() != 1 {
|
||||
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()))
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn test() {
|
||||
use crate::tester::Tester;
|
||||
|
|
@ -551,13 +568,19 @@ fn test() {
|
|||
r"[...array.unknown()]",
|
||||
r"[...Object.notReturningArray(foo)]",
|
||||
r"[...NotObject.keys(foo)]",
|
||||
r"[...Int8Array.from(foo)]",
|
||||
r"[...Int8Array.of()]",
|
||||
r"[...new Int8Array(3)]",
|
||||
// NOTE (@DonIsaac) these are pathological, should really not be done,
|
||||
// and supporting them would add a lot of complexity to the rule's
|
||||
// implementation.
|
||||
// r"[...Int8Array.from(foo)]",
|
||||
// r"[...Int8Array.of()]",
|
||||
// r"[...new Int8Array(3)]",
|
||||
r"[...Promise.all(foo)]",
|
||||
r"[...Promise.allSettled(foo)]",
|
||||
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![
|
||||
|
|
@ -647,6 +670,7 @@ fn test() {
|
|||
r"[...foo.toSpliced(0, 1)]",
|
||||
r"[...foo.with(0, bar)]",
|
||||
r#"[...foo.split("|")]"#,
|
||||
r"[...new Array(3)]",
|
||||
r"[...Object.keys(foo)]",
|
||||
r"[...Object.values(foo)]",
|
||||
r"[...Array.from(foo)]",
|
||||
|
|
@ -661,20 +685,60 @@ fn test() {
|
|||
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![
|
||||
// array literals
|
||||
("[...[1,2,3]]", "[1,2,3]"),
|
||||
("[...[1,2,3], ...[4,5,6]]", "[1, 2, 3, 4, 5, 6]"),
|
||||
("[...[1,2,3], ...x]", "[...[1,2,3], ...x]"),
|
||||
("[...[...[1,2,3]]]", "[...[1,2,3]]"),
|
||||
("[...foo.map(x => !!x)]", "foo.map(x => !!x)"),
|
||||
(r"[...await Promise.all(foo)]", r"await Promise.all(foo)"),
|
||||
("const array = [...[a, , b,]]", "const array = [a, , b,]"),
|
||||
// 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"[...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)"),
|
||||
// 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();
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
||||
⚠ 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.
|
||||
╭─[no_useless_spread.tsx:1:2]
|
||||
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.
|
||||
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))
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// `offset` bytes.
|
||||
///
|
||||
|
|
@ -393,4 +441,29 @@ mod test {
|
|||
assert!(!span.contains_inclusive(Span::new(5, 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