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_01PVeuchch67CtjSbizYm8giNo 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
Uh oh!
There was an error while loading. Please reload this page.
…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_01PVeuchch67CtjSbizYm8giThere 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_01PVeuchch67CtjSbizYm8giUh oh!
There was an error while loading. Please reload this page.
Что и зачем
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.