Skip to content

feat(lint): add noUselessCall rule#10237

Open
realknove wants to merge 3 commits into
biomejs:mainfrom
realknove:feat/no-useless-call
Open

feat(lint): add noUselessCall rule#10237
realknove wants to merge 3 commits into
biomejs:mainfrom
realknove:feat/no-useless-call

Conversation

@realknove
Copy link
Copy Markdown
Contributor

Summary

Implements the new nursery lint rule noUselessCall, which ports ESLint's no-useless-call rule to Biome.

This rule reports unnecessary uses of .call() and .apply() when they can be replaced with a normal function call without changing the this binding or argument semantics.

Examples reported by this rule include:

js
foo.call(undefined, 1, 2);
foo.call(null, 1, 2);
foo.apply(undefined, [1, 2]);
foo.apply(null, [1, 2]);

obj.foo.call(obj, 1, 2);
obj.foo.apply(obj, [1, 2]);

The implementation is conservative and avoids reporting cases where replacing .call() or .apply() could change behavior, such as calls with a different receiver or .apply() calls whose second argument is not a static array expression.

Closes #10187.

Test Plan

  • Added valid and invalid fixtures for noUselessCall covering:
  • unnecessary .call() with undefined or null as the receiver;
  • unnecessary .apply() with undefined or null and a static array argument;
  • unnecessary member calls such as obj.foo.call(obj, ...);
  • valid cases where the receiver differs;
  • valid .apply() cases using a dynamic argument list;
  • missing or incomplete .call() / .apply() arguments.

Generated snapshots for the new rule.

Commands run:

cargo test -p biome_js_analyze no_useless_call

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 4, 2026

🦋 Changeset detected

Latest commit: a3fdc82

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
Name Type
@biomejs/biome Patch
@biomejs/cli-win32-x64 Patch
@biomejs/cli-win32-arm64 Patch
@biomejs/cli-darwin-x64 Patch
@biomejs/cli-darwin-arm64 Patch
@biomejs/cli-linux-x64 Patch
@biomejs/cli-linux-arm64 Patch
@biomejs/cli-linux-x64-musl Patch
@biomejs/cli-linux-arm64-musl Patch
@biomejs/wasm-web Patch
@biomejs/wasm-bundler Patch
@biomejs/wasm-nodejs Patch
@biomejs/backend-jsonrpc Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added A-Project Area: project A-Linter Area: linter L-JavaScript Language: JavaScript and super languages A-Diagnostic Area: diagnostocis labels May 4, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 4, 2026

Walkthrough

Adds the noUselessCall JavaScript lint rule to the analyzer, detecting unnecessary Function.prototype.call() and Function.prototype.apply() uses when the this receiver is statically the same or nullish. Implements rule logic, public state types (UselessCallState, CallMethod), options struct, test fixtures for valid and invalid cases, and a changeset entry declaring a patch release for @biomejs/biome.

Suggested reviewers

  • dyc3
  • Netail
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the primary change: adding the noUselessCall lint rule to Biome.
Description check ✅ Passed The description clearly explains the rule implementation, examples of what it reports, and test coverage related to the changeset.
Linked Issues check ✅ Passed The PR successfully implements all coding requirements from issue #10187: the noUselessCall rule detects unnecessary .call() and .apply() uses conservatively, includes proper test fixtures covering valid and invalid cases, and follows Biome's analyzer practices.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the noUselessCall rule: the lint rule implementation, rule options, test fixtures, and module exports are all within scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.changeset/add-no-useless-call.md:
- Line 5: Update the changeset sentence to include the docs link for the rule by
replacing the plain rule name with the required markdown link format;
specifically change the mention of noUselessCall to
[`noUselessCall`](https://biomejs.dev/linter/rules/no-useless-call/) in
.changeset/add-no-useless-call.md so it follows the "Include links to rule
documentation" guideline.

In `@crates/biome_js_analyze/src/lint/nursery/no_useless_call.rs`:
- Around line 159-173: The function are_same_simple_reference currently only
matches identifier expressions; add a new match arm to treat this expressions as
equal by matching (AnyJsExpression::JsThisExpression(_),
AnyJsExpression::JsThisExpression(_)) => true before the existing
AnyJsExpression::JsIdentifierExpression arm so calls like this.foo.call(this,
...) and this.foo.apply(this, [...]) are recognized; also add test cases to
invalid.js for this.foo.call(this, 1) and this.foo.apply(this, [1]) to cover the
regressions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cecc3576-19be-4abc-b612-7318e97c03c0

📥 Commits

Reviewing files that changed from the base of the PR and between 8c849e2 and 4f2d645.

⛔ Files ignored due to path filters (6)
  • crates/biome_configuration/src/analyzer/linter/rules.rs is excluded by !**/rules.rs and included by **
  • crates/biome_configuration/src/generated/linter_options_check.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_diagnostics_categories/src/categories.rs is excluded by !**/categories.rs and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noUselessCall/invalid.js.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noUselessCall/valid.js.snap is excluded by !**/*.snap and included by **
  • packages/@biomejs/biome/configuration_schema.json is excluded by !**/configuration_schema.json and included by **
📒 Files selected for processing (6)
  • .changeset/add-no-useless-call.md
  • crates/biome_js_analyze/src/lint/nursery/no_useless_call.rs
  • crates/biome_js_analyze/tests/specs/nursery/noUselessCall/invalid.js
  • crates/biome_js_analyze/tests/specs/nursery/noUselessCall/valid.js
  • crates/biome_rule_options/src/lib.rs
  • crates/biome_rule_options/src/no_useless_call.rs

"@biomejs/biome": patch
---

Fixed [#10187](https://github.com/biomejs/biome/issues/10187): Added the `noUselessCall` lint rule to report unnecessary uses of `Function.prototype.call()` and `Function.prototype.apply()`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add the rule docs link in the changeset sentence.

Line 5 references the rule name but misses the required website link format. Tiny docs nit, big changelog win.

Suggested edit
-Fixed [`#10187`](https://github.com/biomejs/biome/issues/10187): Added the `noUselessCall` lint rule to report unnecessary uses of `Function.prototype.call()` and `Function.prototype.apply()`.
+Fixed [`#10187`](https://github.com/biomejs/biome/issues/10187): Added the [`noUselessCall`](https://biomejs.dev/linter/rules/no-useless-call/) lint rule to report unnecessary uses of `Function.prototype.call()` and `Function.prototype.apply()`.

As per coding guidelines: "Include links to rule documentation on the website when referencing rules in changesets, formatted as: ruleName".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Fixed [#10187](https://github.com/biomejs/biome/issues/10187): Added the `noUselessCall` lint rule to report unnecessary uses of `Function.prototype.call()` and `Function.prototype.apply()`.
Fixed [`#10187`](https://github.com/biomejs/biome/issues/10187): Added the [`noUselessCall`](https://biomejs.dev/linter/rules/no-useless-call/) lint rule to report unnecessary uses of `Function.prototype.call()` and `Function.prototype.apply()`.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.changeset/add-no-useless-call.md at line 5, Update the changeset sentence
to include the docs link for the rule by replacing the plain rule name with the
required markdown link format; specifically change the mention of noUselessCall
to [`noUselessCall`](https://biomejs.dev/linter/rules/no-useless-call/) in
.changeset/add-no-useless-call.md so it follows the "Include links to rule
documentation" guideline.

Comment on lines +159 to +173
fn are_same_simple_reference(left: &AnyJsExpression, right: &AnyJsExpression) -> bool {
match (left.clone().omit_parentheses(), right.clone().omit_parentheses()) {
(AnyJsExpression::JsIdentifierExpression(left), AnyJsExpression::JsIdentifierExpression(right)) => {
left.name()
.ok()
.zip(right.name().ok())
.is_some_and(|(left, right)| {
left.value_token()
.ok()
.zip(right.value_token().ok())
.is_some_and(|(left, right)| left.text_trimmed() == right.text_trimmed())
})
}
_ => false,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does ESLint's no-useless-callreportthis.foo.call(this, 1)andthis.foo.apply(this, [1]) as violations?

💡 Result:

Yes. ESLint’s no-useless-call rule reports both this.foo.call(this, 1) and this.foo.apply(this, [1]) as violations, because they match the rule’s “same as obj.foo(...);” pattern where the this value passed to .call/.apply is the same receiver (thisArg equals the method’s owning object). ESLint’s rule documentation shows these as incorrect examples: - obj.foo.call(obj, 1, 2, 3); - obj.foo.apply(obj, [1, 2, 3]); Substituting obj -> this and the arguments accordingly yields the reported violations for this.foo.call(this, 1) and this.foo.apply(this, [1]).

Citations:


🏁 Script executed:

# First, let's look at the current implementation and context
cd crates/biome_js_analyze && cat -n src/lint/nursery/no_useless_call.rs | sed -n '150,180p'

Repository: biomejs/biome

Length of output: 1168


🏁 Script executed:

# Check if there are test files for this rule
find . -path "*/tests/*no_useless_call*" -type f | head -20

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

# Look for the invalid.js fixture file
find . -path "*/no_useless_call/*" -name "invalid.js" -o -path "*/no_useless_call/*" -name "*.js" | head -10

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

# Search for test files related to no_useless_call
find . -type f -name "*.js" | xargs grep -l "no_useless_call" 2>/dev/null | head -20

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

# Look for the specs directory and this rule
find . -type d -name "specs" | xargs find 2>/dev/null | grep -i useless_call | head -20

Repository: biomejs/biome

Length of output: 39


🏁 Script executed:

# Try a broader search for invalid.js files in test directories
find . -path "*/tests/*" -name "invalid.js" 2>/dev/null | xargs grep -l "call\|apply" 2>/dev/null | head -10

Repository: biomejs/biome

Length of output: 788


🏁 Script executed:

# Read the invalid.js test file for noUselessCall
cat -n crates/biome_js_analyze/tests/specs/nursery/noUselessCall/invalid.js

Repository: biomejs/biome

Length of output: 328


🏁 Script executed:

# Look at how are_same_simple_reference is used in the file
cat -n crates/biome_js_analyze/src/lint/nursery/no_useless_call.rs | grep -B5 -A5 "are_same_simple_reference"

Repository: biomejs/biome

Length of output: 1023


🏁 Script executed:

# Check the valid.js file as well
cat crates/biome_js_analyze/tests/specs/nursery/noUselessCall/valid.js

Repository: biomejs/biome

Length of output: 504


Add JsThisExpression to are_same_simple_reference to catch this.foo.call(this, …) and this.foo.apply(this, […]) patterns.

The function only handles identifier comparisons, so this expressions always fail to match. ESLint's no-useless-call rule reports these as violations. Add a match arm for (AnyJsExpression::JsThisExpression(_), AnyJsExpression::JsThisExpression(_)) => true before the identifier case, and add test cases to invalid.js for this.foo.call(this, 1) and this.foo.apply(this, [1]).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_analyze/src/lint/nursery/no_useless_call.rs` around lines 159
- 173, The function are_same_simple_reference currently only matches identifier
expressions; add a new match arm to treat this expressions as equal by matching
(AnyJsExpression::JsThisExpression(_), AnyJsExpression::JsThisExpression(_)) =>
true before the existing AnyJsExpression::JsIdentifierExpression arm so calls
like this.foo.call(this, ...) and this.foo.apply(this, [...]) are recognized;
also add test cases to invalid.js for this.foo.call(this, 1) and
this.foo.apply(this, [1]) to cover the regressions.

@github-actions github-actions Bot added the A-CLI Area: CLI label May 4, 2026
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 4, 2026

Merging this PR will not alter performance

✅ 59 untouched benchmarks
⏩ 195 skipped benchmarks1


Comparing realknove:feat/no-useless-call (a3fdc82) with main (183c8fa)

Open in CodSpeed

Footnotes

  1. 195 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

rule_category!(),
call.range(),
markup! {
"This "<Emphasis>"."{method}"()"</Emphasis>" is unnecessary."
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Insufficient diagnostics, please follow the 3 pillars. The Why and What the user should do are missing

https://github.com/biomejs/biome/blob/main/crates/biome_analyze/CONTRIBUTING.md#what-a-rule-should-say-to-the-user

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
crates/biome_js_analyze/src/lint/nursery/no_useless_call.rs (1)

188-202: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

are_same_simple_reference silently drops this expressions, so this.foo.call(this, …) is never flagged.

The wildcard arm (_ => false) swallows every non-identifier pair, including two JsThisExpression nodes that are trivially equal. Adding a single match arm restores parity with ESLint's no-useless-call.

🐛 Proposed fix
 fn are_same_simple_reference(left: &AnyJsExpression, right: &AnyJsExpression) -> bool {
     match (left.clone().omit_parentheses(), right.clone().omit_parentheses()) {
+        (AnyJsExpression::JsThisExpression(_), AnyJsExpression::JsThisExpression(_)) => true,
         (AnyJsExpression::JsIdentifierExpression(left), AnyJsExpression::JsIdentifierExpression(right)) => {
             left.name()
                 .ok()
                 .zip(right.name().ok())
                 .is_some_and(|(left, right)| {
                     left.value_token()
                         .ok()
                         .zip(right.value_token().ok())
                         .is_some_and(|(left, right)| left.text_trimmed() == right.text_trimmed())
                 })
         }
         _ => false,
     }
 }

Don't forget to add this.foo.call(this, 1) and this.foo.apply(this, [1]) to invalid.js.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_analyze/src/lint/nursery/no_useless_call.rs` around lines 188
- 202, The helper are_same_simple_reference currently returns false for any
non-identifier pairs because the wildcard arm swallows cases like
JsThisExpression, so calls like this.foo.call(this, …) are not detected; add a
match arm to treat two JsThisExpression operands as equal (i.e., return true
when both left and right are AnyJsExpression::JsThisExpression after
omit_parentheses) and keep the existing identifier comparison intact in
are_same_simple_reference, then add test cases this.foo.call(this, 1) and
this.foo.apply(this, [1]) to invalid.js to cover these paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_js_analyze/src/lint/nursery/no_useless_call.rs`:
- Around line 86-88: The rule currently early-returns for CallMethod::Apply
whenever has_array_argument(call)? yields Some(false), which incorrectly skips
cases like foo.apply(null) (one argument) that should be treated like
foo.call(null); change the logic in no_useless_call.rs so that when method ==
CallMethod::Apply and has_array_argument(call)? == Some(false) you only return
None for calls that are NOT the one-argument form (i.e., check
call.arguments.len() and allow len == 1 to continue); reference
CallMethod::Apply, has_array_argument(call), and the call.arguments length check
when implementing this conditional.

---

Duplicate comments:
In `@crates/biome_js_analyze/src/lint/nursery/no_useless_call.rs`:
- Around line 188-202: The helper are_same_simple_reference currently returns
false for any non-identifier pairs because the wildcard arm swallows cases like
JsThisExpression, so calls like this.foo.call(this, …) are not detected; add a
match arm to treat two JsThisExpression operands as equal (i.e., return true
when both left and right are AnyJsExpression::JsThisExpression after
omit_parentheses) and keep the existing identifier comparison intact in
are_same_simple_reference, then add test cases this.foo.call(this, 1) and
this.foo.apply(this, [1]) to invalid.js to cover these paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d0525cb6-8079-4fb9-b9bf-13fdbdda6797

📥 Commits

Reviewing files that changed from the base of the PR and between 4f2d645 and a3fdc82.

⛔ Files ignored due to path filters (3)
  • crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs is excluded by !**/migrate/eslint_any_rule_to_biome.rs and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noUselessCall/invalid.js.snap is excluded by !**/*.snap and included by **
  • packages/@biomejs/backend-jsonrpc/src/workspace.ts is excluded by !**/backend-jsonrpc/src/workspace.ts and included by **
📒 Files selected for processing (1)
  • crates/biome_js_analyze/src/lint/nursery/no_useless_call.rs

Comment on lines +86 to +88
if method == CallMethod::Apply && !has_array_argument(call)? {
return None;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

foo.apply(null) / foo.apply(undefined) (no second argument) is silently skipped, unlike the equivalent .call() forms.

has_array_argument returns Some(false) when the argument count is not exactly 2, causing the early return None. However, foo.apply(null) with no spread argument is semantically identical to foo(), just like foo.call(null) — and the .call path does flag this. foo.apply(undefined, args) is listed as valid (dynamic args), but ESLint's rule is otherwise aimed at statically-decidable useless calls. Allowing the 1-arg .apply() path through closes the inconsistency:

🐛 Proposed fix
-        if method == CallMethod::Apply && !has_array_argument(call)? {
-            return None;
-        }
+        if method == CallMethod::Apply {
+            let apply_arg_count = call.arguments().ok()?.args().len();
+            // 1-arg .apply(thisArg) is equivalent to .call(thisArg) — allow it through.
+            // 2-arg .apply() is only flaggable when the second arg is a static array.
+            // Any other arity is too dynamic to analyse safely.
+            if apply_arg_count == 2 && !has_array_argument(call)? {
+                return None;
+            } else if apply_arg_count > 2 {
+                return None;
+            }
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_analyze/src/lint/nursery/no_useless_call.rs` around lines 86
- 88, The rule currently early-returns for CallMethod::Apply whenever
has_array_argument(call)? yields Some(false), which incorrectly skips cases like
foo.apply(null) (one argument) that should be treated like foo.call(null);
change the logic in no_useless_call.rs so that when method == CallMethod::Apply
and has_array_argument(call)? == Some(false) you only return None for calls that
are NOT the one-argument form (i.e., check call.arguments.len() and allow len ==
1 to continue); reference CallMethod::Apply, has_array_argument(call), and the
call.arguments length check when implementing this conditional.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-CLI Area: CLI A-Diagnostic Area: diagnostocis A-Linter Area: linter A-Project Area: project L-JavaScript Language: JavaScript and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

📎 Port no-useless-call from eslint

2 participants