Skip to content

Add no-index-key eslint rule#3429

Merged
arkid15r merged 3 commits intomainfrom
add-no-index-key-rule
Jan 21, 2026
Merged

Add no-index-key eslint rule#3429
arkid15r merged 3 commits intomainfrom
add-no-index-key-rule

Conversation

@kasya
Copy link
Collaborator

@kasya kasya commented Jan 20, 2026

Added a no-index-key rule to custom eslint-rules. This will help keep codebase clean and comply with typescript:S6479 rule

Checklist

  • Required: I followed the contributing workflow
  • Required: I verified that my code works as intended and resolves the issue as described
  • Required: I ran make check-test locally: all warnings addressed, tests passed
  • I used AI for code, documentation, tests, or communication related to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 20, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Replaced unstable array-index keys with stable unique keys across multiple list renderings.
    • Fixed a skeleton import typo.
  • Refactoring

    • Centralized label rendering into a shared component and updated related components to use it.
    • Introduced required identifier/namespace props to several list components for stable keying.
    • Enforced stricter linting for array-index keys with targeted exceptions for skeletons.
  • Tests

    • Augmented test fixtures with identifier fields to match runtime keying.
  • Chores

    • Added a new spelling token to the custom dictionary.

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

This PR adds stable identifiers and namespacing for list keys: introduces cardKey and entityKey props, adds id fields to several types and test fixtures, replaces index-based React keys with composite ids, updates related components/pages/tests, and tightens ESLint rule for array-index keys with skeleton exemptions.

Changes

Cohort / File(s) Summary
Lint & Dictionary
cspell/custom-dict.txt, frontend/eslint.config.mjs
Added token "quasis" to cspell dictionary; enabled react/no-array-index-key: 'error' with overrides disabling it for skeleton files.
Core types
frontend/src/types/card.ts, frontend/src/types/contributor.ts, frontend/src/types/event.ts, frontend/src/types/home.ts, frontend/src/types/leaders.ts, frontend/src/types/release.ts
Added required cardKey/entityKey/id fields to CardProps, Contributor, Event, Sponsor, LeadersListProps, and Release types.
Labeling & list components
frontend/src/components/LabelList.tsx, frontend/src/components/LeadersList.tsx, frontend/src/components/ToggleableList.tsx
Introduced entityKey prop; replaced index-based keys with ${entityKey}-${item} keys; LeadersList trims/filters leaders and early-returns on empty.
Card & details wiring
frontend/src/components/Card.tsx, frontend/src/components/CardDetailsPage.tsx
Card API reorganized to accept cardKey and labels; LabelList now invoked with entityKey={${cardKey}-labels}; CardDetailsPage passes entityKey to LeadersList and ToggleableList call sites.
Pages: card usage / keying
frontend/src/app/chapters/page.tsx, frontend/src/app/committees/page.tsx, frontend/src/app/contribute/page.tsx, frontend/src/app/projects/page.tsx, frontend/src/app/community/snapshots/[id]/page.tsx
Added cardKey prop to Card usages; removed relying on mapped element keys and wrapped items with Fragment keys where needed (use release.id fallback logic).
Pages: leaders & bio keying
frontend/src/app/page.tsx, frontend/src/app/members/[memberKey]/page.tsx
LeadersList calls augmented with entityKey values per section; bio word map keys changed from indices to login+word.
Mentorship / LabelList usages
frontend/src/app/my/mentorship/.../issues/[issueId]/page.tsx, .../mentees/[menteeKey]/page.tsx
Replaced manual label rendering with LabelList; added entityKey props for labels/domains/skills.
LogoCarousel & search keys
frontend/src/components/LogoCarousel.tsx, frontend/src/components/MultiSearch.tsx
Sponsor type gains id; sponsor keys use sponsor.id; MultiSearch keys scoped by suggestion indexName to avoid collisions.
Nav, Pagination, Skeletons, Misc key fixes
frontend/src/components/NavDropDown.tsx, frontend/src/components/Pagination.tsx, frontend/src/components/skeletons/*, frontend/src/components/Card.tsx
Replaced index keys with prefixed string keys; added readonly props where applicable; NOSONAR/ESLint comments added for intentional index usage in constrained cases.
GraphQL query
frontend/src/server/queries/projectQueries.ts
Added id to recentReleases selection in GET_PROJECT_DATA.
Tests & mocks (a11y, unit, mockData)
frontend/__tests__/... (many files; see diff)
Augmented test fixtures with id and cardKey fields; updated mocked components to accept entityKey/cardKey; adjusted tests to use stable keys instead of indices; fixed import typo ApiKeySkeleton.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • arkid15r
  • aramattamara
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add no-index-key eslint rule' accurately describes the main objective of the changeset, which is implementing an ESLint rule to discourage index-based React keys.
Description check ✅ Passed The description clearly explains the purpose of the PR: adding a no-index-key ESLint rule to comply with SonarCloud rule typescript:S6479, and includes the required checklist confirmations.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch add-no-index-key-rule

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

🤖 Fix all issues with AI agents
In `@frontend/eslint-rules/no-index-key.mjs`:
- Around line 144-156: The BinaryExpression branch incorrectly returns true when
only a string operand is present; change the final return so we only flag when
an index variable is present. In the node.type === 'BinaryExpression' block,
keep the compound check using hasIndexVariable(...) and hasString, but replace
"return hasIndex || hasString" with "return hasIndex" so the rule only flags
when hasIndexVariable(...) is true (use the existing hasIndex and hasString
variables and the hasIndexVariable function to locate the logic).
🧹 Nitpick comments (3)
frontend/src/app/my/mentorship/programs/[programKey]/modules/[moduleKey]/issues/[issueId]/page.tsx (1)

277-279: Prefer a stable label identifier when possible.

If label values are unique in this list, using key={label} avoids any index-derived key and aligns better with the intent of the no-index-key rule. Otherwise, consider a stable ID from the API.

♻️ Suggested tweak (only if labels are unique)
-            {visibleLabels.map((label, index) => (
-              <span key={`${label}-${index}`} className={labelButtonClassName}>
+            {visibleLabels.map((label) => (
+              <span key={label} className={labelButtonClassName}>
                 {label}
               </span>
             ))}
frontend/src/components/MenteeContributorsList.tsx (1)

54-57: Use a stable contributor identifier for the key.

This list is dynamic; prefer a stable field (e.g., login or id) over any index-derived key.

♻️ Suggested tweak (if login is unique)
-        {displayContributors.map((item, index) => (
+        {displayContributors.map((item) => (
           <div
-            key={`contributor-${index}`}
+            key={item.login}
             className="overflow-hidden rounded-lg bg-gray-200 p-4 dark:bg-gray-700"
           >
frontend/src/components/MenteeIssues.tsx (1)

76-78: Prefer label-only keys if they’re unique.

If label values are unique per issue, key={label} is more stable than including the index.

♻️ Suggested tweak (only if labels are unique)
-                    {issue.labels.slice(0, 3).map((label, index) => (
+                    {issue.labels.slice(0, 3).map((label) => (
                       <span
-                        key={`${label}-${index}`}
+                        key={label}
                         className="rounded bg-blue-100 px-2 py-1 text-xs text-blue-800"
                       >

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
frontend/src/components/Card.tsx (1)

142-148: Inconsistent with PR goal: topContributors still uses array index for keys.

The PR aims to eliminate array-index-based keys, yet ContributorAvatar still uses index in both the key fallback and uniqueKey prop. Since Contributor now has an id field, use it for stable keying.

Suggested fix
-            {topContributors?.map((contributor, index) => (
+            {topContributors?.map((contributor) => (
               <ContributorAvatar
-                key={contributor.login || `contributor-${index}`}
+                key={contributor.id}
                 contributor={contributor}
-                uniqueKey={index.toString()}
+                uniqueKey={contributor.id}
               />
             ))}
frontend/src/app/members/[memberKey]/page.tsx (1)

61-82: Duplicate keys when words repeat in bio.

Using ${user.login}-${word} as the key will produce duplicates when the same word appears multiple times in a bio (e.g., "I am a a developer" → two elements with key johndoe-a). Similarly, the mention key could duplicate if the same user is mentioned twice.

Consider retaining the index but combining it with a stable prefix to satisfy the lint rule while ensuring uniqueness:

Proposed fix
- const formattedBio = user?.bio?.split(' ').map((word) => {
+ const formattedBio = user?.bio?.split(' ').map((word, index) => {
    const mentionMatch = word.match(/^@([\w-]+(?:\.[\w-]+)*)([^\w@])?$/)
    if (mentionMatch && mentionMatch.length > 1) {
      const username = mentionMatch[1]
      const punctuation = mentionMatch[2] || ''
      return (
-       <React.Fragment key={`mention-${user.login}-${username}`}>
+       <React.Fragment key={`${user.login}-bio-${index}`}>
          <Link
            href={`https://github.com/${username}`}
            target="_blank"
            rel="noopener noreferrer"
            className="text-blue-400 hover:underline"
          >
            @{username}
          </Link>
          {punctuation}
          <span> </span>
        </React.Fragment>
      )
    }
-   return <span key={`${user.login}-${word}`}>{word} </span>
+   return <span key={`${user.login}-bio-${index}`}>{word} </span>
  })

Note: If the ESLint rule disallows array indices entirely, an alternative is to generate a unique ID for each word position using a deterministic hash or by tracking word occurrence count.

🤖 Fix all issues with AI agents
In `@frontend/__tests__/unit/components/CardDetailsPage.test.tsx`:
- Around line 176-179: The prop type declaration is using the wrong property
name `_entityKey`; change the interface so it declares `entityKey: string` (not
`_entityKey`) to match the actual prop passed in and the component
destructuring; keep `leaders: string` and the index signature as-is, and ensure
any places that destructure props (where you alias `entityKey` to `_entityKey`)
continue to work with the renamed type property.
- Around line 349-354: The props type in the CardDetailsPage test mock declares
`_entityKey: string` but the component destructures `entityKey`, causing a
mismatch; update the type/interface in CardDetailsPage.test.tsx to use
`entityKey: string` (matching the destructured prop name)—mirror the same fix
applied to the LeadersList mock so other tests remain consistent and rebuild
correctly.

In `@frontend/__tests__/unit/components/MultiSearch.test.tsx`:
- Around line 326-352: The test fixtures in the "filters event data based on
query" test are missing the required Event.id field and are being force-cast
with "as Event[]"; update each event object in the eventData array to include a
unique id property (e.g., 'event-1', 'event-2', 'event-3') so the objects
conform to the Event type instead of relying on the type assertion; ensure the
test continues to use the same objectID/key values and remove or keep the "as
Event[]" cast once the objects are fully typed.
🧹 Nitpick comments (4)
frontend/src/components/NavDropDown.tsx (1)

70-73: Avoid potentially non-unique/undefined keys.

submenu.href is optional, so key={${submenu.href}} can become "undefined" for multiple items, causing duplicate keys and unstable reconciliation. Consider a safer composite fallback.

♻️ Proposed fix
-          {link.submenu?.map((submenu) => (
+          {link.submenu?.map((submenu) => (
             <Link
-              key={`${submenu.href}`}
+              key={`${submenu.href ?? 'no-href'}-${submenu.text}`}
               href={submenu.href || '/'}
               className={cn(
frontend/src/components/Pagination.tsx (1)

71-73: Prefer stable keys and drop the eslint disable.

Index-based keys are avoidable here; use the page value as the key and only fall back to index for ellipses. This keeps the new rule meaningful.

♻️ Proposed refactor
-        {pageNumbers.map((number, index) => (
-          // eslint-disable-next-line react/no-array-index-key
-          <React.Fragment key={`pagination-${index}-${number}`}>
+        {pageNumbers.map((number, index) => {
+          const key =
+            number === '...' ? `pagination-ellipsis-${index}` : `pagination-${number}`
+          return (
+            <React.Fragment key={key}>
             {number === '...' ? (
               <div className="flex h-10 w-10 items-center justify-center text-gray-600 dark:text-gray-400">
                 <FaEllipsis className="h-5 w-5" aria-hidden="true" />
               </div>
             ) : (
               <Button
                 type="button"
                 aria-current={currentPage === number ? 'page' : undefined}
                 aria-label={`Go to page ${number}`}
                 className={`flex h-10 min-w-10 items-center justify-center rounded-md px-3 text-sm font-medium ${
                   currentPage === number
                     ? 'bg-[`#83a6cc`] text-white dark:bg-white dark:text-black'
                     : 'border-1 border-gray-200 bg-white text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700'
                 }`}
                 onPress={() => onPageChange(number as number)}
               >
                 {number}
               </Button>
             )}
-          </React.Fragment>
-        ))}
+            </React.Fragment>
+          )
+        })}
frontend/src/components/LeadersList.tsx (1)

27-38: Potential key collision with duplicate leader names.

Using ${entityKey}-${leader} as the key assumes leader names are unique within the list. If the same leader name appears multiple times (e.g., from a malformed input string like "John, John"), React will encounter duplicate keys.

Consider including the index as a fallback for uniqueness while still benefiting from stable identity:

Suggested fix
-        <span key={`${entityKey}-${leader}`}>
+        <span key={`${entityKey}-${leader}-${index}`}>
frontend/src/app/community/snapshots/[id]/page.tsx (1)

152-156: Consider simplifying by moving key to the Card component.

The React.Fragment wrapper is solely used to attach the key prop. Since renderChapterCard returns a single Card element, you could simplify by passing the key directly to the Card.

Suggested simplification
-            {snapshot.newChapters
-              .filter((chapter) => chapter.isActive)
-              .map((chapter) => (
-                <React.Fragment key={chapter.key}>{renderChapterCard(chapter)}</React.Fragment>
-              ))}
+            {snapshot.newChapters
+              .filter((chapter) => chapter.isActive)
+              .map((chapter) => renderChapterCard(chapter))}

Then update renderChapterCard to accept and use the key:

const renderChapterCard = (chapter: Chapter) => {
  // ... existing code ...
  return (
    <Card
      key={chapter.key}
      button={submitButton}
      cardKey={chapter.key}
      // ... rest of props
    />
  )
}

@sonarqubecloud
Copy link

@kasya kasya marked this pull request as ready for review January 21, 2026 03:23
@kasya kasya requested a review from arkid15r as a code owner January 21, 2026 03:23
@arkid15r arkid15r added this pull request to the merge queue Jan 21, 2026
Merged via the queue into main with commit 8aab927 Jan 21, 2026
31 of 32 checks passed
@arkid15r arkid15r deleted the add-no-index-key-rule branch January 21, 2026 04:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants