Skip to content

chore(lint): report unused eslint-disable directives as errors - #7934

Draft
claude[bot] wants to merge 1 commit into
mainfrom
claude/issue-4853-flip-report-unused-disable-directives
Draft

chore(lint): report unused eslint-disable directives as errors#7934
claude[bot] wants to merge 1 commit into
mainfrom
claude/issue-4853-flip-report-unused-disable-directives

Conversation

@claude

@claude claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #4853

Second dispatch on this card: THE FLIP. The cleanup half landed as PR #7909; this PR is the switch itself.

What this changes

One top-level object in eslint.config.js carrying only linterOptions and no files key:

linterOptions: {
  reportUnusedDisableDirectives: 'error',
},

That is the whole diff (25 lines, 22 of them the comment explaining why). ⛔ No --max-warnings, ⛔ no change to .github/workflows/lint.yml — both are explicitly out of scope by maintainer ruling (comment 5536044589, decision batch #28).

This is a severity change on a check already running, not a new check. ESLint v10.8.1 defaults this option to 'warn', so every stale directive was already reported on every CI run; it was inert only because lint.yml:20 deliberately sets no --max-warnings. Measured with --print-config: the option reads 1 before this PR and 2 after, on every path kind.

The measurement that authorises the flip (ruling condition 2(a))

The ruling makes the flip conditional on a measured zero, re-measured at the implementing branch point — not read off main. Method mirrors the first dispatch: all 47 of turbo's lint tasks (46 package lint scripts + //#lint:root, confirmed by turbo run lint --dry=json) run directly from their own task directory with --report-unused-disable-directives-severity error --format json.

reading at branch point 797b39f68 value
tasks mirrored 47
file rows linted 4330
errors 0
warnings 11964
unused directives 0

The re-measure is load-bearing, not ceremonial: the count went 0 to 2 in the 21 days after PR #4849, with nobody touching those lines.

--print-config on three path kinds

path before after rules resolved
packages/core/src/index.ts 1 2 116
packages/components/src/custom/badge.tsx 1 2 117
scripts/github-slug.mjs 1 2 0

The .mjs row is why the block carries no files key: every rule-bearing object in this config is scoped **/*.{ts,tsx} or narrower, so a linterOptions placed on any of them would miss everything lint:root walks.

Reverse verification — the card's own acceptance criterion

Implementation committed first; each leg plants // eslint-disable-next-line no-debugger above a line that cannot trigger it, proves the mutation reached disk by grep count, runs lint, then restores under a trap with absolute paths.

leg planted mutated run red message restore
A — a .ts in a 117-rule population apps/console/src/__tests__/bootSplash.test.ts:169 (grep 0 to 1) exit 1 169:5 error Unused eslint-disable directive (no problems were reported from 'no-debugger') blob 658cba85 equals HEAD's, git diff HEAD empty, restored run exit 0
B — a .mjs in the ZERO-rule population scripts/github-slug.mjs:45 (grep 0 to 1) exit 1 45:1 error Unused eslint-disable directive (no problems were reported from 'no-debugger') blob a70bea36 equals HEAD's, git diff HEAD empty, restored run exit 0

Ablation — the red is caused by this diff, not by ESLint's default. Same planted directive at the same line, linted against HEAD~1's config (0 hits for linterOptions): exit 0, 1 problem (0 errors, 1 warning). With this PR's config: exit 1, 1 problem (1 error, 0 warnings). The delta is exactly this block.

Interaction with #7908 (measured here, deliberately not repaired here)

#7908 records that eslint.config.js resolves zero rules for every .js/.mjs/.cjs path, so lint:root walks 89 files against nothing. Leg B answers the open question that raised: yes, an unused directive on a zero-rule file goes red. Directive processing does not depend on any rule being configured — the directive names a rule, nothing is reported from it, so it is unused. So this flip is the one check that has real teeth on that population today. #7908 stays open and is untouched by this PR.

Gates, each quoted from its own printed verdict line

Exit codes captured by redirect-then-capture, all at head 28f9f309a.

gate verdict
turbo run lint --concurrency=2 (pnpm lint) Tasks: 47 successful, 47 total · Cached: 0 cached, 47 total — fully uncached; turbo.json declares $TURBO_ROOT$/eslint.config.js as a lint input, so this diff invalidated all 46 package caches and //#lint:root is cache: false
pnpm lint:root exit 0 · ✖ 32 problems (0 errors, 32 warnings)
pnpm lint:coverage ✅ lint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).
pnpm check:control-bytes ✅ check-control-bytes: OK (scanned 6401 tracked text file(s); skipped 85 binary).
node scripts/check-changeset-presence.mjs ✅ No source or published contract of a released package changed in this range, so no changeset is owed. — so this PR deliberately carries none
node scripts/check-governed-queue-guard.mjs --test eslint.config.js ✅ NOT GOVERNED — 1 path(s) checked against 5 governed surface(s); none matched.
pnpm exec vitest run on the 6 test files that read eslint.config.js Test Files 6 passed (6) · Tests 217 passed (217)

Merge posture

⚠️ Stays DRAFT, and auto-merge is deliberately NOT enabled. Ruling condition 2(b) delegates the timing to the domain:devx @ objectui seat: the flip lands in a merge-queue lull, because the one real cost is a red on someone else's in-flight PR whose diff makes an existing directive stale. The remedy for such a red is deleting the stale directive on that PR — the red names the exact file and line.

The zero above is measured at this branch point only. Anything merged to main afterwards can reintroduce a stale directive, and it will red at the merge-queue rebuild rather than silently.

Part of the ratchet started by #4833 / PR #4849 (49 directives cleared) and continued by PR #7909 (2 more). Related: #4835, #4850, #7908.


Generated by Claude Code

Add one top-level `linterOptions: { reportUnusedDisableDirectives: 'error' }`
object to `eslint.config.js`. ESLint's own default for the option is already
`'warn'`, so this is a severity change on a check already running (1 -> 2),
inert until now only because `.github/workflows/lint.yml` deliberately sets no
`--max-warnings`.

The object carries only `linterOptions` and no `files` key, which is what makes
it reach every linted path — including the `.js/.mjs/.cjs` family that
`lint:root` walks and that every rule-bearing object in this config skips.

Precondition measured on this branch point, not assumed: all 47 of turbo's lint
tasks run directly with `--report-unused-disable-directives-severity error`
report 0 unused directives (4330 file rows, 0 errors, 11964 warnings).

Part of objectui#4853

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013uAaxiwgYDybsTNV9xwa1M
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant