* test: add failing tests for issue #1756 false positives
Covers three patterns:
- Chained timer cleared by helper (timer reassigned, helper clears current value)
- Timer allocated in nested function after await with guard
- AbortController cleanup via abort event handler
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
* docs: document issue #1756 root causes with failing regression tests
Three false positive patterns in effect-needs-cleanup:
1. **Chained timer**: Timer reassigned inside its own callback, cleared by
helper. Rule doesn't recognize mutable handle semantics - clearTimeout(timer)
clears whatever is currently in the variable.
2. **Async guarded allocation**: Guard in async caller protects sync callee
allocation. Rule recognizes guards inside promise callbacks (#1241) but not
guards protecting function calls that contain allocations.
3. **AbortController delegation**: signal.addEventListener('abort', ...) removes
listener, cleanup calls controller.abort(). Rule recognizes direct {signal}
but not event-based delegation pattern.
Tests currently fail as expected. Implementations need to extend:
- Timer cleanup to understand mutable handles
- Guard tracking to follow call boundaries
- AbortController detection to recognize abort event delegation
Related: #306, #1241, #1594, #1736
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
* wip: partial AbortController delegation fix (needs debugging)
Added `hasAbortSignalDelegatedCleanup` helper to recognize the pattern:
- signal.addEventListener('abort', () => removeEventListener(...))
- cleanup calls controller.abort()
Logic handles both direct (controller.signal) and destructured
(const { signal } = controller) signal references.
Current status: Helper added and integrated into effectHasCleanupForUsage,
but tests still fail. Needs debugging of AST traversal logic to correctly
identify and match the abort listener with the cleanup abort() call.
The other two patterns (chained timer, async guard) still need implementation.
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
* fix(rule): recognize owned effect cleanup paths
* fix(rule): preserve unsafe timer diagnostics
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
Co-authored-by: Aiden Bai <aiden@million.dev>
* fix(nextjs-no-side-effect-in-get-handler): track safe Headers passed through helpers
Fixes#1757
The rule now tracks when a locally-constructed safe object (like
`new Headers()`) is passed as an argument to a same-file helper
function. The corresponding parameter in that helper is treated as
safe, preventing false positives when the helper mutates the response
headers.
Added `collectHelperParameterSafeBindings` utility that maps call
arguments to helper parameters, extending the set of safe bindings
when scanning helper bodies.
Regression tests added for:
- Headers passed to helper as first parameter
- Headers passed as second parameter
- Headers passed via destructured parameter
- Module-level Map still correctly flagged (not locally scoped)
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
* chore: add changeset for #1757 fix
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
* fix(rule): scope GET helper safety by call
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
Co-authored-by: Aiden Bai <aiden@million.dev>
* fix(async-defer-await): recognize 'live' as a liveness guard name
The async-defer-await rule was firing on post-await liveness guards that
check a 'live' flag, commonly used in React effects to detect unmounting
during async operations.
The issue: 'live' was not in the CANCELLATION_NAME_FRAGMENTS list, so
guards like 'if (!run.live) return' were not recognized as staleness
checks.
The fix adds 'live' to the fragments list so it matches patterns like:
- run.live
- isLive
- liveness
- stillLive
Closes#1758
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
* chore: add changeset for async-defer-await fix
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
* fix(rule): narrow live cancellation guards
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
Co-authored-by: Aiden Bai <aiden@million.dev>
* fix: exempt send/resend/notify/email mutations from cache invalidation requirement
These operations are cache-effect-free (sending emails, notifications, codes)
with no server data that could go stale. Added send, resend, notify, and email
to READ_ONLY_MUTATION_WORDS alongside download, export, validate, etc.
Closes#1759
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
* chore: add changeset for send/resend/notify/email exemption
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
* fix(rule): narrow magic link mutations
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
Co-authored-by: Aiden Bai <aiden@million.dev>
* fix: respect 'use no memo' directive in react-compiler-no-manual-memoization rule
When a component has the 'use no memo' directive, React Compiler skips
optimization for that component, so manual memoization (useMemo,
useCallback, memo) is still needed.
This change adds support for detecting the 'use no memo' directive and
suppresses the react-compiler-no-manual-memoization rule in those cases.
- Add hasUseNoMemoDirective utility function
- Update rule to check for directive in enclosing function (useMemo/useCallback)
- Update rule to check for directive in wrapped component (memo)
- Add comprehensive tests including regression tests
Fixes#1749
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
* chore: add changeset for use no memo directive fix
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
* fix: respect React Compiler opt-out directives
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
Co-authored-by: Aiden Bai <aiden@million.dev>
* fix: classify fragment returns of translation text as text-producing
Fragment returns mixing translation text elements (fbt/fbs) with literal
text now classify correctly as text-producing components. The fragment
is render-transparent, so both halves land in the caller's <Text>.
Fixes#1731
- Extract isTranslationTextJsxRoot helper that recursively checks
fragments
- Fragment children that are all translation elements (plus literals)
pass the check
- Fragment children with non-text elements still fail (guard test added)
- Add regression coverage for the reported mixed-literal case
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
* chore: add changeset for fragment translation text fix
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
* fix: keep fragment translation classification conservative
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
Co-authored-by: Aiden Bai <aiden@million.dev>
* fix: prevent false positive in js-set-map-lookups for String().includes()
- Add check to ensure String() refers to global constructor, not shadowed variable
- Add regression tests for the String() constructor case
- Add corpus regression test file
Closes#1733
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
* fix: recognize global String substring lookups
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
Co-authored-by: Aiden Bai <aiden@million.dev>
* fix(rerender-state-only-in-handlers): recognize state consumed via member expression hook calls
State consumed by hook calls through member expressions (e.g. styles.useVariants({ state }))
was incorrectly flagged as unused. This commonly occurs with react-native-unistyles,
where state selects style variants via StyleSheet.useVariants().
The rule now checks both direct identifier calls (useX) and member expression calls
(obj.useX) when collecting custom hook arguments, following the existing pattern used
throughout the codebase.
Fixes#1716
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
* chore: add changeset for member expression hook fix
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
* test: add edge case for non-hook member expressions
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
* fix(rules): preserve state setter bindings
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
Co-authored-by: Aiden Bai <aiden@million.dev>
* fix: ignore TypeScript type-only positions in no-unguarded-browser-global-at-module-scope
The rule was incorrectly flagging browser-global names (window, navigator, etc.) when they appeared as property keys in TypeScript interfaces and type aliases. These are type-only positions that TypeScript erases during compilation, so no runtime reference error occurs.
Add isTypeScriptTypePosition check to skip identifiers in TS type contexts before reporting. Add regression tests covering interface properties, type alias properties, and the preserved behavior for actual runtime references.
Fixes#1667
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
* chore: add changeset for #1667 fix
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
* test: add edge case tests for TypeScript type positions
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
* test: preserve type-only browser global regression
* docs(changeset): clarify type-only global fix
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
Co-authored-by: Aiden Bai <aiden.bai05@gmail.com>
Resolve destructured Zustand imperative APIs by binding and property identity while preserving diagnostics for legitimate bound store hooks with API-like names.
* fix(rules): correct form and fragment diagnostics
* fix(rules): handle fragment review edge cases
* fix: handle document-top fragment links
* fix(a11y): parse ids from html tags only
* fix(a11y): resolve static fragment aliases
* fix(a11y): retain known fragment targets
* fix(a11y): tokenize html id attributes
* fix: preserve known fragment IDs in mixed spreads
* fix: preserve known fragment ID defaults