Both helpers built identical ProjectInfo shapes inline. Moved the
omitted-vs-explicit-null reactMajorVersion distinction into
buildTestProject so collectRuleHits can just delegate.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Lint flagged it as no-unused-vars — left over from when the local
flattenMemberName helper used the type directly. After aliasing to
the canonical flattenCalleeName, the type-only import is dead.
Behaviour-neutral.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
After the canonical helper landed in `utils/is-inline-function-expression.ts`,
`event-handler-reference.ts` kept a re-export so collect-function-like-
local-names could keep its existing import path. Migrate that single
remaining consumer to import the canonical location directly, then
drop the transitional re-export.
`event-handler-reference.ts` still imports the helper internally
(used by isEventHandlerValue) so the import line stays — just no
longer fronted as a public surface of this module.
Behaviour-neutral.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Format drift in 6 files touched by d09a994c (isInlineFunctionExpression
promotion + 7 rule-file applications). Reformat is behaviour-neutral.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
`isInlineFunctionExpression` (the 2-type `ArrowFunctionExpression`
or `FunctionExpression` predicate, used for expression-position
checks where a FunctionDeclaration can't appear) was tucked inside
`rules/state-and-effects/utils/event-handler-reference.ts`. Promote
to `utils/is-inline-function-expression.ts` so all rule files can
use one canonical type guard.
Apply to seven new sites that previously inlined the 2-type triplet:
- architecture/no-many-boolean-props.ts (VariableDeclarator.init check)
- architecture/react-compiler-destructure-method.ts (initializer check)
- correctness/no-prevent-default.ts (JSX prop expression check)
- correctness/no-uncontrolled-input.ts (VariableDeclarator.init check)
- js-performance/async-await-in-loop.ts (replaces local
isFunctionishExpression with the canonical helper)
- js-performance/js-length-check-first.ts (CallExpression callback check)
- nextjs/nextjs-async-client-component.ts (VariableDeclarator.init check)
The 2-type pattern is intentionally narrower than isFunctionLike
because declarations only appear as statements — using the wider
`isFunctionLike` at these expression-position call sites would be
incorrect (and even if it weren't, the type-narrow wouldn't match).
event-handler-reference.ts re-exports the helper so its existing
intra-directory consumer keeps working without an import path change.
Behaviour-neutral; -28 lines net across the 7 files.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Sweep across rule files where the inline triplet
`isNodeOfType(x, 'FunctionDeclaration') || ... || ...ArrowFunctionExpression`
was used as a pure boundary-check (not part of a richer predicate):
- architecture/no-legacy-context-api.ts
- js-performance/async-await-in-loop.ts (inside walkAst visitor)
- js-performance/js-batch-dom-css.ts
- js-performance/js-hoist-intl.ts
- nextjs/nextjs-no-side-effect-in-get-handler.ts
- performance/async-defer-await.ts (two sites — positive + negated)
Each site now imports the canonical isFunctionLike type guard.
Files where the triplet check is intentionally selective (e.g.
no-unstable-nested-components includes ClassDeclaration / ClassExpression,
inferFunctionLikeName in no-unstable-nested-components only matches
FunctionDeclaration + FunctionExpression for named-function lookup,
react-compiler-destructure-method uses a 2-type ArrowFunction/FunctionExpression
check for an initializer position) are left as-is.
Behaviour-neutral; ~30 lines net removed across the 6 files.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Two adjacent function-boundary checks (each constructed the same
isCrossingFunctionBoundary local from the 3-type triplet) collapse
to direct isFunctionLike(current) calls.
Behaviour-neutral; -10 lines net.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
The walker that walks up the AST to find an enclosing component
inlined the 3-check (FunctionDeclaration / FunctionExpression /
ArrowFunctionExpression) pattern. Replace with the canonical
`isFunctionLike` type guard.
The other 3-check at `inferFunctionLikeName` is NOT replaced — that
one is specifically about "has a named function id" and only
matches FunctionDeclaration + FunctionExpression (excludes
ArrowFunctionExpression). Different semantic; left alone.
Behaviour-neutral; -4 lines net.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
The local NESTED_FUNCTION_TYPES set (FunctionDeclaration /
FunctionExpression / ArrowFunctionExpression) was the same 3-element
set already centralised as FUNCTION_LIKE_TYPES in constants/js.ts.
Drop the local copy; import the canonical version.
Behaviour-neutral; -5 lines net.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
The function-boundary check inlined the same three isNodeOfType calls
(FunctionDeclaration / FunctionExpression / ArrowFunctionExpression)
that utils/is-function-like.ts already exposes as a typed predicate.
Drop the inline check; import isFunctionLike.
Behaviour-neutral; -4 lines net.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
forbid-elements.ts had its own `flattenMemberName` helper for JS
member-expression chains — functionally equivalent to the canonical
`flattenCalleeName` (utils/flatten-callee-name.ts). The canonical
helper also rejects computed member access (`a['b']`); the local
copy implicitly rejected the same case by checking that the property
node was an Identifier.
Drop the local helper; alias `flattenMemberName` to the canonical
import so existing call sites keep their name. Behaviour-neutral.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
After previously consolidating NON_REACT_JSX_DIALECT_PACKAGES, no-unknown-property
still kept its own local copy of `fileImportsNonReactJsxDialect` AND `startsWithAny`.
The utils version exports both pieces (the function with its
`NON_REACT_JSX_DIALECT_PACKAGE_PREFIXES` companion) under the same
name. Drop the local copies entirely, import the function instead.
The local impl hardcoded `["solid-js", "@builder.io/qwik"]` as
the deep-import prefixes; the utils version uses the
`NON_REACT_JSX_DIALECT_PACKAGE_PREFIXES` constant which is the same
two prefixes. Behaviour-neutral; -20 lines net.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Four rules each declared their own `compileGlob` — a simple glob-to-
RegExp converter (escape regex specials, replace `*` with `.*`,
anchor with ^...$). All four implementations produced the same
RegExp; one was a one-liner, three used a block body, but all four
were semantically identical.
- rules/a11y/label-has-associated-control.ts
- rules/react-builtins/forbid-component-props.ts
- rules/react-builtins/jsx-handler-names.ts
- rules/react-builtins/no-unstable-nested-components.ts
Promote to utils/compile-glob.ts. All four sites now import.
Behaviour-neutral; -15 lines net.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Four files each declared `flattenJsxName`. Two were byte-identical
(forbid-elements + jsx-props-no-spreading); both promoted to
utils/flatten-jsx-name.ts under the same name.
The other two variants stay local because they encode different
intent:
- forbid-component-props.ts returns a NON-nullable string with empty-
string sentinel + handles ThisExpression / JSXThisExpression
(`<this.x />` rare-but-valid form).
- utils/get-element-type.ts returns a non-nullable string + handles
JSXNamespacedName (`<svg:circle />` style).
Consolidating those would change behaviour or force callback options;
left as-is. Behaviour-neutral; -16 lines net for the two byte-equal
sites.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Three files retained imports for helpers that were replaced by
canonical shared utilities during the consolidation series:
- rules/state-and-effects/utils/effect/get-program-analysis.ts:
isNodeOfType (left over from the inlined findProgramRoot)
- rules/state-and-effects/utils/effect/react.ts:
EsTreeNodeOfType type-only import (was for the local isFunctionLike)
- rules/state-and-effects/no-pass-live-state-to-parent.ts:
isNodeOfType (left over from the inlined getCallMethodName)
Lint flagged each as no-unused-vars; drop them. Behaviour-neutral.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
The cleanup series introduced minor formatting drift (mostly import-
ordering / line-wrap differences) in ~20 files. `pnpm format` brings
them back into prettier's canonical shape so format:check is green
across the entire repo. Behaviour-neutral; no code changes.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Plan stretch T4 flagged InspectOptions as mixing engine inputs
(lint, deadCode, includePaths, configOverride, respectInlineDisables)
with rendering / orchestration knobs (scoreOnly, noScore, silent,
verbose, outputSurface, isCi). A full type split was investigated and
deferred — every call site builds the union anyway, so the gain was
purely documentary.
Capture the same intent in source order + a leading doc comment:
group engine fields together at the top, rendering knobs below,
with section markers between them. Same external type, same call
sites; the future-reader signal is preserved without churning a
published-API shape.
Behaviour-neutral.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
has-binding-named.ts maintained its own collectBindingNamesFromPattern
helper that was functionally equivalent to the existing
utils/collect-pattern-names.ts (both recursively walk
Identifier/ObjectPattern/ArrayPattern/RestElement/AssignmentPattern
and write name strings into a Set<string>).
Replace the local copy with the canonical `collectPatternNames` import.
The implementations differed only in code style (raw type-string +
`in` checks vs `isNodeOfType` narrowing) — the same name set was
collected either way.
Behaviour-neutral; -32 lines net.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
api/diagnose.ts had a private `buildLayerStack()` that constructed
the exact same Layer.mergeAll as core's already-exported
`layerInspectLive`. Two consumers of the same set with no
divergence.
Drop the local rebuild and import `layerInspectLive` directly.
Removes 9 lines of inline service plumbing + 9 redundant imports
from the api package (Config, DeadCode, Files, Git, Linter,
LintPartialFailures, Progress, Project, Reporter, Score).
Any future change to the default production layer stack (e.g. adding
a new service to runInspect's requirements) now propagates to both
api and the core's documented default automatically.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Plan section 4 flagged inspectAction's staged-arm + project-loop-arm
as having 'heavy duplicated JSON-report writing + surface filtering +
fail-on-level checking'. Each arm inlined the same 4-step post-scan
shape:
1. (if JSON mode) writeJsonReport(buildJsonReport({...}))
2. (if --annotations) printAnnotations(diagnostics, isJsonMode)
3. ciFailureDiagnostics = filterDiagnosticsForSurface(... 'ciFailure' ...)
4. (if !scoreOnly && shouldFailForDiagnostics) process.exitCode = 1
Extract to a single `finalizeScans({diagnostics, completedScans, mode,
diff, ...})` helper. Both arms package their results into the same
{diagnostics, completedScans} shape and delegate finalisation.
Slight LoC growth (+3 lines) — but adding a new finalisation step
(e.g. emitting an SDF / SARIF report alongside JSON) is now a
single-place change. The post-scan invariant is no longer scattered
across the function.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Four rules each carried their own `containsJsx` AST scanner. Two of
them — jsx-filename-extension and prefer-function-component — used
the exact same shape (find any JSXElement / JSXFragment in subtree).
Promote to utils/contains-jsx-element.ts under the clearer name
`containsJsxElement`. Both rules now import it.
The other two variants stay local:
- display-name.ts also matches `createElement(...)` calls (richer
detection — would need an extra param or a callback)
- no-multi-comp.ts stops at nested function / class boundaries
(semantically distinct walk strategy)
Behaviour-neutral.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Two utility modules each had their own isFunctionNode — the same
predicate as utils/is-function-like.ts (introduced earlier in this
series). One was already a type-guard (structurally identical to the
canonical helper), the other was a plain boolean returning the same
value.
- utils/create-component-prop-stack-tracker.ts
- utils/is-inside-server-only-scope.ts
Both now import isFunctionLike and drop their local copies.
Behaviour-neutral.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
The 12-line helper that recognises an ArrayExpression of primitive
literals (`[1, 2, 3]` etc.) was duplicated in:
- rules/correctness/no-array-index-as-key.ts
- rules/react-builtins/no-array-index-key.ts
Functionally equivalent (one had a redundant cast). Promote to
utils/is-all-literal-array-expression.ts.
Behaviour-neutral.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
The 9-line helper that extracts the static method name from a
MemberExpression callee (`obj.method` → `'method'`) was duplicated
byte-identical in no-pass-data-to-parent and
no-pass-live-state-to-parent. Promote to utils/get-call-method-name.ts.
Behaviour-neutral.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Two follow-up consolidations from the duplicate-helper sweep.
`isProjectBoundary` (`.git` or monorepo manifest) was implemented
byte-equivalent three times across core:
- detect-user-lint-config.ts
- load-config.ts
- project-info/detect-react-compiler.ts
Promote to packages/core/src/utils/is-project-boundary.ts. All three
sites now import.
`event-handler-reference.ts` exported a same-named `isFunctionLike`
that does NOT include FunctionDeclaration (declarations never appear
as JSX prop values). The name collided with the canonical
`utils/is-function-like.ts` helper that DOES include declarations.
Rename to `isInlineFunctionExpression` so future readers don't
mistake one for the other. Updated the one downstream consumer
(`collect-function-like-local-names.ts`) accordingly.
Behaviour-neutral.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Three rules each had their own `isFunctionLikeNode` — the boolean
check for 'is this AST node one of the three function shapes' — that
was already covered by the canonical `utils/is-function-like.ts`
helper introduced earlier in this cleanup series.
- rules/performance/rendering-usetransition-loading.ts
- rules/server/server-auth-actions.ts
- rules/state-and-effects/no-direct-state-mutation.ts
Each rule now imports the shared `isFunctionLike` and renames the
local call sites. The shared version is a type-guard (returns
`node is ArrowFunctionExpression | FunctionExpression |
FunctionDeclaration`) which is strictly more useful than the plain
`boolean` form these three returned.
Behaviour-neutral; -15 lines net.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Both jsx-key and jsx-no-useless-fragment had their own `hasKeyAttribute`
(walk JSX opening element attributes, return true on key={...}).
Functionally equivalent — slight code-shape differences. Promote to
utils/has-jsx-key-attribute.ts.
Behaviour-neutral.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
The 7-line helper that walks an AST parent chain to test ancestor
membership was duplicated byte-identical in:
- rules/react-builtins/exhaustive-deps-symbol-stability.ts
- semantic/closure-captures.ts
Promote to utils/is-ast-descendant.ts. Both callsites now import.
Behaviour-neutral.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Five sites each defined isFunctionLike — the test for whether an AST
node is one of the three function shapes (ArrowFunctionExpression,
FunctionExpression, FunctionDeclaration):
- rules/react-builtins/rules-of-hooks.ts (plain boolean, used local
FUNCTION_LIKE_TYPES)
- semantic/closure-captures.ts (plain boolean)
- semantic/control-flow-graph.ts (plain boolean)
- semantic/scope-analysis.ts (plain boolean)
- rules/state-and-effects/utils/effect/react.ts (type-guard form
with null/undefined handling — strictly more useful)
Promote the type-guard form from effect/react.ts to
utils/is-function-like.ts and have all five sites import from it.
The five callsites all pass a non-null EsTreeNode, so the narrowed
signature (EsTreeNode | null | undefined → type guard) is a strict
generalisation. The four sites that previously kept their own
FUNCTION_LIKE_TYPES import no longer need it (the predicate's
internals use isNodeOfType directly).
Behaviour-neutral; -22 lines net.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
The same 8-line helper that walks an AST node's parent chain to find
its enclosing Program root was inlined byte-identical in SEVEN
places:
- rules/react-builtins/display-name.ts
- rules/react-builtins/jsx-no-undef.ts
- rules/react-builtins/react-in-jsx-scope.ts
- utils/find-import-source-for-name.ts
- utils/find-variable-initializer.ts
- utils/wrap-with-semantic-context.ts
- rules/state-and-effects/utils/effect/get-program-analysis.ts (this
one was slightly different — returned the narrower
EsTreeNodeOfType<'Program'> type and used isNodeOfType for the
check)
Promote the better-typed version from get-program-analysis.ts to
utils/find-program-root.ts and have all seven sites import from it.
Each callsite that previously got back `EsTreeNode | null` now gets
`EsTreeNodeOfType<'Program'> | null` — strict narrowing, no behaviour
change because every existing caller only uses the result through the
narrow shape anyway.
Behaviour-neutral; -42 lines net.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
`flattenCalleeName` (flatten a call-expression callee — Identifier
or MemberExpression chain — to a dotted string) was inlined in two
places with slightly different implementations:
- no-multi-comp.ts: recursive, handles arbitrarily deep chains
- build-same-file-memo-registry.ts: shallow, handles only one-level
member expressions
Both versions produce the same result for the HOC-name detection
they're used for (memo, React.memo, etc. are never 3+ levels deep).
Consolidate behind utils/flatten-callee-name.ts using the recursive
form so any future MemberExpression chain root resolves uniformly.
`getCallExpressionCalleeName` in exhaustive-deps.ts and rules-of-hooks.ts
is structurally similar but each variant is intentionally restricted
(exhaustive-deps allows any obj.prop; rules-of-hooks restricts to
React.prop only). Left as-is — they encode rule-specific intent.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
The 10-element set of Solid / Qwik / Voby / Vidode runtime package
names was duplicated byte-identical between:
- utils/non-react-jsx-dialect.ts (already had the canonical detector
helpers)
- rules/react-builtins/no-unknown-property.ts (carried its own copy)
Export the set from utils/non-react-jsx-dialect.ts (its natural home,
since the file is literally named after this concept) and have
no-unknown-property.ts import it instead of redeclaring.
Behaviour-neutral.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Follow-up to the previous consolidation: the same 3-element set was
ALSO duplicated byte-identical in two other semantic helpers:
- semantic/control-flow-graph.ts
- semantic/closure-captures.ts
Both now import from the canonical constants/js.ts. The semantically
distinct FUNCTION_LIKE_TYPES in find-variable-initializer.ts is left
alone — it's a superset that adds MethodDefinition + Program (for
hoisting-boundary detection), so it serves a different purpose.
Behaviour-neutral.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
build-same-file-memo-registry.ts maintained its own `HOC_NAMES_FOR_MEMOISATION`
that hand-redeclared the four React-native HOCs (memo / forwardRef /
React.memo / React.forwardRef) before extending with the third-party
ones (observer / observable / lazy / withTracking).
Derive the React-side prefix from the canonical `REACT_HOC_NAMES`
constant in constants/react.ts via spread. Adding a new React-native
HOC in one place now extends both detectors automatically.
Behaviour-neutral.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
`FUNCTION_LIKE_TYPES` (the 3 ESTree node-type names for function
forms — FunctionDeclaration, FunctionExpression, ArrowFunctionExpression)
was duplicated byte-identical in:
- packages/oxlint-plugin-react-doctor/src/plugin/semantic/scope-analysis.ts
- packages/oxlint-plugin-react-doctor/src/plugin/rules/react-builtins/rules-of-hooks.ts
Promote to constants/js.ts (sits next to LOOP_TYPES). Both consumers
now import the same source of truth — adding a new function form
(e.g. if ESTree ever adds an explicit `MethodDefinition` to the
function-like family) propagates to both without drift.
Behaviour-neutral.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Two rules — `no-pass-data-to-parent` and `no-pass-live-state-to-parent`
— each carried a 100+ element set named `ITERATOR_METHOD_NAMES` to
filter out method-call shapes that 'consume' data rather than handing
it back to a parent (Array iterators, EventEmitter subscriptions,
Promise then/catch/finally, Set/Map ops, logger/telemetry, imperative
action verbs). The two sets were byte-identical.
The previous name `ITERATOR_METHOD_NAMES` was misleading — iterators
were only one of the seven categories. Promote to a new shared
`constants/data-sink-method-names.ts` module under the more accurate
name `DATA_SINK_METHOD_NAMES`. Adding a new EventEmitter variant /
imperative-action verb now propagates to both detectors automatically
instead of drifting in one and not the other.
The third local `ITERATOR_METHOD_NAMES` in `js-batch-dom-css.ts` is
unaffected — it's a tiny subset (just the 6 Array.prototype iterators)
used for a structurally different purpose (DOM-style batch detection),
so consolidating would over-broaden it. Left in place.
Net: 1 new 110-line module, ~210 lines removed from the two rule
files. Behaviour-neutral.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
The 4-element `{memo, forwardRef, React.memo, React.forwardRef}` set
was duplicated verbatim in:
- exhaustive-deps.ts (as REACT_HOC_NAMES, 4-element set)
- rules-of-hooks.ts (as REACT_HOC_NAMES, identical 4-element set)
- no-multi-comp.ts (as HOC_NAMES, identical 4-element set under
a different name)
This is exactly the 'duplicated helpers where canonical utilities
already exist' shape the cleanup plan flagged: three rule files
maintaining their own private copy of the same 4-element set.
Promote REACT_HOC_NAMES to constants/react.ts (the canonical home for
shared React-related constants — sits alongside EFFECT_HOOK_NAMES,
HOOKS_WITH_DEPS, etc.) and have all three rule files import from it.
build-same-file-memo-registry.ts keeps its own HOC_NAMES_FOR_MEMOISATION
set — that one extends to MobX 'observer', legend-state 'observable',
'lazy', and 'withTracking', so it's semantically distinct (the
'wrappers that produce memoised output' set, not 'React's two
component-wrapping HOCs').
Behaviour-neutral. Adds 7 lines to constants/react.ts; removes 20
lines across the three rule files.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Completes the third targeted P8 extraction (the previously-deferred
'stretch' from the cleanup plan). The 7-helper cluster that answers
'is this captured symbol structurally stable across renders?' now
lives in its own module; the low-level helpers it shares with the
main rule body (unwrapExpression, getHookName, isOutsideAllFunctions,
TRANSPARENT_WRAPPER_TYPES) sit in a sibling 'low-level' module so the
main rule and the symbol-stability module both import without a
circular dependency.
New module layout for the exhaustive-deps rule:
exhaustive-deps.ts — analysis logic (1144L)
exhaustive-deps-symbol-stability.ts — stability cluster (168L)
exhaustive-deps-low-level.ts — shared helpers (80L)
exhaustive-deps-settings.ts — config resolver (47L)
exhaustive-deps-messages.ts — diagnostic strings (55L)
Main rule file: 1311 -> 1144 lines (-167). Behaviour-neutral; the
7 cluster helpers, the 3 low-level helpers, and the
TRANSPARENT_WRAPPER_TYPES set are re-imported into the main rule
under the same names.
The previous `docs(exhaustive-deps): annotate why unwrapExpression
doesn't reuse stripParenExpression` (6e20c3c4) comment is preserved
verbatim in the new low-level module — same rationale, new home.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Section 4 of the cleanup plan flagged only-export-components.ts (842
lines) as 'single rule, complex tree-walking; acceptable' but it
follows the same shape as the jsx-no-new-X-as-prop sister rules:
~25% of the file is hand-curated data tables (basenames, expression
types, path segments) ahead of the actual analysis logic.
Apply the same pattern. The new
only-export-components-tables.ts holds:
- NOT_REACT_COMPONENT_EXPRESSION_TYPES (12 expression-type names)
- NON_FAST_REFRESH_PATH_SEGMENTS (~20 dir-name path segments)
- ENTRY_POINT_BASENAMES (~38 conventional entry-point filenames)
- UTILITY_FILE_BASENAMES (~85 utility / helper filename conventions)
only-export-components.ts: 842 -> 651 lines (-191 / -23%).
Smaller pattern-style constants (ASSET_FILE_BASENAME_PATTERN,
HOOK_FILE_BASENAME_PATTERN, UTILITY_BASENAME_SUFFIX_PATTERN,
NODE_DEFINITION_BASENAME_PATTERN) stay inline with their helpers
since they're single-line regexes whose context-comments are short
and whose helpers (isAssetOrUtilityFile, ...) read directly from them.
Behaviour-neutral. Same tests pass.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Sister rules jsx-no-new-array-as-prop.ts and jsx-no-new-object-as-prop.ts
follow the same shape as jsx-no-new-function-as-prop.ts: ~80% of each
file was a hand-curated allow-list of prop names + suffix patterns
that get exempted from the 'fresh literal per render' warning, ahead
of ~150 lines of analysis logic.
Apply the same extraction:
- jsx-no-new-array-as-prop-tables.ts holds DATA_ARRAY_PROP_NAMES
(~165 names) and DATA_ARRAY_PROP_SUFFIXES (~120 suffixes).
- jsx-no-new-object-as-prop-tables.ts holds ALWAYS_FRESH_OBJECT_PROPS
(2 names — React API contract), CONFIG_OBJECT_PROP_NAMES (~130
names) and CONFIG_OBJECT_PROP_SUFFIXES (~85 suffixes).
The corresponding rule files import the same readonly collections
through named imports.
- jsx-no-new-array-as-prop.ts: 496 -> 198 lines (-298 / -60%)
- jsx-no-new-object-as-prop.ts: 430 -> 184 lines (-246 / -57%)
Behaviour-neutral. No tests changed.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Section 4 of the cleanup plan flagged jsx-no-new-function-as-prop.ts
(837 lines) as 'mostly a giant lookup-table of handler names plus a
small analysis'. Four hand-curated allow-list tables took up ~260
lines of the file:
- ONE_SHOT_LIFECYCLE_HANDLER_NAMES (~180 lines, ~150 names)
- ONE_SHOT_HANDLER_SUFFIXES (~25 lines)
- ACCESSOR_PREDICATE_PREFIXES (~12 lines)
- SAFE_RECEIVER_NAMES (~35 lines)
Move all four into a sibling jsx-no-new-function-as-prop-tables.ts
module. Behaviour-neutral — the rule imports the same readonly
collections through named imports.
jsx-no-new-function-as-prop.ts: 837 -> 575 lines (-262 / -31%). The
file now opens directly on the rule's analysis logic instead of
~180 lines of curated handler names.
Same pattern as P8's exhaustive-deps-messages.ts extraction.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
The shared core/plugin/utils/strip-paren-expression.ts util covers
essentially the same six wrapper types — and was investigated as a
replacement for the rule-local unwrapExpression. The blocker is that
TRANSPARENT_WRAPPER_TYPES is ALSO read directly by the member-chain
walker further down the file (`isTransparentWrapper = ...` check),
so swapping unwrapExpression for stripParenExpression alone would
split the same intent across two modules. Comment records the
tradeoff so future cleanup attempts don't re-trip on the same
investigation.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Completes P4 of the cleanup plan.
The fixtures/ directory (basic-react, monorepo-with-root-react,
nextjs-app, pnpm-catalog-workspace, ..., 14 project shapes + the
.oxlintignore stub) move to packages/core/tests/fixtures/, its
canonical home — project-shape fixtures are core's territory under
AGENTS.md, and the previous packages/react-doctor/tests/fixtures/
location forced packages/api/tests/diagnose.test.ts to reach
across packages with '../../react-doctor/tests/fixtures'.
Seven more core-internal tests move to packages/core/tests/:
- discover-project.test.ts (1092 lines — biggest remaining
architectural-drift test file)
- find-monorepo-root.test.ts
- read-ignore-file.test.ts
- can-oxlint-extend-config.test.ts
- check-dead-code.test.ts
- merge-and-filter-diagnostics.test.ts (inlined the 5-line writeFile
+ buildDiagnostic helpers from regressions/_helpers.ts to avoid
coupling core tests to the regressions harness)
Consumers that stay in react-doctor/tests/ (CLI / end-to-end /
regression-style tests) update their FIXTURES_DIRECTORY constant
to point at the new core/tests/fixtures location:
- packages/api/tests/diagnose.test.ts
- packages/react-doctor/tests/inspect.test.ts
- packages/react-doctor/tests/inspect-surface-filter.test.ts (this
tests the CLI inspect() wrapper, not the core orchestrator, so
it stays in react-doctor/tests/)
- packages/react-doctor/tests/namespace-hooks.test.ts
- packages/react-doctor/tests/regressions/rn-package-scoping.test.ts
- packages/react-doctor/tests/run-oxlint/_helpers.ts
After this final batch:
- 21 core-internal test files have moved from react-doctor/tests/
to packages/core/tests/ (was 14 in the previous batches).
- Core has 346 tests across 35 files (was 247 / 29).
- react-doctor has 1317 tests across 103 files.
- Total tests unchanged (1670).
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Continuation of the previous P4 batch. These tests have no fixture
dependency, no CLI dependency, and exercise pure core helpers:
- apply-severity-controls.test.ts
- build-rule-severity-controls.test.ts
- calculate-score.test.ts
- filter-diagnostics.test.ts
- resolve-rule-severity-override.test.ts
- schemas.test.ts (uses @react-doctor/core/schemas subpath import)
- validate-config-types.test.ts
to-json-report.test.ts STAYS in react-doctor/tests because it tests
the toJsonReport export from packages/react-doctor/src/index.ts (a
CLI helper, not a core helper).
After two batches, total: 14 core-internal tests moved out of
react-doctor/tests/, ~140 tests now living in their canonical
package. Test count unchanged overall.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Per AGENTS.md: 'packages/core/tests/ — service tests + run-inspect
orchestration tests; packages/react-doctor/tests/ — CLI +
end-to-end fixture tests'.
Seven test files were testing pure core helpers from the wrong
package (long-standing architectural drift; tests imported from
@react-doctor/core to exercise core internals):
- dedupe-diagnostics.test.ts
- evaluate-suppression.test.ts
- errors.test.ts
- filter-for-surface.test.ts
- find-jsx-opener-span.test.ts
- format-error-chain.test.ts
- parse-tailwind-major-minor.test.ts
These have no CLI dependency, no fixture-directory dependency, and no
end-to-end concerns. Move them into packages/core/tests/. Imports
keep using @react-doctor/core (core supports self-imports — see the
existing tests/run-inspect.test.ts).
74 tests migrated; total test count unchanged (185 + 1478 = 1663
matches the pre-move 111 + 1552 = 1663).
Larger fixture-dependent tests (discover-project.test.ts,
find-monorepo-root.test.ts, etc.) stay in place for now — moving them
requires either moving the fixtures directory or threading parent-
relative paths through the tests. Documented as a follow-up.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
install-git-hook-config-managers.ts had five near-identical functions
(installSimpleGitHooks, installGhooks, installPreCommitNpm,
installPrettyQuick, installPackageJsonPreCommitString) that each:
1. Read package.json
2. Walked to or created a specific nested config object
3. Appended the react-doctor command to a leaf value (newline-joined
string OR array of strings)
4. Wrote the file back
The differences were the path ("simple-git-hooks" vs "config.ghooks"
vs "pre-commit" vs "gitHooks"...) and the leaf shape (string vs
array). 100+ lines of copy-pasted boilerplate per manager.
Collapse to one installPackageJsonHook(options, { kind, path,
leafShape }) strategy. Each manager becomes a 5-line config:
export const installSimpleGitHooks = (options) =>
installPackageJsonHook(options, {
kind: GitHookKind.SimpleGitHooks,
path: ["simple-git-hooks", "pre-commit"],
leafShape: "string",
});
Yorkie + GitHooksJs were previously special-cased in
install-git-hook.ts because they reused
installPackageJsonPreCommitString as a parameterized helper. They
now get their own one-liner entry points, which makes the dispatch
table flat (every kind -> its own installer) and removes the
generic helper.
Lefthook / pre-commit / overcommit stay separate because they
write text/YAML files, not JSON.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
The exhaustive-deps rule file (1363 lines) opened with 17 single-line
buildXMessage builder functions and a separate resolveSettings block
before any analysis logic.
Split out:
- exhaustive-deps-messages.ts: 17 user-facing diagnostic strings
- exhaustive-deps-settings.ts: ExhaustiveDepsSettings interface +
resolveExhaustiveDepsSettings (renamed from resolveSettings for
namespace clarity)
The main rule file drops from 1363 -> 1307 lines and now opens
directly on analysis logic instead of message bookkeeping. Symbol-
stability helpers (symbolHasStableHookOrigin / symbolHasStableValue /
symbolHasStableFunctionOrigin) stay co-located with the rule for now
because they depend on later-declared helpers (unwrapExpression,
isOutsideAllFunctions); extracting them is a larger surgery.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Two silent paths existed:
- --silent created an inline Proxy({} as Console.Console, ...) and
provided it via Effect.provideService(Console.Console, ...).
- --json's installSilentConsole() monkey-patched globalThis.console
with inline noop closures.
Extract makeNoopConsole() into cli/utils/noop-console.ts. Both silent
paths now build their noop from the same source. The Proxy cast is
gone — the noop is a concrete object with the exact Effect v4
Console.Console method shape, so Effect.provideService takes it
without a cast.
The JSON-mode global-patch route stays for now (the surrounding CLI
command body is still imperative), but it copies methods from the
same canonical object. When the command body becomes Effect-typed,
the monkey-patch can be deleted without touching the noop
definition.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
resolveCliInspectOptions (CLI flag → InspectOptions) and
mergeInspectOptions (inside inspect()) both applied userConfig
fallback on the same fields (lint, deadCode, verbose,
respectInlineDisables). The CLI always passes configOverride:
userConfig so inspect() sees the same config the CLI loaded, but the
defaults were resolved twice — and if the two ever drifted (e.g.
configOverride loaded via a different path) the layered defaults
could diverge silently.
resolveCliInspectOptions now ONLY does flag-specific work:
- The flag-to-computed-field translations stay (scoreOnly, silent,
outputSurface, isCi)
- noScore stays because it folds the --score=false flag with
userConfig.noScore; that's flag-and-config, not pure config
- Pure config-fallback fields (lint, deadCode, verbose,
respectInlineDisables) pass through as the flag value (undefined
when unset) and inspect()'s mergeInspectOptions applies the
userConfig default once.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Every generated rule-registry entry stored framework, category, and
severity at BOTH the outer entry level AND inside the inner rule:
{...} object. Consumers read framework/category/severity from
whichever side they happened to import — rules.ts read the outer,
react-doctor-plugin.ts and core/runners/oxlint/config.ts read the
inner.
Stop emitting the outer copies. Keep them only on the inner Rule
object (where they're set by the codegen spread). The outer entry
now ships:
{ key, id, source, originallyExternal, rule: { ...sourceRule,
framework, category, tags? } }
Consumers in rules.ts now read severity/framework via entry.rule.*
through a small toKeyedSeverity helper.
rule-registry.ts: 4324 -> 3469 lines (-855 / -20%).
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>