Commit 2a1b0ae
fix(react-doctor): tighten state-and-effects rules against false positives (#172)
Audit pass on `main` after the consolidation/AGENTS.md cleanups (#167, #169).
Targets eight specific FP/FN patterns surfaced by re-reading the merged code
end-to-end and checking each rule against its own intent docstring.
High
- H1 unify release detection between `effectHasCleanupRelease` and
`cleanupReleasesSubscription`: both now share `isCleanupReturn` /
`isReleaseLikeCall` / `containsReleaseLikeCall`. Previously
`prefer-use-sync-external-store` and `prefer-use-effect-event` could
disagree on whether a cleanup with a generic teardown verb (`cleanup()`,
`dispose()`) counted as a release, producing inconsistent diagnostics.
- H2 require function-shaped return for `isExternalSyncEffect`: any
`return <expr>` previously qualified the effect as "external sync" and
silently disabled chain detection, so `return null` / `return 42` /
`return condition && cleanup` would mask real chains. We now only
treat function-shaped returns (Arrow, FnExpr, Call, Identifier) as
cleanup.
- H3 directional version gating: `prefer-use-effect-event` is a
"prefer-newer-api" rule and was firing on projects where the React
major couldn't be detected. Gating now records whether each rule is
"prefer-newer-api" (skip when version is unknown) or
"deprecation-warning" (keep firing when version is unknown).
Medium
- M1 receiver-gate ambiguous direct callees: `track`, `logEvent`, `del`
removed from `EVENT_TRIGGERED_SIDE_EFFECT_CALLEES` — they were too
generic as bare identifiers and produced FPs on user-defined helpers.
Receiver-gated member calls (`analytics.track(...)`) still fire.
- M2 extend `findSubHandlerForEnclosingFunction` to FunctionDeclaration
and AssignmentExpression shapes — `function handler() {}` and
`let h; h = (e) => {}` are now recognized alongside `const h = ...`.
- M3 deep-walk for `noPropCallbackInEffect`: the rule previously only
scanned top-level effect statements and missed the very common
`if (didChange) onChange(state)` shape. Walk now descends through
control-flow blocks but stops at function boundaries (so deferred
sub-handlers like `setTimeout(() => onChange(state))` stay the
domain of `prefer-use-effect-event`).
- M4 `collectWrittenStateNamesInEffect` no longer counts setter calls
inside nested functions for chain detection — deferred writes
(`setTimeout(() => setX(...))`) are not synchronous chain triggers.
Low
- L1 `noMirrorPropEffect` now accepts multi-element deps as long as the
mirrored prop's root is in the deps array. The prior
"exactly one dep" requirement missed legitimate mirror shapes with an
unrelated extra dep.
- L2 (paired with H1/H2) `effectHasCleanupRelease` "return Identifier"
now narrows to known bound release names so a stray non-function
identifier return doesn't silently look like a release.
- L3 extend `prop-stack-barrier.test.ts` to cover all four
`createComponentPropStackTracker` consumers (`no-derived-useState`,
`no-prop-callback-in-effect`, `no-mirror-prop-effect`,
`prefer-use-effect-event`) so the empty-frame barrier semantic is
regression-tested for every rule that depends on it.
- L4 extract a sibling `createComponentBindingStackTracker` and migrate
`noEffectEventInDeps` onto it — the third inlined push/pop scaffold
was effectively a copy-paste of the prop-stack one specialised to
binding sets.
Tests
- 634 passing (up from 619). New regressions cover each behavior change
above plus FP guards (e.g. mirror shape with prop root NOT in deps
must not fire, sub-handler reads must not fire `no-prop-callback-in-effect`).
- `tests/run-oxlint.test.ts` now passes `reactMajorVersion: 19` for the
basic / Next / TanStack Start fixtures so they exercise the
prefer-newer-api rules under a known React major.
Co-authored-by: Cursor <cursoragent@cursor.com>1 parent 36518ba commit 2a1b0ae
7 files changed
Lines changed: 761 additions & 181 deletions
File tree
- packages/react-doctor
- src
- plugin
- rules
- tests
- regressions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
381 | 381 | | |
382 | 382 | | |
383 | 383 | | |
384 | | - | |
385 | | - | |
386 | | - | |
387 | | - | |
388 | | - | |
389 | | - | |
390 | | - | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
391 | 416 | | |
392 | 417 | | |
393 | 418 | | |
394 | 419 | | |
395 | 420 | | |
396 | 421 | | |
397 | | - | |
398 | 422 | | |
399 | 423 | | |
400 | | - | |
401 | | - | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
402 | 428 | | |
403 | 429 | | |
404 | 430 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
568 | 568 | | |
569 | 569 | | |
570 | 570 | | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
571 | 578 | | |
572 | 579 | | |
573 | 580 | | |
574 | 581 | | |
575 | 582 | | |
576 | 583 | | |
577 | | - | |
578 | 584 | | |
579 | 585 | | |
580 | 586 | | |
| |||
584 | 590 | | |
585 | 591 | | |
586 | 592 | | |
587 | | - | |
588 | | - | |
589 | 593 | | |
590 | 594 | | |
591 | 595 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
22 | 33 | | |
23 | 34 | | |
24 | 35 | | |
| |||
476 | 487 | | |
477 | 488 | | |
478 | 489 | | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
0 commit comments