Skip to content

feat(ownts): парсинг function-колбэков/cleanup'ов + поймали реальную утечку в OSS#150

Merged
PhysShell merged 4 commits into
mainfrom
claude/ownts-function-callbacks
Jun 27, 2026
Merged

feat(ownts): парсинг function-колбэков/cleanup'ов + поймали реальную утечку в OSS#150
PhysShell merged 4 commits into
mainfrom
claude/ownts-function-callbacks

Conversation

@PhysShell

@PhysShell PhysShell commented Jun 27, 2026

Copy link
Copy Markdown
Owner

Что и зачем

Hook-библиотеки чистят за собой by design — поэтому охота за реальной утечкой ушла в прикладной компонентный код, и нашла её: react-scroll-to-bottom@4.2.0 (ScrollToBottom/Composer.js:574)

target.addEventListener('focus', handleFocus, { capture: true, passive: true });
return () => target.removeEventListener('focus', handleFocus);   // capture по умолчанию (false)

removeEventListener обязан совпадать по capture-флагу: добавлен capture: true, снимается с falselistener никогда не удаляется, копится на каждую смену 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 снять нельзя.

Тип изменения

  • feat — новая возможность
  • fix — исправление бага
  • docs — документация
  • refactor / chore / test / ci — без изменения поведения

Как проверено

  • 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-шаг)
  • README/docs обновлены (docs/notes/ownts-oss-benchmark.md — первый подтверждённый TP + ES5-покрытие + новый residual FP от optional-chaining)
  • коммиты в conventional-commit стиле (feat:)

🤖 Generated with Claude Code


Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • Expanded effect analysis to better understand ES5-style function callbacks and additional cleanup return forms.
    • Added a new example demonstrating correct cleanup vs intentional capture/removal mismatch scenarios.
  • Bug Fixes
    • More reliably detects event-listener teardown problems across more effect syntaxes, including extra async-related edge cases.
  • Documentation
    • Updated benchmark notes with newly confirmed real-world hanging effect cases and added ES5 coverage details.
  • Tests
    • Updated regression expectations and added end-to-end checks for the new example parsing behavior.

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
@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0fcc0f45-e5d3-4a69-af0b-fbcfd20ffdbe

📥 Commits

Reviewing files that changed from the base of the PR and between f180ea5 and 3e24fd0.

📒 Files selected for processing (3)
  • .github/workflows/ci.yml
  • frontend/ownts/examples/EffectLeakControl.tsx
  • frontend/ownts/test_ownts.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/ownts/examples/EffectLeakControl.tsx
  • frontend/ownts/test_ownts.py

📝 Walkthrough

Walkthrough

OWNTs now parses ES5 function effect callbacks and return function cleanups, adds an example and regression test for that shape, extends CI coverage, and records new benchmark notes and confirmed listener-leak cases.

Changes

ES5 effect callback parsing and validation

Layer / File(s) Summary
Parser span handling
frontend/ownts/ownts.py
_effect_callback distinguishes function-expression callbacks from arrows, _cleanup_span matches ES5 function cleanup returns, and extract skips cleanup extraction for async effects.
Example, tests, and CI
frontend/ownts/examples/EffectFunctionCallback.tsx, frontend/ownts/examples/EffectLeakControl.tsx, frontend/ownts/test_ownts.py, .github/workflows/ci.yml
EffectFunctionCallback.tsx adds an ES5-style effect example, EffectLeakControl.tsx adds async leak scenarios and updates the expected count, test_ownts.py asserts both cases, and CI runs the matching regression checks.
Benchmark notes
docs/notes/ownts-oss-benchmark.md
The benchmark note adds confirmed listener-leak examples and documents ES5 build coverage plus the optional-chaining receiver mismatch case.

Possibly related PRs

  • PhysShell/Own.NET#144: Also changes the OwnTS frontend parsing pipeline in frontend/ownts/ownts.py, which is the same core path extended here for ES5 callbacks.
  • PhysShell/Own.NET#145: Also updates frontend/ownts/ownts.py around useEffect parsing and cleanup extraction, overlapping with the callback and cleanup handling changed here.
  • PhysShell/Own.NET#149: Also modifies cleanup-span extraction in frontend/ownts/ownts.py, directly adjacent to the ES5 return function handling added in this PR.

Poem

I hopped through hooks with bunny grace,
To sniff out leaks in ES5 space.
One focus flap, one resize hush,
One OWN001 — no need to rush 🐰

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title is concise and accurately reflects the main change: ES5 function callback/cleanup parsing plus an OSS leak regression.
Description check ✅ Passed The description follows the template well and fills all required sections with concrete details, checks, issues, and checklist items.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/ownts-function-callbacks

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread frontend/ownts/ownts.py
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:]):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 4516e29 and 70cbb5f.

📒 Files selected for processing (5)
  • .github/workflows/ci.yml
  • docs/notes/ownts-oss-benchmark.md
  • frontend/ownts/examples/EffectFunctionCallback.tsx
  • frontend/ownts/ownts.py
  • frontend/ownts/test_ownts.py

Comment thread docs/notes/ownts-oss-benchmark.md
…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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
.github/workflows/ci.yml (1)

1146-1157: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the async function variant to this regression.

This step locks the sync function () {} shape, but the new async-cleanup suppression is only exercised via async () => in EffectLeakControl.tsx. Adding one useEffect(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

📥 Commits

Reviewing files that changed from the base of the PR and between 70cbb5f and f180ea5.

📒 Files selected for processing (5)
  • .github/workflows/ci.yml
  • docs/notes/ownts-oss-benchmark.md
  • frontend/ownts/examples/EffectLeakControl.tsx
  • frontend/ownts/ownts.py
  • frontend/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
@PhysShell
PhysShell merged commit 0f124c4 into main Jun 27, 2026
30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants