+
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .changeset/proud-bananas-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
"@biomejs/biome": patch
---

Added the new lint rule, [`useSpread`](https://biomejs.dev/linter/rules/use-spread/), ported from the ESLint rule [`prefer-spread`](https://eslint.org/docs/latest/rules/prefer-spread).

This rule enforces the use of the **spread syntax** (`...`) over `Function.prototype.apply()` when calling variadic functions, as spread syntax is generally more concise and idiomatic in modern JavaScript (ES2015+).

The rule provides a safe fix.

#### Invalid

```js
Math.max.apply(Math, args);
foo.apply(undefined, args);
obj.method.apply(obj, args);
```

#### Valid

```js
Math.max(...args);
foo(...args);
obj.method(...args);

// Allowed: cases where the `this` binding is intentionally changed
foo.apply(otherObj, args);
```
27 changes: 24 additions & 3 deletions crates/biome_configuration/src/analyzer/linter/rules.rs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions crates/biome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ define_categories! {
"lint/nursery/useQwikValidLexicalScope": "https://biomejs.dev/linter/rules/use-qwik-valid-lexical-scope",
"lint/nursery/useReactFunctionComponents": "https://biomejs.dev/linter/rules/use-react-function-components",
"lint/nursery/useSortedClasses": "https://biomejs.dev/linter/rules/use-sorted-classes",
"lint/nursery/useSpread": "https://biomejs.dev/linter/rules/use-spread",
"lint/nursery/useVueMultiWordComponentNames": "https://biomejs.dev/linter/rules/use-vue-multi-word-component-names",
"lint/performance/noAccumulatingSpread": "https://biomejs.dev/linter/rules/no-accumulating-spread",
"lint/performance/noAwaitInLoops": "https://biomejs.dev/linter/rules/no-await-in-loops",
Expand Down
3 changes: 2 additions & 1 deletion crates/biome_js_analyze/src/lint/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,6 @@ pub mod use_qwik_method_usage;
pub mod use_qwik_valid_lexical_scope;
pub mod use_react_function_components;
pub mod use_sorted_classes;
pub mod use_spread;
pub mod use_vue_multi_word_component_names;
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_floating_promises :: NoFloatingPromises , self :: no_import_cycles :: NoImportCycles , self :: no_jsx_literals :: NoJsxLiterals , self :: no_misused_promises :: NoMisusedPromises , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_non_null_asserted_optional_chain :: NoNonNullAssertedOptionalChain , self :: no_qwik_use_visible_task :: NoQwikUseVisibleTask , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_secrets :: NoSecrets , self :: no_shadow :: NoShadow , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_anchor_href :: UseAnchorHref , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_consistent_type_definitions :: UseConsistentTypeDefinitions , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_image_size :: UseImageSize , self :: use_max_params :: UseMaxParams , self :: use_qwik_classlist :: UseQwikClasslist , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_react_function_components :: UseReactFunctionComponents , self :: use_sorted_classes :: UseSortedClasses , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_floating_promises :: NoFloatingPromises , self :: no_import_cycles :: NoImportCycles , self :: no_jsx_literals :: NoJsxLiterals , self :: no_misused_promises :: NoMisusedPromises , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_non_null_asserted_optional_chain :: NoNonNullAssertedOptionalChain , self :: no_qwik_use_visible_task :: NoQwikUseVisibleTask , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_secrets :: NoSecrets , self :: no_shadow :: NoShadow , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_anchor_href :: UseAnchorHref , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_consistent_type_definitions :: UseConsistentTypeDefinitions , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_image_size :: UseImageSize , self :: use_max_params :: UseMaxParams , self :: use_qwik_classlist :: UseQwikClasslist , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_react_function_components :: UseReactFunctionComponents , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
168 changes: 168 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery/use_spread.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
use crate::JsRuleAction;
use biome_analyze::{
Ast, FixKind, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule,
};
use biome_console::markup;
use biome_js_factory::make;
use biome_js_syntax::{
AnyJsCallArgument, AnyJsExpression, AnyJsMemberExpression, JsCallExpression, JsLanguage, T,
};
use biome_rowan::{AstNode, AstSeparatedList, BatchMutationExt, SyntaxToken};
use biome_rule_options::use_spread::UseSpreadOptions;

declare_lint_rule! {
/// Enforce the use of the spread operator over `.apply()`.
///
/// The `apply()` method is used to call a function with a given `this` value and arguments provided as an array.
/// The spread operator `...` can be used to achieve the same result, which is more concise and easier to read.
///
/// ## Examples
///
/// ### Invalid
///
/// ```js,expect_diagnostic
/// foo.apply(null, args);
/// ```
///
/// ```js,expect_diagnostic
/// foo.apply(null, [1, 2, 3]);
/// ```
///
/// ```js,expect_diagnostic
/// foo.apply(undefined, args);
/// ```
///
/// ```js,expect_diagnostic
/// obj.foo.apply(obj, args);
/// ```
///
/// ### Valid
///
/// ```js
/// foo(...args);
///
/// obj.foo(...args);
///
/// foo.apply(obj, [1, 2, 3]);
///
/// ```
///
pub UseSpread {
version: "next",
name: "useSpread",
language: "js",
sources: &[RuleSource::Eslint("prefer-spread").same()],
recommended: true,
fix_kind: FixKind::Unsafe,
}
}

impl Rule for UseSpread {
type Query = Ast<JsCallExpression>;
type State = (AnyJsExpression, AnyJsExpression);
type Signals = Option<Self::State>;
type Options = UseSpreadOptions;

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let node = ctx.query();
let callee = node.callee().ok()?;

let member_expr = AnyJsMemberExpression::cast_ref(callee.syntax())?;
if member_expr.member_name()?.text() != "apply" {
return None;
}

let arguments = node.arguments().ok()?.args();
if arguments.len() != 2 {
return None;
}

let first_arg = arguments.first()?.ok()?;
let this_arg = first_arg.as_any_js_expression()?;

let second_arg = arguments.last()?.ok()?;
let spread_candidate = second_arg.as_any_js_expression()?;

let applied_object = member_expr.object().ok()?;

let is_same_reference = if let Some(object_member) =
AnyJsMemberExpression::cast(applied_object.clone().into_syntax())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use cast_ref

{
are_nodes_equal(&object_member.object().ok()?, this_arg)
} else {
// This handles cases like `foo.apply(null, args)` or `foo.apply(undefined, args)`
this_arg
.as_static_value()
.is_some_and(|v| v.is_null_or_undefined())
};

if is_same_reference {
Some((applied_object.clone(), spread_candidate.clone()))
} else {
None
}
}

fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
let node = ctx.query();
Some(RuleDiagnostic::new(
rule_category!(),
node.range(),
markup! {
<Emphasis>"apply()"</Emphasis>" is used to call a function with arguments provided as an array."
},
))
}

fn action(ctx: &RuleContext<Self>, state: &Self::State) -> Option<JsRuleAction> {
let node = ctx.query();
let mut mutation = ctx.root().begin();

let (object, spread_candidate) = state;

let new_arguments = make::js_call_arguments(
make::token(T!['(']),
make::js_call_argument_list(
[AnyJsCallArgument::from(make::js_spread(
make::token(T![...]),
spread_candidate.clone(),
))],
[],
),
make::token(T![')']),
);

let new_call_expression = make::js_call_expression(object.clone(), new_arguments).build();

mutation.replace_node(node.clone(), new_call_expression);

Some(JsRuleAction::new(
ctx.metadata().action_category(ctx.category(), ctx.group()),
ctx.metadata().applicability(),
markup! { "Use the spread operator." }.to_owned(),
mutation,
))
}
}

fn get_identifier_token(node: &AnyJsExpression) -> Option<SyntaxToken<JsLanguage>> {
match node {
AnyJsExpression::JsIdentifierExpression(identifier) => identifier
.name()
.ok()
.and_then(|name| name.value_token().ok()),
_ => None,
}
}

fn are_nodes_equal(node1: &AnyJsExpression, node2: &AnyJsExpression) -> bool {
let object_token = get_identifier_token(node1);
let this_token = get_identifier_token(node2);

match (object_token, this_token) {
(Some(object_token), Some(this_token)) => {
object_token.text_trimmed() == this_token.text_trimmed()
}
_ => false,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// should generate diagnostics

foo.apply(null, args);
foo.apply(null, [1, 2, 3]);
foo.apply(undefined, args);
obj.foo.apply(obj, args);
Comment on lines +3 to +6
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

❓ Verification inconclusive

Test coverage looks solid.

The four test cases cover the main patterns: null/undefined as the this argument, array literals, and same-object references. These align well with the rule's intended diagnostics.

Since a past review mentioned parentheses breaking detection (supposedly addressed in 6cbc9ad), consider verifying that edge cases like obj.foo.apply((obj), args) or (foo).apply(null, args) are handled correctly—either flagged here or covered in a separate test file.


🏁 Script executed:

#!/bin/bash
# Description: Check if parenthesized expression test cases exist
# Expected: Find test cases with parentheses in apply calls

rg -n -A2 -B2 '\(.*\)\.apply' crates/biome_js_analyze/tests/specs/nursery/useSpread/

Length of output: 0


Add tests for parenthesized apply calls

No existing specs cover calls like obj.foo.apply((obj), args) or (foo).apply(null, args). Please include these invalid cases to ensure parentheses detection remains correct.

🤖 Prompt for AI Agents
In crates/biome_js_analyze/tests/specs/nursery/useSpread/invalid.js around lines
2 to 5, add test cases that cover parenthesized callee expressions for apply
(e.g., (foo).apply(null, args), obj.foo.apply((obj), args), (obj.foo).apply(obj,
args)) so the suite includes invalid calls with parentheses; update the file by
inserting these parenthesized variants alongside the existing cases to ensure
the parser/validator rejects them the same as their non-parenthesized
counterparts.

108 changes: 108 additions & 0 deletions crates/biome_js_analyze/tests/specs/nursery/useSpread/invalid.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: invalid.js
---
# Input
```js
// should generate diagnostics

foo.apply(null, args);
foo.apply(null, [1, 2, 3]);
foo.apply(undefined, args);
obj.foo.apply(obj, args);

```

# Diagnostics
```
invalid.js:3:1 lint/nursery/useSpread FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i apply() is used to call a function with arguments provided as an array.

1 │ // should generate diagnostics
2 │
> 3 │ foo.apply(null, args);
│ ^^^^^^^^^^^^^^^^^^^^^
4 │ foo.apply(null, [1, 2, 3]);
5 │ foo.apply(undefined, args);

i Unsafe fix: Use the spread operator.

1 1 │ // should generate diagnostics
2 2 │
3 │ - foo.apply(null,·args);
3 │ + foo(...args);
4 4 │ foo.apply(null, [1, 2, 3]);
5 5 │ foo.apply(undefined, args);


```

```
invalid.js:4:1 lint/nursery/useSpread FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i apply() is used to call a function with arguments provided as an array.

3 │ foo.apply(null, args);
> 4 │ foo.apply(null, [1, 2, 3]);
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^
5 │ foo.apply(undefined, args);
6 │ obj.foo.apply(obj, args);

i Unsafe fix: Use the spread operator.

2 2 │
3 3 │ foo.apply(null, args);
4 │ - foo.apply(null,·[1,·2,·3]);
4 │ + foo(...[1,·2,·3]);
5 5 │ foo.apply(undefined, args);
6 6 │ obj.foo.apply(obj, args);


```

```
invalid.js:5:1 lint/nursery/useSpread FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i apply() is used to call a function with arguments provided as an array.

3 │ foo.apply(null, args);
4 │ foo.apply(null, [1, 2, 3]);
> 5 │ foo.apply(undefined, args);
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^
6 │ obj.foo.apply(obj, args);
7 │

i Unsafe fix: Use the spread operator.

3 3 │ foo.apply(null, args);
4 4 │ foo.apply(null, [1, 2, 3]);
5 │ - foo.apply(undefined,·args);
5 │ + foo(...args);
6 6 │ obj.foo.apply(obj, args);
7 7 │


```

```
invalid.js:6:1 lint/nursery/useSpread FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i apply() is used to call a function with arguments provided as an array.

4 │ foo.apply(null, [1, 2, 3]);
5 │ foo.apply(undefined, args);
> 6 │ obj.foo.apply(obj, args);
│ ^^^^^^^^^^^^^^^^^^^^^^^^
7 │

i Unsafe fix: Use the spread operator.

4 4 │ foo.apply(null, [1, 2, 3]);
5 5 │ foo.apply(undefined, args);
6 │ - obj.foo.apply(obj,·args);
6 │ + obj.foo(...args);
7 7 │


```
10 changes: 10 additions & 0 deletions crates/biome_js_analyze/tests/specs/nursery/useSpread/valid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* should not generate diagnostics */

// The `this` argument is a specific object, which is not the same as the callee's object.
foo.apply(obj, [1, 2, 3]);
foo.apply(obj, args);
obj.foo.apply(bar, args);

// The number of arguments is not 2.
foo.apply(null);
foo.apply();
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: valid.js
---
# Input
```js
/* should not generate diagnostics */

// The `this` argument is a specific object, which is not the same as the callee's object.
foo.apply(obj, [1, 2, 3]);
foo.apply(obj, args);
obj.foo.apply(bar, args);

// The number of arguments is not 2.
foo.apply(null);
foo.apply();

```
1 change: 1 addition & 0 deletions crates/biome_rule_options/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ pub mod use_sorted_attributes;
pub mod use_sorted_classes;
pub mod use_sorted_keys;
pub mod use_sorted_properties;
pub mod use_spread;
pub mod use_static_response_methods;
pub mod use_strict_mode;
pub mod use_symbol_description;
Expand Down
6 changes: 6 additions & 0 deletions crates/biome_rule_options/src/use_spread.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
use biome_deserialize_macros::Deserializable;
use serde::{Deserialize, Serialize};
#[derive(Default, Clone, Debug, Deserialize, Deserializable, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields, default)]
pub struct UseSpreadOptions {}
Loading
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载