Skip to content

feat(lint): add nursery rule useIncludes#9876

Open
muratclk wants to merge 1 commit intobiomejs:mainfrom
muratclk:feat/use-includes
Open

feat(lint): add nursery rule useIncludes#9876
muratclk wants to merge 1 commit intobiomejs:mainfrom
muratclk:feat/use-includes

Conversation

@muratclk
Copy link
Copy Markdown

@muratclk muratclk commented Apr 9, 2026

Summary

Port of prefer-includes from typescript-eslint.

This adds a new nursery lint rule useIncludes that enforces the use of .includes() over .indexOf() comparisons when checking for the presence of a value in a string or array.

Detected patterns

Presence checks (replaced with x.includes(y)):

  • x.indexOf(y) !== -1
  • x.indexOf(y) != -1
  • x.indexOf(y) >= 0
  • x.indexOf(y) > -1

Absence checks (replaced with !x.includes(y)):

  • x.indexOf(y) === -1
  • x.indexOf(y) == -1
  • x.indexOf(y) < 0

Also handles reversed operands and parenthesized expressions.

Closes #7654

Test plan

  • Added invalid.js with all detected patterns including reversed operands and parenthesized expressions
  • Added valid.js with cases that should not trigger (direct .includes(), positional .indexOf(), different comparisons, different methods)

Port prefer-includes from typescript-eslint. The rule suggests using
.includes() over .indexOf() comparisons when checking for the presence
of a value in a string or array.
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 9, 2026

🦋 Changeset detected

Latest commit: 12409fd

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-Linter Area: linter L-JavaScript Language: JavaScript and super languages labels Apr 9, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 9, 2026

Walkthrough

This pull request introduces the useIncludes lint rule for JavaScript, porting the prefer-includes rule from typescript-eslint. The implementation detects indexOf() comparisons (e.g., indexOf(...) !== -1) and proposes replacing them with includes(). Changes include the core rule implementation with diagnostic emission and fix generation, accompanying test fixtures covering both valid and invalid patterns, rule option structures, and a changeset announcement for the new nursery rule.

Suggested labels

A-Linter, L-JavaScript, A-Diagnostic

Suggested reviewers

  • ematipico
  • dyc3
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding a new nursery lint rule called useIncludes.
Description check ✅ Passed The description is well-structured and directly related to the changeset, explaining the rule's purpose, detected patterns, and test coverage.
Linked Issues check ✅ Passed The PR successfully implements the typescript-eslint prefer-includes rule port (#7654) with all required pattern detection for presence/absence checks, handling reversed operands and parenthesized expressions.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the useIncludes rule: new lint rule implementation, test fixtures, rule options, and documentation—no extraneous modifications present.

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

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

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

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: 1

🤖 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/use_includes.rs`:
- Line 65: The RuleSource entry currently uses
RuleSource::EslintTypeScript("prefer-includes") without the parity marker;
update the rule definition to use
RuleSource::EslintTypeScript("prefer-includes").same() so it signals identical
behavior to the upstream rule (modify the occurrence of
RuleSource::EslintTypeScript("prefer-includes") in the use_includes rule).
🪄 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: 206da4c5-9ad0-47ec-8ff8-f8a1ca4c2469

📥 Commits

Reviewing files that changed from the base of the PR and between ccf9770 and 12409fd.

📒 Files selected for processing (6)
  • .changeset/add-use-includes.md
  • crates/biome_js_analyze/src/lint/nursery/use_includes.rs
  • crates/biome_js_analyze/tests/specs/nursery/useIncludes/invalid.js
  • crates/biome_js_analyze/tests/specs/nursery/useIncludes/valid.js
  • crates/biome_rule_options/src/lib.rs
  • crates/biome_rule_options/src/use_includes.rs

name: "useIncludes",
language: "js",
recommended: false,
sources: &[RuleSource::EslintTypeScript("prefer-includes")],
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how existing ESLint / typescript-eslint ports encode source parity.
sed -n '60,67p' crates/biome_js_analyze/src/lint/nursery/use_includes.rs
rg -n -C2 'RuleSource::Eslint(TypeScript)?\(' crates/biome_js_analyze/src/lint --glob '*.rs'

Repository: biomejs/biome

Length of output: 1768


🏁 Script executed:

#!/bin/bash
# View the current rule metadata at lines 60-67
echo "=== Current rule metadata (lines 60-67) ==="
sed -n '60,67p' crates/biome_js_analyze/src/lint/nursery/use_includes.rs

# Search for examples of .same() usage with ESLint sources
echo -e "\n=== Examples of .same() usage with ESLint sources ==="
rg -A1 'RuleSource::Eslint.*\.same\(' crates/biome_js_analyze/src/lint --max-count 10

# Search for other Eslint*TypeScript sources to see the pattern
echo -e "\n=== Examples of EslintTypeScript sources ==="
rg -A1 'RuleSource::EslintTypeScript' crates/biome_js_analyze/src/lint --max-count 5

Repository: biomejs/biome

Length of output: 50369


Add .same() to indicate matching behaviour with the upstream rule.

The rule currently references RuleSource::EslintTypeScript("prefer-includes") without a parity marker. Based on the consistently applied pattern across the codebase (and confirmed in similar rules like use_find, use_namespace_keyword, and use_regexp_exec), add .same() to signal that this is a faithful port:

Diff
-        sources: &[RuleSource::EslintTypeScript("prefer-includes")],
+        sources: &[RuleSource::EslintTypeScript("prefer-includes").same()],
📝 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
sources: &[RuleSource::EslintTypeScript("prefer-includes")],
sources: &[RuleSource::EslintTypeScript("prefer-includes").same()],
🤖 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/use_includes.rs` at line 65, The
RuleSource entry currently uses RuleSource::EslintTypeScript("prefer-includes")
without the parity marker; update the rule definition to use
RuleSource::EslintTypeScript("prefer-includes").same() so it signals identical
behavior to the upstream rule (modify the occurrence of
RuleSource::EslintTypeScript("prefer-includes") in the use_includes rule).

@Netail
Copy link
Copy Markdown
Member

Netail commented Apr 9, 2026

Good to know that eslint-plugin-unicorn provides a more powerful version: https://github.com/sindresorhus/eslint-plugin-unicorn/blob/HEAD/docs/rules/prefer-includes.md, which I think is more worth following.

Also there was another PR in progress #8115, but became a bit stale I think

Copy link
Copy Markdown
Member

@ematipico ematipico left a comment

Choose a reason for hiding this comment

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

Thank you for the contribution!

  • The code can be improved; I left some suggestions.
  • The diagnostics must follow our guidelines. We're very strict, please refer to the contribution guidelines of the analyzer
  • I believe you haven't run any tests, so the testing plan is a bit moot 😅 Please run the tests, inspect the snapshots and commit them
  • The current implementation of the rule differs from the source rule. Was that intentional?

name: "useIncludes",
language: "js",
recommended: false,
sources: &[RuleSource::EslintTypeScript("prefer-includes")],
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.

Suggested change
sources: &[RuleSource::EslintTypeScript("prefer-includes")],
sources: &[RuleSource::EslintTypeScript("prefer-includes").inspired()],

I chose inspired because this rule you implemented isn't the same as the eslint one. The one from eslint uses type information, while yours is simply using AST, which means it won't yield the same results.

By being aware of this, what's your plan?

Comment on lines +237 to +247
let mut current = node.parent()?;
loop {
if let Some(binary) = JsBinaryExpression::cast(current.clone()) {
return Some(binary);
}
if biome_js_syntax::JsParenthesizedExpression::can_cast(current.kind()) {
current = current.parent()?;
continue;
}
return None;
}
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.

This function can be simplified:

  • As a golden rule, never pass untyped nodes e.g. JsSyntaxNode
  • Second, there's a method called ancestors() that does what you need
  • You can use find_map to find the first occurrence you need
  • For parentheses and such, here's what I suggest you to do
     let expression = AnyJsExpression:cast(node)?;
     let expression = expression.omit_parenthesis()?;
     if let Some(binary) = expression.as_js_binary_expression() {
     	return Some(binary)
     }
    It's a bit simplified, but hopefully you understand what I mean


fn expression_matches_target(expression: &AnyJsExpression, target: &JsSyntaxNode) -> bool {
expression.clone().omit_parentheses().syntax().eq(target)
}
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.

Can you please add docstrings? It's very difficult to understand what kind of comparison you're looking for. Most of the time, this comparison isn't the intended one.

Plus target is untyped, so it's more difficult to understand what the intended behaviour is.

What I suggest:

  • add doc strings to explain what we're comparing (business logic)
  • target should be typed, so that you signal the intention

After I understand the intention, we can also properly address the equality operation

expression.clone().omit_parentheses().syntax().eq(target)
}

fn is_number_literal_value(expression: &AnyJsExpression, value: f64) -> bool {
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.

Suggested change
fn is_number_literal_value(expression: &AnyJsExpression, value: f64) -> bool {
fn matches_number_literal_value(expression: &AnyJsExpression, value: f64) -> bool {

.is_some_and(|number| number == value)
}

fn is_negative_one_literal(expression: &AnyJsExpression) -> bool {
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.

Docs string that explains the business logic. There are some magic numbers that require explanation

Comment on lines +270 to +273
let unary = match expression.as_js_unary_expression() {
Some(unary) => unary,
None => return false,
};
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.

Suggested change
let unary = match expression.as_js_unary_expression() {
Some(unary) => unary,
None => return false,
};
let Some(unary) = expression.as_js_unary_expression() else {
return false,
};

}
}

fn call_method_name(call: &JsCallExpression) -> Option<String> {
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.

Suggested change
fn call_method_name(call: &JsCallExpression) -> Option<String> {
fn call_method_name(call: &JsCallExpression) -> Option<TokenText> {

let member = callee.as_js_static_member_expression()?;
let member_name = member.member().ok()?;
let name = member_name.as_js_name()?;
Some(name.value_token().ok()?.text_trimmed().to_string())
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.

Suggested change
Some(name.value_token().ok()?.text_trimmed().to_string())
Some(name.value_token().ok()?.text_trimmed())

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.

Where are the snapshots? Did you run the tests? 😅

Comment on lines +185 to +191
markup! {
"Use "<Emphasis>".includes()"</Emphasis>" rather than "<Emphasis>".indexOf()"</Emphasis>" to check for the presence of a value."
},
)
.note(markup! {
<Emphasis>".includes()"</Emphasis>" is more readable and expressive than comparing "<Emphasis>".indexOf()"</Emphasis>" against "<Emphasis>"-1"</Emphasis>"."
}),
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.

Our contribution guidelines for messages and diagnostics are quite strict and follow a structured framework. Can you please address that? https://biomejs.dev/linter/#rule-pillars

@ematipico
Copy link
Copy Markdown
Member

Good to know that eslint-plugin-unicorn provides a more powerful version: sindresorhus/eslint-plugin-unicorn@HEAD/docs/rules/prefer-includes.md, which I think is more worth following.

Also there was another PR in progress #8115, but became a bit stale I think

Is that more powerful, though? The TS eslint rule leverages the type system...

@Netail
Copy link
Copy Markdown
Member

Netail commented Apr 9, 2026

Good to know that eslint-plugin-unicorn provides a more powerful version: sindresorhus/eslint-plugin-unicorn@HEAD/docs/rules/prefer-includes.md, which I think is more worth following.
Also there was another PR in progress #8115, but became a bit stale I think

Is that more powerful, though? The TS eslint rule leverages the type system...

As in wider range, it also covers .lastIndexOf() & certain Array#some() cases

@dyc3
Copy link
Copy Markdown
Contributor

dyc3 commented Apr 14, 2026

@muratclk still interested in this PR?

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

Labels

A-Linter Area: linter L-JavaScript Language: JavaScript and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

📎 Port prefer-includes from typescript-eslint

4 participants