Skip to content

feat: add correctness/each-index-key — flag {#each} blocks keyed by their index - #266

Merged
oekazuma merged 13 commits into
mainfrom
feat/each-index-key
Jul 22, 2026
Merged

oekazuma merged 13 commits into
mainfrom
feat/each-index-key

Conversation

@oekazuma

@oekazuma oekazuma commented Jul 22, 2026 •

Copy link
Copy Markdown
Owner

Summary

First of three rules from the Svelte best-practices survey (documentation/docs/07-misc/01-best-practices.md): the official guidance is explicit — "The key must uniquely identify the object. Do not use the index as a key" — but nothing in the toolchain surfaces it. correctness/each-index-key (warning) flags {#each items as item, i (i)}: an index key gives items position-based identity, the same failure mode as an unkeyed block (element state, focus, and transitions stick to positions on reorder/insert/remove), masked by a visible key that makes the block look safe.

Sister rule to correctness/each-key (unkeyed detection), kept separate for granular off/severity control. A block never triggers both.

How it works

collectEachBlocks sets an optional EachBlockFact.indexKey when the key expression — after satisfies/as unwrapping — is exactly the block's index binding. Deliberately conservative: composite keys containing the index ((item.id + '-' + i) adds uniqueness), wrapped forms (String(i)), no-index blocks, and itemless/constant-list blocks are never flagged. Rides the existing component channel; inline svelte-vitals-disable-next-line suppression works (verified empirically).

Review process

3 tasks via subagent-driven development with per-task reviews, then a final adversarial whole-branch review running 21 empirical probes against the built dist (nested/shadowed indexes, snippets, await blocks, TS wrappers, parenthesized keys, suppression, mutual exclusivity with each-key) — all behaved per spec; READY TO MERGE with zero findings.

Verification

  • pnpm build / pnpm typecheck / pnpm lint pass (2 pre-existing warnings in meta-object.test.ts only)
  • pnpm test: core 603, cli 694, vite 162, action 15, mcp 20 — all green
  • Rule docs added (en/ja), docs-links gate green; changeset (minor × core/cli/vite/mcp); packages/action/dist rebuilt via full workspace build

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a new correctness check that detects Svelte {#each} blocks keyed by their own index.
    • Reports potential state mismatches when list items are reordered, inserted, or removed.
    • Includes guidance for replacing index keys with item-unique identifiers.
    • Supports inline and configuration-based rule disabling.
  • Documentation

    • Added English and Japanese documentation for the new rule.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@oekazuma, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 14 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e903213f-1dbc-4172-b4e9-fd5ca71c52c1

📥 Commits

Reviewing files that changed from the base of the PR and between 055436d and 724817b.

⛔ Files ignored due to path filters (1)
  • packages/action/dist/index.js is excluded by !**/dist/**
📒 Files selected for processing (11)
  • .changeset/each-index-key.md
  • docs/src/content/docs/ja/rules/correctness/each-index-key.md
  • docs/src/content/docs/ja/rules/correctness/each-key.md
  • docs/src/content/docs/rules/correctness/each-index-key.md
  • docs/src/content/docs/rules/correctness/each-key.md
  • docs/superpowers/specs/2026-07-22-each-index-key-design.md
  • packages/core/src/component-parse.ts
  • packages/core/src/component.ts
  • packages/core/src/kit-module-parse.ts
  • packages/core/src/vite-config-parse.ts
  • packages/core/test/component-parse.test.ts
📝 Walkthrough

Walkthrough

Adds the correctness/each-index-key rule, detects exact index-keyed {#each} blocks in component facts, registers and exports the rule, adds parser and rule tests, documents it in English and Japanese, and declares minor package releases.

Changes

Each-index-key correctness rule

Layer / File(s) Summary
Each-block fact detection
packages/core/src/component.ts, packages/core/src/component-parse.ts, packages/core/test/component-parse.test.ts
EachBlockFact records indexKey when an each-block key exactly matches its index binding, including TypeScript wrapper handling and exclusion tests.
Rule implementation and registration
packages/core/src/rules/correctness/each-index-key.ts, packages/core/src/rules/index.ts, packages/core/src/index.ts, packages/core/test/each-index-key.test.ts
Defines, registers, exports, and tests diagnostics for index-keyed each blocks without overlapping the existing each-key rule.
Documentation and release publication
docs/src/content/docs/rules/correctness/each-index-key.md, docs/src/content/docs/ja/rules/correctness/each-index-key.md, docs/superpowers/{plans,specs}/*, .changeset/each-index-key.md
Adds rule guidance, suppression examples, design and implementation plans, and minor release declarations.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SvelteParser
  participant collectEachBlocks
  participant correctnessEachIndexKey
  participant Diagnostic
  SvelteParser->>collectEachBlocks: traverse each blocks
  collectEachBlocks->>correctnessEachIndexKey: provide indexKey facts
  correctnessEachIndexKey->>Diagnostic: report matching blocks
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding the correctness/each-index-key rule for {#each} blocks keyed by their index.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/core/test/component-parse.test.ts (1)

27-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the two unwrapping branches.

isIndexKey now explicitly unwraps TSSatisfiesExpression and TSAsExpression, but these tests exercise neither. Add one case for each wrapper so parser-AST changes cannot silently disable detection.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/test/component-parse.test.ts` around lines 27 - 46, Extend the
each-block key tests around isIndexKey to cover both AST unwrapping branches:
add one index-key case wrapped in TSSatisfiesExpression and another wrapped in
TSAsExpression, asserting each produces hasKey: true with indexKey: true. Keep
the existing direct, renamed, composite, and non-index key coverage unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/superpowers/specs/2026-07-22-each-index-key-design.md`:
- Around line 39-43: Update the “Not detected (non-goals)” section to stop
labeling wrapped index expressions and composite keys such as String(i),
template interpolation, and item.id + '-' + i as legitimate non-goals. Either
specify that index-dependent key expressions are detected broadly, or explicitly
describe these forms as an intentional v1 limitation; retain only accurate
non-goals such as missing index bindings, unkeyed blocks, and constant-list
blocks.

---

Nitpick comments:
In `@packages/core/test/component-parse.test.ts`:
- Around line 27-46: Extend the each-block key tests around isIndexKey to cover
both AST unwrapping branches: add one index-key case wrapped in
TSSatisfiesExpression and another wrapped in TSAsExpression, asserting each
produces hasKey: true with indexKey: true. Keep the existing direct, renamed,
composite, and non-index key coverage unchanged.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d0f1d861-98ea-42e6-a6c8-323e4e1cfbf4

📥 Commits

Reviewing files that changed from the base of the PR and between 034fc7c and 055436d.

⛔ Files ignored due to path filters (1)
  • packages/action/dist/index.js is excluded by !**/dist/**
📒 Files selected for processing (12)
  • .changeset/each-index-key.md
  • docs/src/content/docs/ja/rules/correctness/each-index-key.md
  • docs/src/content/docs/rules/correctness/each-index-key.md
  • docs/superpowers/plans/2026-07-22-each-index-key.md
  • docs/superpowers/specs/2026-07-22-each-index-key-design.md
  • packages/core/src/component-parse.ts
  • packages/core/src/component.ts
  • packages/core/src/index.ts
  • packages/core/src/rules/correctness/each-index-key.ts
  • packages/core/src/rules/index.ts
  • packages/core/test/component-parse.test.ts
  • packages/core/test/each-index-key.test.ts

Comment thread docs/superpowers/specs/2026-07-22-each-index-key-design.md
oekazuma added 7 commits July 22, 2026 11:34
String(i), `${i}`, and i.toString() are still position-based identity,
just wrapped — extend correctness/each-index-key's detection beyond the
bare identifier case, unwrapping TS satisfies/as at every recursion step.
Document the trivial-stringification detection and stop calling
index-plus-item-data composite keys unconditionally "legitimate" —
they dodge Svelte's duplicate-key error but still lose move-tracking,
so state the trade-off instead of only the upside. en/ja rule pages
and the design spec updated together.
Rebuild the bundled action dist to pick up the each-index-key
detection change in @svelte-vitals/core.
…, detect index coercions

Exempt length-only lists (Array(n), [...Array(n)], Array.from({length: n})) at
the each-block collector - both each-key and each-index-key were false-
positiving on skeleton/placeholder lists that have no item identity to key by.

Also widen correctness/each-index-key's index-coercion detection to Number(i)
and i + '' (either operand order), and unwrap non-null assertions (i!) in the
key expression alongside the existing satisfies/as TS wrappers.
kit-module-parse.ts and vite-config-parse.ts each carried their own
unwrapTs (satisfies/as only). Delete both local copies and import the
one now exported from component-parse.ts, which also unwraps non-null
assertions (x!). Broadening TS-wrapper unwrapping in the Kit-module and
Vite-config parsers is strictly more correct; no existing test pinned
non-null-assertion behavior there.
…rrections

Document the widened index-coercion detection (Number(i), i + '', TS
non-null i!) and the shared length-only-list exemption in both the en
and ja rule pages for correctness/each-index-key and correctness/each-key.
Correct the design spec's stale Testing-section wording and record the
actual Detection/non-goals behavior shipped in this review wave.
Rebuild the bundled GitHub Action dist to pick up the core fixes/refactor
from this review wave (identity-free-list exemption, index-coercion
detection, shared unwrapTs).
@oekazuma
oekazuma merged commit 2862f96 into main Jul 22, 2026
8 checks passed
@oekazuma
oekazuma deleted the feat/each-index-key branch July 22, 2026 03:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant