feat(ownts): парсинг function-колбэков/cleanup'ов + поймали реальную утечку в OSS#150
Conversation
Hook libraries clean up by design, so the real-leak hunt moved to app-shaped
component code — and found one: react-scroll-to-bottom@4.2.0 Composer.js:574 adds
a `focus` listener with { capture: true } but removes it with the default capture
(false), so per the DOM spec it is NEVER removed and piles up on every `target`
change. That code ships transpiled ES5 (`function () {}` callbacks + `return
function () {}` cleanups), which the arrow-tuned parser could not read.
This teaches the frontend those shapes:
- _effect_callback: detect a `function (...) {}` callback (body brace after the
params via _body_brace), alongside arrow block / arrow expression.
- _cleanup_span: a cleanup may be `return function () {}` (not only `return () =>`).
With both, the Composer bug is caught for the RIGHT reason — the cleanup parses,
and the listener key rejects the capture:true-vs-false removal — instead of by the
coarse "no cleanup" path. EffectFunctionCallback.tsx pins it: a correctly-matched
ES5 cleanup stays silent; the capture mismatch is the single OWN001. CI step added.
The ES5 corpus (ahooks + react-use, ~93 useEffect) now parses; its 11 findings
triage as FPs from the known patterns plus one new transpilation artifact
(optional-chaining desugars the cleanup receiver to a temp `_a` ≠ `ref.current`),
documented as residual in docs/notes/ownts-oss-benchmark.md (which also records the
first confirmed true positive). Arrow corpus unchanged at 11; full suite green;
ruff + mypy --strict clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PVeuchch67CtjSbizYm8gi
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughOWNTs now parses ES5 ChangesES5 effect callback parsing and validation
Possibly related PRs
Poem
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 70cbb5fb05
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| j0 = after_open | ||
| while j0 < len(masked) and masked[j0] in " \t\r\n": | ||
| j0 += 1 | ||
| if re.match(r"(?:async\s+)?function\b", masked[j0:]): |
There was a problem hiding this comment.
Do not credit cleanups from async effect callbacks
When the first argument is an async function expression, React receives a Promise from the effect and will not run the inner returned function as the cleanup. With this branch, useEffect(async function () { window.addEventListener(...); return function () { window.removeEventListener(...); }; }, []) is parsed like a normal synchronous effect, so _cleanup_span marks the listener released and the leak is missed in JS/transpiled inputs that are not type-checked.
Useful? React with 👍 / 👎.
An `async` effect callback returns a Promise, so React never runs its returned function as cleanup. With `function`-callback support now accepting `useEffect(async () => …)` / `useEffect(async function () …)`, a `return () => …` inside an async effect was wrongly treated as a real cleanup, masking the leak in JS/transpiled inputs that aren't type-checked. extract() now detects an async outer callback and credits no cleanup for it (an inner sync IIFE keeps its own cleanup — only the OUTER callback's async-ness matters). EffectLeakControl.tsx gains a fourth control (async effect whose dead cleanup must still leak); test + CI updated to 4. Full suite green; ruff + mypy --strict clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PVeuchch67CtjSbizYm8gi
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/notes/ownts-oss-benchmark.md`:
- Around line 132-149: The `react-scroll-to-bottom` example should be rewritten
to match the ES5 transpiled output described in the surrounding text. Update the
snippet in the benchmark doc so the cleanup uses a `function () {}` form rather
than an arrow function, keeping the listener add/remove pattern aligned with the
`target.addEventListener` and `removeEventListener` example and the parser
coverage rationale.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: b63e19c0-55c5-441e-827b-014a1790d9e3
📒 Files selected for processing (5)
.github/workflows/ci.ymldocs/notes/ownts-oss-benchmark.mdfrontend/ownts/examples/EffectFunctionCallback.tsxfrontend/ownts/ownts.pyfrontend/ownts/test_ownts.py
…m (CodeRabbit)
The benchmark note printed the Composer cleanup as an arrow, which made the leak
look detectable by the old arrow-only path and undercut the rationale for parsing
`function () {}` cleanups. Show it as the published ES5 `return function () {}` and
note it's only reachable after the parser change — the bug is then the
capture-mismatch class, not a missing cleanup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PVeuchch67CtjSbizYm8gi
There was a problem hiding this comment.
🧹 Nitpick comments (1)
.github/workflows/ci.yml (1)
1146-1157: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd the
async functionvariant to this regression.This step locks the sync
function () {}shape, but the new async-cleanup suppression is only exercised viaasync () =>inEffectLeakControl.tsx. Adding oneuseEffect(async function () { ... return function () { ... } }, [])case here would keep the ES5 branch covered too.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/ci.yml around lines 1146 - 1157, The CI regression step only covers the sync ES5 callback shape, so add an async-function variant to the same workflow test to exercise the async-cleanup suppression path too. Update the existing check around the ES5 `function` callback case to also run an `useEffect(async function () { ... return function () { ... } }, [])` scenario from the relevant OWN/ownlang fixtures, and assert it still preserves the exact OWN001 capture-mismatch leak behavior while keeping the cleanup suppression covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 1146-1157: The CI regression step only covers the sync ES5
callback shape, so add an async-function variant to the same workflow test to
exercise the async-cleanup suppression path too. Update the existing check
around the ES5 `function` callback case to also run an `useEffect(async function
() { ... return function () { ... } }, [])` scenario from the relevant
OWN/ownlang fixtures, and assert it still preserves the exact OWN001
capture-mismatch leak behavior while keeping the cleanup suppression covered.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 573f0642-51c3-4c16-80c7-34359b401c91
📒 Files selected for processing (5)
.github/workflows/ci.ymldocs/notes/ownts-oss-benchmark.mdfrontend/ownts/examples/EffectLeakControl.tsxfrontend/ownts/ownts.pyfrontend/ownts/test_ownts.py
✅ Files skipped from review due to trivial changes (1)
- docs/notes/ownts-oss-benchmark.md
🚧 Files skipped from review as they are similar to previous changes (1)
- frontend/ownts/test_ownts.py
…Rabbit)
The async-cleanup suppression path was only exercised via `async () =>` in
EffectLeakControl.tsx. Add a fifth control — `useEffect(async function () { …
return function () {} }, [])` — so the ES5 transpiled async shape is regressed
too: React still ignores the returned cleanup, so the listener must stay OWN001.
Bumps the expected count to 5 in the spike test and the CI step.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PVeuchch67CtjSbizYm8gi
Что и зачем
Hook-библиотеки чистят за собой by design — поэтому охота за реальной утечкой ушла в прикладной компонентный код, и нашла её:
react-scroll-to-bottom@4.2.0(ScrollToBottom/Composer.js:574)removeEventListenerобязан совпадать по capture-флагу: добавленcapture: true, снимается сfalse→ listener никогда не удаляется, копится на каждую сменуtarget. Этот код собран в ES5 (function () {}колбэки +return function () {}cleanup'ы), которые arrow-заточенный парсер не читал.Учу фронтенд этим формам:
_effect_callback— распознаётfunction (...) {}-колбэк (тело-{после списка параметров через_body_brace) наряду со стрелочными блоком/выражением._cleanup_span— cleanup может бытьreturn function () {}, не толькоreturn () =>.Теперь баг Composer ловится по правильной причине — cleanup парсится, и listener-ключ отвергает снятие
capture:trueчерезcapture:false, а не по грубому пути «нет cleanup».EffectFunctionCallback.tsxпинит: корректный ES5-cleanup молчит, capture-mismatch — единственныйOWN001.Бонус-кейс:
@reactuses/core@6.4.0передаётonPressed('mouse')(свежевозвращаемую функцию) и в add, и в remove → drag/touch listeners снять нельзя.Тип изменения
Как проверено
python tests/run_tests.py— зелёный (effects 31/31,ownir 194/194)ruff check .иmypy(--strict поownlang) — чистоpython frontend/ownts/test_ownts.py(новая фикстураEffectFunctionCallback.tsx→ 1 OWN001; arrow-корпус без изменений = 11)react-scroll-to-bottom@4.2.0баг ловится на Composer.js:574Связанные issue
Refs P-020 (item 7 — OSS benchmark). Follow-up к #149. Closes — нет.
Чеклист
EffectFunctionCallback.tsx+ CI-шаг)docs/notes/ownts-oss-benchmark.md— первый подтверждённый TP + ES5-покрытие + новый residual FP от optional-chaining)feat:)🤖 Generated with Claude Code
Generated by Claude Code
Summary by CodeRabbit
functioncallbacks and additional cleanup return forms.