Skip to content

fix(linter): handle re-ordered index in template string for noArrayIndexKey#9237

Closed
slegarraga wants to merge 1 commit intobiomejs:mainfrom
slegarraga:fix/no-array-index-key-template
Closed

fix(linter): handle re-ordered index in template string for noArrayIndexKey#9237
slegarraga wants to merge 1 commit intobiomejs:mainfrom
slegarraga:fix/no-array-index-key-template

Conversation

@slegarraga
Copy link

Summary

Fixes #8812

The noArrayIndexKey rule had a false negative when the array index appeared before other interpolations in a template literal. For example:

// ✅ Was correctly detected
<div key={`${item}-${index}`} />

// ❌ Was NOT detected (false negative)
<div key={`${index}-${item}`} />

Root cause

The template expression handler iterated over all template elements but kept overwriting capture_array_index with each one. Only the last interpolation was checked. When index appeared first, it was overwritten by item, which doesn't resolve to an array index parameter.

Fix

Instead of keeping only the last identifier, collect all identifier expressions from template elements and check each one against the array index parameter binding. The first match is used.

Test Plan

  • Added test case for ${index}-${item} ordering in invalid.jsx
  • All existing tests pass
  • Snapshot updated

…dexKey

Previously, the rule only detected array index usage in template literals when
the index appeared as the last interpolation (e.g. `${item}-${index}`).
When the index appeared earlier (e.g. `${index}-${item}`), it was missed
because each template element overwrote the previous candidate.

Now all identifier expressions in template literals are collected and each is
checked against the array index parameter binding, catching the false negative.

Closes biomejs#8812
@changeset-bot
Copy link

changeset-bot bot commented Feb 25, 2026

⚠️ No Changeset found

Latest commit: 1d58417

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@github-actions github-actions bot added A-Linter Area: linter L-JavaScript Language: JavaScript and super languages labels Feb 25, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 25, 2026

Walkthrough

This change refactors the noArrayIndexKey lint rule to improve detection accuracy. Rather than capturing a single reference, the rule now collects multiple candidate references from various expression forms (identifiers, template expressions, and binary expressions) and resolves them against array method contexts. The logic traverses candidate references to find the first one matching array index parameters in surrounding function calls. A test case is added to cover template string scenarios with reordered values and indices.

Suggested labels

A-Linter, L-JavaScript

Suggested reviewers

  • ematipico
  • dyc3
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly describes the fix: handling re-ordered index in template strings for the noArrayIndexKey linter rule.
Description check ✅ Passed Description is well-related to the changeset, explaining the false negative, root cause, and the implemented fix with test plan.
Linked Issues check ✅ Passed Changes directly address issue #8812 by collecting all template interpolations and checking each against array index binding, fixing the false negative.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing the noArrayIndexKey rule behaviour and adding corresponding test cases; no unrelated modifications detected.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
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/suspicious/no_array_index_key.rs`:
- Around line 172-177: cap_array_index_value only returns a single identifier,
so when handling AnyJsExpression::JsBinaryExpression you miss index identifiers
if other identifiers appear later; update the logic to collect all identifier
captures from the binary expression (not just one) and push each into
candidate_references. Concretely: change cap_array_index_value (or add a new
helper) to traverse the binary expression tree and return a Vec of captures
(instead of a single Option), then in the AnyJsExpression::JsBinaryExpression
arm iterate over those captures and push each reference into
candidate_references (references: AnyJsExpression::JsBinaryExpression,
cap_array_index_value, capture_array_index, candidate_references).

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1d2ca15 and 1d58417.

⛔ Files ignored due to path filters (1)
  • crates/biome_js_analyze/tests/specs/suspicious/noArrayIndexKey/invalid.jsx.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (2)
  • crates/biome_js_analyze/src/lint/suspicious/no_array_index_key.rs
  • crates/biome_js_analyze/tests/specs/suspicious/noArrayIndexKey/invalid.jsx

Comment on lines 172 to +177
AnyJsExpression::JsBinaryExpression(binary_expression) => {
let mut capture_array_index = None;
let _ = cap_array_index_value(&binary_expression, &mut capture_array_index);
if let Some(reference) = capture_array_index {
candidate_references.push(reference);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Binary-expression capture is still order-dependent.

cap_array_index_value returns only one identifier, so keys like index + "-" + item can still miss the index when another identifier appears later. That leaves a false-negative path similar to the one this PR fixes for template literals.

💡 Proposed patch
-            AnyJsExpression::JsBinaryExpression(binary_expression) => {
-                let mut capture_array_index = None;
-                let _ = cap_array_index_value(&binary_expression, &mut capture_array_index);
-                if let Some(reference) = capture_array_index {
-                    candidate_references.push(reference);
-                }
-            }
+            AnyJsExpression::JsBinaryExpression(binary_expression) => {
+                let mut captured_references = Vec::new();
+                let _ = cap_array_index_values(&binary_expression, &mut captured_references);
+                candidate_references.extend(captured_references);
+            }
-fn cap_array_index_value(
+fn cap_array_index_values(
     binary_expression: &JsBinaryExpression,
-    capture_array_index: &mut Option<JsReferenceIdentifier>,
+    captured_references: &mut Vec<JsReferenceIdentifier>,
 ) -> Option<()> {
@@
-    if let Some(left_binary) = left.as_js_binary_expression() {
-        cap_array_index_value(left_binary, capture_array_index);
+    if let Some(left_binary) = left.as_js_binary_expression() {
+        cap_array_index_values(left_binary, captured_references);
     };
@@
-    if let Some(right_binary) = right.as_js_binary_expression() {
-        cap_array_index_value(right_binary, capture_array_index);
+    if let Some(right_binary) = right.as_js_binary_expression() {
+        cap_array_index_values(right_binary, captured_references);
     };
@@
-    if let Some(left_expression) = left.as_js_identifier_expression() {
-        *capture_array_index = left_expression.name().ok();
+    if let Some(left_expression) = left.as_js_identifier_expression() {
+        if let Ok(reference) = left_expression.name() {
+            captured_references.push(reference);
+        }
     };
@@
-    if let Some(right_expression) = right.as_js_identifier_expression() {
-        *capture_array_index = right_expression.name().ok();
+    if let Some(right_expression) = right.as_js_identifier_expression() {
+        if let Ok(reference) = right_expression.name() {
+            captured_references.push(reference);
+        }
     };
📝 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
AnyJsExpression::JsBinaryExpression(binary_expression) => {
let mut capture_array_index = None;
let _ = cap_array_index_value(&binary_expression, &mut capture_array_index);
if let Some(reference) = capture_array_index {
candidate_references.push(reference);
}
AnyJsExpression::JsBinaryExpression(binary_expression) => {
let mut captured_references = Vec::new();
let _ = cap_array_index_values(&binary_expression, &mut captured_references);
candidate_references.extend(captured_references);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_analyze/src/lint/suspicious/no_array_index_key.rs` around
lines 172 - 177, cap_array_index_value only returns a single identifier, so when
handling AnyJsExpression::JsBinaryExpression you miss index identifiers if other
identifiers appear later; update the logic to collect all identifier captures
from the binary expression (not just one) and push each into
candidate_references. Concretely: change cap_array_index_value (or add a new
helper) to traverse the binary expression tree and return a Vec of captures
(instead of a single Option), then in the AnyJsExpression::JsBinaryExpression
arm iterate over those captures and push each reference into
candidate_references (references: AnyJsExpression::JsBinaryExpression,
cap_array_index_value, capture_array_index, candidate_references).

Copy link
Member

Choose a reason for hiding this comment

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

@slegarraga please address this comment. Is that correct?

Copy link
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.

Changeset is missing

let reference = node.as_js_expression()?;

let mut capture_array_index = None;
let mut candidate_references: Vec<JsReferenceIdentifier> = Vec::new();
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
let mut candidate_references: Vec<JsReferenceIdentifier> = Vec::new();
let mut candidate_references = Vec::new();

I think you don't need an explicit type

Comment on lines 172 to +177
AnyJsExpression::JsBinaryExpression(binary_expression) => {
let mut capture_array_index = None;
let _ = cap_array_index_value(&binary_expression, &mut capture_array_index);
if let Some(reference) = capture_array_index {
candidate_references.push(reference);
}
Copy link
Member

Choose a reason for hiding this comment

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

@slegarraga please address this comment. Is that correct?

.and_then(|arguments| arguments.parent::<JsCallExpression>())?;
is_array_method_index(&param, &call_expr)
})
.unwrap_or(false)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
.unwrap_or(false)
.unwrap_or_default()

@codspeed-hq
Copy link

codspeed-hq bot commented Feb 27, 2026

Merging this PR will not alter performance

✅ 58 untouched benchmarks
⏩ 156 skipped benchmarks1


Comparing slegarraga:fix/no-array-index-key-template (1d58417) with main (1d2ca15)

Open in CodSpeed

Footnotes

  1. 156 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.

@dyc3
Copy link
Contributor

dyc3 commented Feb 28, 2026

Closing in favor of #8968

@dyc3 dyc3 closed this Feb 28, 2026
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.

💅 lint/suspicious/noArrayIndexKey false negative when re-ordering value and index in template string

3 participants