Commit Graph

1865 Commits

Author SHA1 Message Date
Cursor Agent d6e6377be7 refactor(tests): collapse collectRuleHits ProjectInfo construction into buildTestProject
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>
2026-05-25 17:04:56 +00:00
Cursor Agent af34ef6666 style: drop unused EsTreeNode type import from forbid-elements
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>
2026-05-25 16:49:02 +00:00
Cursor Agent 76a971c931 refactor(oxlint-plugin): drop transitional re-export of isInlineFunctionExpression
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>
2026-05-25 16:45:18 +00:00
Cursor Agent 809d02b7cf style: pnpm format after the latest consolidation sweep
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>
2026-05-25 16:40:24 +00:00
Cursor Agent d09a994c22 refactor(oxlint-plugin): promote isInlineFunctionExpression + apply across 7 rule files
`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>
2026-05-25 16:37:51 +00:00
Cursor Agent 565f9c07f5 refactor(oxlint-plugin): replace inline 3-type function checks with isFunctionLike in 6 more rule files
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>
2026-05-25 16:21:55 +00:00
Cursor Agent f14eed5483 refactor(oxlint-plugin): no-array-index-as-key uses isFunctionLike for boundary checks
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>
2026-05-25 16:12:32 +00:00
Cursor Agent 6d9103c863 refactor(oxlint-plugin): no-unstable-nested-components uses isFunctionLike for the parent-walker check
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>
2026-05-25 16:09:11 +00:00
Cursor Agent 61edc3f0b7 refactor(oxlint-plugin): function-body-has-return-with-value uses FUNCTION_LIKE_TYPES
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>
2026-05-25 16:05:06 +00:00
Cursor Agent 78d11d4a45 refactor(oxlint-plugin): walk-inside-statement-blocks uses canonical isFunctionLike
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>
2026-05-25 16:01:51 +00:00
Cursor Agent 7e9be6fe77 refactor(oxlint-plugin): forbid-elements reuses canonical flattenCalleeName
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>
2026-05-25 15:55:11 +00:00
Cursor Agent 14723f3741 refactor(oxlint-plugin): no-unknown-property uses canonical fileImportsNonReactJsxDialect
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>
2026-05-25 15:51:00 +00:00
Cursor Agent c518fdfae4 refactor(oxlint-plugin): consolidate compileGlob across four rule files
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>
2026-05-25 15:47:03 +00:00
Cursor Agent 02732a0df8 refactor(oxlint-plugin): consolidate flattenJsxName helper (2 of 4 variants)
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>
2026-05-25 15:41:22 +00:00
Cursor Agent f2b4715731 style: remove unused imports left behind by the helper-consolidations
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>
2026-05-25 15:30:38 +00:00
Cursor Agent a75ccedc1f style: run pnpm format across files touched by the cleanup series
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>
2026-05-25 15:26:38 +00:00
Cursor Agent c69e321f5f docs(core): annotate InspectOptions engine vs. rendering grouping
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>
2026-05-25 15:21:25 +00:00
Cursor Agent fe274c375e refactor(oxlint-plugin): has-binding-named uses canonical collectPatternNames
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>
2026-05-25 15:14:04 +00:00
Cursor Agent 7dff4727b9 refactor(api): use core's exported layerInspectLive instead of reimplementing it
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>
2026-05-25 15:06:52 +00:00
Cursor Agent 838831adfe refactor(cli): extract finalizeScans helper from inspectAction's two arms
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>
2026-05-25 15:03:33 +00:00
Cursor Agent 950f3aa017 refactor(oxlint-plugin): consolidate containsJsx across two rule files
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>
2026-05-25 14:58:10 +00:00
Cursor Agent d20302e786 refactor(oxlint-plugin): use shared isFunctionLike for two more isFunctionNode sites
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>
2026-05-25 14:49:54 +00:00
Cursor Agent a257f86063 refactor(oxlint-plugin): consolidate isAllLiteralArrayExpression helper
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>
2026-05-25 14:43:45 +00:00
Cursor Agent 6011d901f2 refactor(oxlint-plugin): consolidate getCallMethodName helper
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>
2026-05-25 14:39:32 +00:00
Cursor Agent 217ed4e0db refactor(core): consolidate isProjectBoundary + rename event-handler isFunctionLike
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>
2026-05-25 14:34:13 +00:00
Cursor Agent f20551edc6 refactor(oxlint-plugin): use shared isFunctionLike instead of isFunctionLikeNode in three more rules
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>
2026-05-25 14:23:28 +00:00
Cursor Agent 379417e8f7 refactor(oxlint-plugin): consolidate hasJsxKeyAttribute helper
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>
2026-05-25 14:17:08 +00:00
Cursor Agent e31bcf0b35 refactor(oxlint-plugin): consolidate two copies of isAstDescendant
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>
2026-05-25 14:11:51 +00:00
Cursor Agent 53e2ccf05a refactor(oxlint-plugin): consolidate five copies of isFunctionLike
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>
2026-05-25 14:07:57 +00:00
Cursor Agent 5be94e9e01 refactor(oxlint-plugin): consolidate seven copies of findProgramRoot
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>
2026-05-25 14:01:26 +00:00
Cursor Agent d4e93509a7 refactor(oxlint-plugin): consolidate flattenCalleeName into shared utility
`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>
2026-05-25 13:49:17 +00:00
Cursor Agent 355ef7001f refactor(oxlint-plugin): consolidate duplicate NON_REACT_JSX_DIALECT_PACKAGES
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>
2026-05-25 13:44:28 +00:00
Cursor Agent 2da10c72a8 refactor(oxlint-plugin): consolidate two more FUNCTION_LIKE_TYPES sites
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>
2026-05-25 13:39:56 +00:00
Cursor Agent 92475da776 refactor(oxlint-plugin): derive HOC_NAMES_FOR_MEMOISATION from REACT_HOC_NAMES
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>
2026-05-25 13:35:04 +00:00
Cursor Agent 9426276434 refactor(oxlint-plugin): consolidate FUNCTION_LIKE_TYPES into constants/js
`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>
2026-05-25 13:32:12 +00:00
Cursor Agent 1488b9ea9f refactor(oxlint-plugin): consolidate duplicate DATA_SINK_METHOD_NAMES (was ITERATOR_METHOD_NAMES)
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>
2026-05-25 13:27:09 +00:00
Cursor Agent a260da6a88 refactor(oxlint-plugin): consolidate REACT_HOC_NAMES across three rule files
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>
2026-05-25 13:21:08 +00:00
Cursor Agent 3b5e304b64 refactor(oxlint-plugin): extract exhaustive-deps symbol-stability cluster
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>
2026-05-25 13:13:33 +00:00
Cursor Agent 76874c0af3 refactor(oxlint-plugin): extract only-export-components data tables
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>
2026-05-25 13:03:35 +00:00
Cursor Agent 6899a46996 refactor(oxlint-plugin): extract array + object data tables (mirror of jsx-no-new-function-as-prop)
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>
2026-05-25 12:52:52 +00:00
Cursor Agent 68520ade7f refactor(oxlint-plugin): extract jsx-no-new-function-as-prop data tables
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>
2026-05-25 12:43:51 +00:00
Cursor Agent 6e20c3c4ee docs(exhaustive-deps): annotate why unwrapExpression doesn't reuse stripParenExpression
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>
2026-05-25 12:32:16 +00:00
Cursor Agent f5e10af405 test: move fixtures/ + 7 fixture-dependent core tests to core/tests
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>
2026-05-25 12:27:01 +00:00
Cursor Agent 1f3a5509e8 test: move 7 more core-helper unit tests to core/tests
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>
2026-05-25 12:13:04 +00:00
Cursor Agent c210272254 test: move pure core unit tests from react-doctor/tests to core/tests
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>
2026-05-25 12:09:25 +00:00
Cursor Agent 9287db01d8 refactor(cli): collapse 5 install-X-hook functions behind one strategy
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>
2026-05-25 12:05:21 +00:00
Cursor Agent dacde53079 refactor(oxlint-plugin): extract exhaustive-deps message + settings modules
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>
2026-05-25 12:00:15 +00:00
Cursor Agent df89dcde7a refactor(cli): one canonical no-op Console for silent and JSON modes
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>
2026-05-25 11:54:15 +00:00
Cursor Agent d5d504c862 refactor(cli): stop double-applying userConfig fallback in option resolution
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>
2026-05-25 11:49:16 +00:00
Cursor Agent ae7752ba86 refactor(oxlint-plugin): collapse duplicate framework/category/severity from rule-registry
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>
2026-05-25 11:45:17 +00:00