feat: library-aware React 19 rules + test-file scoping + build-entry dead-code + js-set-map-lookups string fix#186
Open
aidenybai wants to merge 10 commits into
Open
Conversation
…h legacy peer deps When a package being scanned declares `react` as a `peerDependency` with a range that admits any React major below 19 (e.g. `^17.0.0 || ^18.0.0 || ^19.0.0`), the React-19-deprecation rules become noise — the library MUST keep using `forwardRef`, `defaultProps`, and the legacy `react-dom` root API to honor its peer contract. Detected at `discoverProject` time and surfaced on `ProjectInfo` via two new fields: - `reactPeerRange`: the raw `peerDependencies.react` range, or null - `isLibraryTargetingLegacyReact`: true when the peer range admits a major < 19 When that flag is set, `createOxlintConfig` skips the three `deprecation-warning` rules (`no-react19-deprecated-apis`, `no-default-props`, `no-react-dom-deprecated-apis`). The `prefer-newer-api` gates (`prefer-use-effect-event`) keep firing — library mode is about not breaking peer compat, not about hiding generally useful API recommendations. The scan output also adds a detection step so it's clear when this mode kicks in. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…orkspace prop Review-pass fixes for the library-mode detection added in the prior commit: 1. Wildcard peer ranges (`peerDependencies.react: "*"`, `"x"`, `"x.x.x"`) now correctly trigger library mode. The first-integer parser returned false on these because no integers existed; added an explicit wildcard branch. 2. pnpm catalog references in the peer dep (`"catalog:"` / `"catalog:react18"`) are now resolved through the surrounding monorepo's catalog system. The first attempt only checked the leaf package's directory, but pnpm-workspace.yaml lives at the monorepo root — added the same root-fallback the version resolution already uses. 3. Removed misleading dead code: the prior commit propagated `reactPeerRange` through `findReactInWorkspaces` and `findDependencyInfoFromMonorepoRoot`, but `discoverProject` correctly only reads the LOCAL peer range (a sibling's peer dep tells us nothing about THIS package's compat surface). The propagation was never read; moved the field off `DependencyInfo` and into a focused `resolveReactPeerRange` helper. 4. Defensive guard against malformed package.json (`react: null`, `react: 19`-as-number) — the runtime shape isn't bound by the TS type, so a non-string entry is treated as "no peer dep". 5. README mentions library mode in the rule-toggling sentence so library authors know it auto-engages. New tests cover the wildcard branch (`*`, `x`, `x.x.x`), the pnpm catalog peer-dep path, and malformed peer entries. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Member
Author
|
react review |
1 similar comment
Member
Author
|
react review |
…ngle predicate Folds the wildcard branch into the per-comparator predicate. A comparator votes "supports legacy" if it's a pure wildcard (`*` / `x.x.x`) or its first integer is in [1, 19); one vote flips the range. Removes the separate `comparators.every(isWildcard)` short-circuit, the intermediate `comparators.length === 0` guard (`some()` on an empty array is `false` already), and the `Number.isFinite` check (already covered by the regex matching only ASCII digits). Same observable behavior; ~25 fewer lines and one obvious flow instead of two parallel ones. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
…code FPs Two new project-driven defaults addressing real-world noise reported on @pierre/diffs: 1. Test-file rule scoping. Tests intentionally exercise patterns that would be wrong in production (array-index keys in fixture rows, oversize fixture components, `forwardRef` / `defaultProps` / `flushSync` assertions, design-system spot checks). Detect test files by path shape — `.test.*` / `.spec.*` / `.stories.*` and anything under `__tests__` / `tests` / `test` / `__mocks__` / `cypress` / `e2e` / `playwright` — and suppress a curated set of "noise-in-fixtures" rules. Correctness rules (`rules-of-hooks`, `effect-needs-cleanup`, the `react-hooks-js/*` compiler rules, `alt-text`, …) keep firing because a buggy test is still a bug. Opt out via `suppressNoiseRulesInTestFiles: false`. 2. Build-entry dead-code suppression. Knip's `files` rule flags source files that no other source file imports — but library entry points (CLI scripts, web/service workers, additional bundle entries) are imported by the BUILD process, not by source. Suppress `knip/files` when the source has a matching artifact under any of the standard build dirs (`dist` / `build` / `lib` / `out` / `esm` / `cjs`) with a `.js` / `.mjs` / `.cjs` extension. Conservative in the right direction: false negatives (real dead code with a coincidental dist artifact) are vanishingly rare; false positives (real entry flagged as dead) were the user-facing pain. Opt out via `suppressDeadCodeForBuildEntries: false`. Both run as a project-driven (no user-config required) filter pass in `mergeAndFilterDiagnostics`, ahead of the existing user-config / inline-suppression layers. The new boolean fields are added to `validateConfigTypes` so JSON-typing mistakes are coerced + warned the same way `adoptExistingLintConfig` etc. are. README updated with the new behavior and opt-out flags. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
|
Note No issues found |
…rewrite for substring search)
Reported false positives on @pierre/diffs:
- `contents.indexOf('\n', cursor)` where `contents: string`
- `style?.textContent?.includes(expected)` (DOM string property)
- `array.includes()` in fixture loops inside test files
The rule's premise — "convert to a Set for O(1) lookups" — only
applies to arrays. Strings have no Set equivalent for substring search,
so flagging `string.indexOf()` / `string.includes()` is wrong advice.
Two fixes:
1. Skip when the receiver is obviously a string. Detection signals
(high-confidence, no type info available at the AST layer):
- String / template literal receiver
- `String(...)` cast
- String-returning method call: `.toString`, `.toLowerCase`,
`.toUpperCase`, `.trim*`, `.pad*`, `.replace*`, `.substring`,
`.substr`, `.normalize`, `.repeat`, `.charAt`, `.toFixed` etc.
- Member access ending in a known DOM/built-in string property:
`.textContent`, `.innerHTML`, `.outerHTML`, `.innerText`,
`.nodeValue`, `.tagName`, `.className`, URL parts, error
properties, etc.
- Identifier with a name that overwhelmingly binds to a string:
`text`, `contents`, `html`, `json`, `url`, `path`, `line`,
`needle`, etc. (ambiguous names like `data`, `value`, `item`
deliberately omitted — those bind to arrays nearly as often)
- ChainExpression wrapper transparently unwrapped so optional
chaining (`a?.textContent?.includes(x)`) still detects the
string property at the tail.
2. Add the JS micro-perf and async-perf rules to the test-file
disabled set (`js-set-map-lookups`, `js-flatmap-filter`,
`js-combine-iterations`, `js-tosorted-immutable`,
`js-hoist-regexp`, `js-hoist-intl`, `js-cache-property-access`,
`js-length-check-first`, `js-min-max-loop`, `js-batch-dom-css`,
`js-index-maps`, `js-cache-storage`, `js-early-exit`,
`async-defer-await`, `async-await-in-loop`, `async-parallel`).
Test code isn't on the hot path; fixture loops over allow-lists
shouldn't be held to production-perf standards.
The array-on-array case (the actual perf concern) still fires —
verified by two regression tests covering `array.includes()` and
`array.indexOf()` on plain array variables.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Member
Author
|
react review |
1 similar comment
Member
Author
|
react review |
…ugbot review) Bugbot caught a low-severity copy-paste duplicate: `"tagName"` was listed both in the "Node text accessors" group and the "Element identity / metadata" group. Harmless at runtime (the `Set` dedupes) but a real review nit. Moved to the metadata group where it semantically fits — tagName describes element identity, not text content. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Member
Author
|
react review |
2 similar comments
Member
Author
|
react review |
Member
Author
|
react review |
…plugin + new fixtures) Conflict 1: README.md — main added an "Optional companion plugins" section right where I added the `suppressNoiseRulesInTestFiles` / `suppressDeadCodeForBuildEntries` config docs. Kept both sections side-by-side. Conflict 2: oxlint-config.ts — main spread `youMightNotNeedEffectRules` between `reactCompilerRules` and `filterRulesByReactMajor(...)`, right where I extended the `filterRulesByReactMajor` signature to accept `isLibraryTargetingLegacyReact`. Kept both: the new spread is preserved, AND the third arg lands on the filter call. All 758 tests pass (737 mine + ~21 new from main); lint, typecheck, format clean. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Member
Author
|
react review |
1 similar comment
Member
Author
|
react review |
main #202 added `tailwindVersion` plumbing through the same ProjectInfo → scan → runOxlint → createOxlintConfig pipeline I added `isLibraryTargetingLegacyReact` to. Resolved by including BOTH params at every layer: - `OxlintConfigOptions.tailwindVersion` lives next to `isLibraryTargetingLegacyReact` in the interface. - `createOxlintConfig` destructures both with their defaults. - The rule pipeline composes the filters: Tailwind gating wraps the React-major+library gating output. - `runOxlint` forwards both, including in the no-extends fallback call. `scan` and `diagnose` pass `projectInfo.tailwindVersion` alongside `projectInfo.isLibraryTargetingLegacyReact`. - `discoverProject` destructures `tailwindVersion` from `extractDependencyInfo` while keeping the `reactPeerRange` resolution unchanged. - `collectRuleHits` test helper passes both options. 779 tests pass (758 mine + ~21 new from main); lint, typecheck, format clean. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit e49eee0. Configure here.
…peer ranges (Bugbot review) Bugbot caught: `resolveReactPeerRange` was calling `resolveCatalogVersion` without forwarding the catalog name extracted from the peer-range string. When a project's `peerDependencies.react` is `"catalog:react18"` and the workspace has multiple grouped catalogs (e.g. `react18` and `react19`), the resolver would fall through to the `Object.values()` group scan and silently pick whichever catalog iterated first — producing wrong library-mode verdicts. Fix mirrors the `leafReactCatalogReference` plumbing already used by the main `reactVersion` resolver: extract the catalog name via `extractCatalogName` and pass it to `resolveCatalogVersion` as the explicit `catalogReference` argument. Both the local and monorepo- root resolution paths now route correctly. Regression test verifies the `react18` group is picked over `react19` when the peer is `"catalog:react18"`. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Per-feature docs in the intro (library mode, test-file scoping, build-entry suppression) were noisy — they don't fit the high-level "score + issue list" framing. The dedicated config entries in the Configuration section (`suppressNoiseRulesInTestFiles`, `suppressDeadCodeForBuildEntries`) cover the same ground for users who actually need to toggle them. Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Motivation
User feedback from a library author who scanned
@pierre/diffs(rolled up across multiple rounds):This PR addresses all four — three new project-driven defaults plus a targeted false-positive fix on
js-set-map-lookups.Four fixes
1. Library-aware React 19 deprecation rules
When a package declares
reactas apeerDependencywith a range that admits any React major below 19 (e.g.^17 || ^18 || ^19,>=17,^16.8 || ^17 || ^18 || ^19, plain*, or a pnpmcatalog:reference resolving to one of the above), three rules now skip:react-doctor/no-react19-deprecated-apis(forwardRef,useContext)react-doctor/no-default-propsreact-doctor/no-react-dom-deprecated-apisThe library MUST keep these APIs to honor its peer contract.
prefer-use-effect-eventand otherprefer-newer-apirules keep firing — library context is about not breaking peer compat, not about hiding generally useful API recommendations.2. Test-file rule scoping
Tests intentionally exercise patterns that would be wrong in production: array-index keys in fixture rows, oversize fixture components,
forwardRef/defaultProps/flushSyncassertions, design-system spot checks, micro-perf patterns in fixture loops, etc. Files matching.test.*/.spec.*/.stories.*or anything under__tests__/tests/test/__mocks__/cypress/e2e/playwrightnow skip a curated set of "noise-in-fixtures" rules. Correctness rules (rules-of-hooks,effect-needs-cleanup, thereact-hooks-js/*compiler rules,alt-text,role-has-required-aria-props, …) keep firing — a buggy test is still a bug.Opt out via
suppressNoiseRulesInTestFiles: false.3. Build-entry dead-code suppression
Knip's
filesrule flags source files that no other source file imports — but library entry points (CLI scripts, web/service workers, additional bundle entries) are imported by the BUILD process, not by other source files. We now suppressknip/fileswhen the source has a matching artifact under any of the standard build dirs (dist/build/lib/out/esm/cjs) with a.js/.mjs/.cjsextension.Conservative in the right direction: false negatives (real dead code with a coincidental dist artifact) are vanishingly rare; false positives (real entry flagged as dead) were the actual user-facing pain. Requires the project to have been built — pre-build, every entry is still flagged.
Opt out via
suppressDeadCodeForBuildEntries: false.4.
js-set-map-lookupsskips string receiversThe rule's premise — "convert to a Set for O(1) lookups" — only applies to arrays. Strings have no Set equivalent for substring search, so flagging
string.indexOf()/string.includes()is wrong advice. Two reported false positives:contents.indexOf('\n', cursor)wherecontents: stringstyle?.textContent?.includes(expected)(DOM string property)The rule now skips when the receiver is obviously a string. High-confidence detection signals (no type info available at the AST layer):
String(...)cast.toString,.toLowerCase,.toUpperCase,.trim*,.pad*,.replace*,.substring,.substr,.normalize,.repeat,.charAt,.toFixed, …).textContent,.innerHTML,.outerHTML,.innerText,.nodeValue,.tagName,.className, URL parts, error properties, …)text,contents,html,json,url,path,line,needle, …) — ambiguous names likedata,value,itemdeliberately omittedChainExpressionwrapper transparently unwrapped so optional chaining (a?.textContent?.includes(x)) still detects the string property at the tailPlus all JS micro-perf and async-perf rules (
js-set-map-lookups,js-flatmap-filter,js-combine-iterations,async-await-in-loop, …) are now in the test-file disabled set — fixture loops over allow-lists shouldn't be held to production-perf standards.The array-on-array case (the actual perf concern) still fires — verified by regression tests covering
array.includes()andarray.indexOf()on plain array variables.Implementation notes
peerRangeSupportsLegacyReact()splits on semver's boolean separators (||,,, whitespace), and a comparator votes "supports legacy" if it's a pure wildcard (*/x.x.x) or its first integer is in [1, 19). One vote flips the range. Reading only the first integer per comparator avoids false positives on minor/patch zeros (^19.0.0) and canary hex digits (0.0.0-canary-abc).resolveReactPeerRange()reads the local package'speerDependencies.react, defensively guards against malformed entries (null/ numbers), and resolves pnpmcatalog:references through the catalog system. Named catalog references ("catalog:react18") forward the catalog name toresolveCatalogVersionso multi-grouped catalogs resolve to the correct group. Falls back to the monorepo root when the leaf package's directory has nopnpm-workspace.yaml(the common shape).filterAutomaticSuppressionsruns as a project-driven (no user-config required) filter pass inmergeAndFilterDiagnostics, ahead of the existing user-config / inline-suppression layers. Per-file results cached so an N-diagnostic file pays the test/build-entry checks once.isLikelyStringReceiverlives next to thejsSetMapLookupsrule — local to the one rule that needs it, no premature abstraction.validateConfigTypesso JSON-typing mistakes are coerced + warned.Tests
parse-react-peer-range.test.ts(7 cases): basic legacy-supporting ranges, 19+only ranges, wildcards, npm dist-tags, canary tags, double-counting0digits.discoverProjectcases: existing fixture, library with^19.0.0peer (no relaxation), app with no peer dep, wide^16.8 || ^17 || ^18 || ^19peer,*wildcard peer, pnpm catalog peer (resolves through monorepo root), named catalog peer (catalog:react18correctly picks thereact18group over a siblingreact19group), malformed peer (react: null).react-19-migration-rules.test.ts: each of the three deprecation rules in library mode + a "still flagsprefer-use-effect-eventin library mode" check.is-test-file.test.ts(7 cases):.test.*/.spec.*suffixes, Storybook fixtures, test directories at any depth, Windows path normalization, false positives likecontestants/,protest/,testing-utils.ts.is-likely-build-entry.test.ts(9 cases):src/cli.ts ↔ dist/cli.js, the user-reported worker entries, every (build dir, ext) combo, top-level vs nested files, missing artifacts, Windows paths.filter-automatic-suppressions.test.ts(11 cases): test-file scoping including opt-out, build-entry suppression including opt-out, fast-path when both disabled, JS micro-perf rules dropped in test files (regression).js-set-map-lookups-string-fp.test.ts(9 cases):contents.indexOf('\n'),Element.textContent?.includes(), string literal / template literal /String(...)/.toLowerCase()chain receivers, optional-chained DOM property, plus two "still flags arrays" sanity cases for the array-on-array regression risk.All 780 tests pass; lint, typecheck, format are clean.
Review feedback
ec42a02): caught a low-severity copy-paste duplicate —"tagName"listed twice inSTRING_TYPED_PROPERTY_NAMES. Fixed in49f0075.e49eee0, post first merge): caught named catalog reference being dropped —resolveReactPeerRangewas callingresolveCatalogVersionwithout forwarding the catalog NAME, so"catalog:react18"resolved against an arbitrary group rather than the requestedreact18one. Fixed in1336a0aplus a regression test that verifies thereact18group is picked over a siblingreact19group.origin/mainrebases handled cleanly: PR feat(plugin): integrate eslint-plugin-react-you-might-not-need-an-effect #201 (eslint-plugin-react-you-might-not-need-an-effect) and PR feat(react-doctor): detect Tailwind version and gatesize-Nrule on it #202 (Tailwind version detection), both of which extended the sameOxlintConfigOptionsinterface and call sites I addedisLibraryTargetingLegacyReactto.