Skip to content

fix(extractor): static-class subscriber exemption (OWN014 FP)#157

Closed
PhysShell wants to merge 3 commits into
mainfrom
claude/mos-ownership-summary-n3q3j4
Closed

fix(extractor): static-class subscriber exemption (OWN014 FP)#157
PhysShell wants to merge 3 commits into
mainfrom
claude/mos-ownership-summary-n3q3j4

Conversation

@PhysShell

@PhysShell PhysShell commented Jun 28, 2026

Copy link
Copy Markdown
Owner

Что и зачем

Подписка на процесс-вечный статический ивент (AppDomain.ProcessExit, Console.CancelKeyPress) изнутри static class — не OWN014 region-escape: у статического класса нет экземпляра, поэтому посылка OWN014 «подписка промоутит экземпляр компонента до времени жизни источника» ложна по гарантии языка (его состояние и так процесс-вечное). Чиним этот класс FP в экстракторе (clsIsStatic-эксемпшн), подтверждено живым прогоном оракула на CsvHelper.

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

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

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

  • python tests/run_tests.py (зелёный; corpus 28/28 reductions)
  • ruff check . и mypy (в CI: lint job — pass)
  • селфтесты затронутых скриптов (python scripts/oracle_compare.py --selftest 31/31)

C# проверен в CI (golden C#, leak extractor, corpus benchmark — все зелёные). Сверх того — живой прогон оракула на JoshClose/CsvHelper с отключённым baseline по ConsoleHost: Own.NET leak total 2 → 0, обе находки (ProcessExit + CancelKeyPress) отсутствуют и в own-only, и в baselined → погашены в источнике.

Связанные issue

Нет. Продолжение oracle-FP-триажа (docs/notes/oracle-known-fps.md), поверх #156.

Чеклист

  • изменение покрыто тестом/селфтестом — корпусная фикстура subscription-static-class-host
  • README/docs обновлены при необходимости — docs/notes/oracle-known-fps.md
  • коммиты в conventional-commit стиле (fix:/oracle: scoped)

Подробнее

Фикс (precision, ~6 строк, sound по гарантии языка)

IsSelfOwnedSource уже снимал static-source region-escape для процесс-вечного подписчика — WPF App singleton (clsIsApp). Добавлен ровно такой же по смыслу clsIsStatic: static class не имеет экземпляра, значит промоутить нечего. Гейтится идентично (source == "static", не-таймеры). Найдено на статическом ConsoleHost в CsvHelper, чьи shutdown-хуки ProcessExit/CancelKeyPress были ложными OWN014.

Фикстура

corpus/real-world/subscription-static-class-host: before.csinstance-хост подписывает this на Console.CancelKeyPress (настоящий OWN014), after.cs — то же в static class (молчит). case.own моделирует token-форму (OWN001), как соседний screentogif-systemevents-leak.

Что НЕ чинилось (осознанно)

Newtonsoft TraceJsonReader._textWriter остаётся в baseline: это JsonTextWriter над StringWriter — no-op только из-за обёртки; JsonTextWriter в общем виде может обернуть FileStream (реальная течь), поэтому blanket-exempt unsound. Нужен wrapper-cascade анализ ради одной находки — честнее оставить в baseline. Процесс-вечный instance-хост (нестатический singleton) тоже остаётся открытым (нужен lifetime-сигнал).

🤖 Generated with Claude Code


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for recognizing safe process-lifetime subscriptions made from static host types, reducing false alarms in affected analyses.
  • Bug Fixes

    • Removed outdated false-positive baselines for shutdown and cancel-hook scenarios that are now correctly handled.
    • Updated diagnostics so the expected warning set matches the latest behavior.
  • Documentation

    • Refreshed notes and triage summaries to describe the new exemption and updated findings status.

claude added 2 commits June 28, 2026 05:08
A subscription to a process-lived static event (AppDomain.ProcessExit,
Console.CancelKeyPress) from inside a `static class` is not an OWN014 region
escape: a static class has NO instance, so the 'promotes the component instance
to the source's lifetime' premise is vacuously false — its state is already
process-lived by the language definition. Sound by guarantee, not heuristic.

Extends the existing process-lived-subscriber exemption (the WPF `App` singleton,
clsIsApp) with clsIsStatic, gated identically: only the source=="static" region
escape, non-timers. Mined on CsvHelper's static ConsoleHost, whose
ProcessExit/CancelKeyPress shutdown hooks were false OWN014s.

New corpus fixture subscription-static-class-host: before.cs is an INSTANCE host
subscribing this to Console.CancelKeyPress (real OWN014), after.cs is the same as
a static class (silent). case.own models the token form (OWN001) like the sibling
screentogif-systemevents-leak case.

Newtonsoft TraceJsonReader._textWriter stays baselined: it is a JsonTextWriter
over a StringWriter, no-op only because of what it wraps — recognising that needs
wrapper-cascade analysis (unsound to blanket-exempt JsonTextWriter), so the
honest call is to leave it. Re-pointing the oracle at CsvHelper to confirm the
ConsoleHost FPs clear.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019846YSZ35c7CdkWQ1qX5gm
…restore scaffolding

Live CsvHelper oracle re-run confirms the clsIsStatic exemption clears both
ConsoleHost hooks (AppDomain.ProcessExit / Console.CancelKeyPress) at the source:
Own.NET leak total 2 -> 0, both findings absent from own-only and baselined.
Delete the two CsvHelper baseline entries (no CsvHelper FP entries remain).
Restore dev scaffolding: oracle-target.txt back to the systemevents-console
fixture, remove the branch from the oracle.yml push filter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019846YSZ35c7CdkWQ1qX5gm
@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The extractor gains a clsIsStatic check that extends the OWN014 process-lived/static-source subscription exemption to static class types alongside the existing WPF App singleton exemption. A new corpus test case (subscription-static-class-host) is added with before/after fixtures, an OWN model, expected diagnostics, and notes. Two CsvHelper ConsoleHost oracle baseline entries are retired, and the oracle known-FPs doc is updated to reflect the fix.

Changes

Static-class subscriber exemption (OWN014)

Layer / File(s) Summary
Extractor: clsIsStatic exemption
frontend/roslyn/OwnSharp.Extractor/Program.cs
Detects static modifier on the declaring class and extends the process-lived/static-source skip condition to include clsIsStatic, alongside clsIsApp and !isTimer.
Corpus test case
corpus/real-world/subscription-static-class-host/before.cs, after.cs, case.own, expected-diagnostics.txt, notes.md
Before fixture shows an instance-capturing Console.CancelKeyPress subscription (real OWN001); after fixture shows the static-class equivalent (silent). OWN model, expected diagnostic, and notes formalize the exemption rule.
Oracle baseline and docs
corpus/oracle-fp-baseline.txt, docs/notes/oracle-known-fps.md
Removes the two retired CsvHelper ConsoleHost OWN014 baseline entries; updates disposition counts and the lifetime-unaware subscription root-cause section to reflect the clsIsStatic fix.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • PhysShell/Own.NET#81: Adds the WPF App singleton (clsIsApp) skip in the same OWN014 exemption condition that this PR extends with clsIsStatic.
  • PhysShell/Own.NET#88: Extends OWN014 suppression for process-lived shutdown hooks; this PR broadens the same exemption to static class subscribers, clearing the CsvHelper ConsoleHost findings.
  • PhysShell/Own.NET#156: Introduces the oracle baseline mechanism and related extractor FP-hardening flow that this PR modifies and retires entries from.

Poem

🐇 A static class holds no this to promote,
No instance escapes—the checker took note.
clsIsStatic now guards the gate,
CsvHelper's baseline entries: cleared of late.
The corpus grows richer, the oracle pleased,
One more false positive gracefully ceased! 🎉

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately reflects the main extractor fix for the OWN014 static-class subscriber exemption.
Description check ✅ Passed The description follows the template and includes the required sections, change type, verification, linked issue, and checklist.
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/mos-ownership-summary-n3q3j4

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: 6dd6ad5e6d

ℹ️ 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".

// Scoped to NON-timers: a timer is forced to source "static" above, but a
// never-stopped timer in `App` is still a real leak (CodeRabbit).
if (!isTimer && source == "static" && clsIsApp)
if (!isTimer && source == "static" && (clsIsApp || clsIsStatic))

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 Don't drop captured locals in static classes

This skips every static-source subscription in a static class before checking whether the handler captures anything. In a static helper such as Attach(ViewModel vm) { SystemEvents.UserPreferenceChanged += (_, _) => vm.Refresh(); }, the process-lived event retains the closure and vm even though there is no this; before this change that subscription was emitted as a capture/OWN014, but now it is silently continued. The exemption should be limited to handlers that retain no instance/locals, using the same kind of guard as HandlerRetainsNoInstance, rather than suppressing all static-class lambdas.

Useful? React with 👍 / 👎.

@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: 2

🤖 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 `@corpus/real-world/subscription-static-class-host/case.own`:
- Around line 1-9: The fixture only validates the existing OWN001 leak model and
does not exercise the new static-class extractor behavior. Update the corpus
test setup around this case so it asserts extractor output for the `static
class` variant from `before.cs`/`after.cs`, and verify that the static-class
subscriber path is silent (no diagnostic) instead of only presence-checking the
files. Use the existing `tests/test_corpus.py` flow and the
`case.own`/`expected-diagnostics.txt` comparison to add a golden/assertion that
covers the `clsIsStatic` exemption end-to-end.

In `@frontend/roslyn/OwnSharp.Extractor/Program.cs`:
- Around line 3282-3287: The static-class suppression in the OWN014 extraction
logic is too broad because it only checks `clsIsStatic`/`source == "static"` and
can still hide handlers that capture locals through closures. Update the branch
around the static-class event subscription handling to only suppress when the
handler is proven capture-free, using the existing capture analysis such as
`HandlerRetainsNoInstance(...)` or equivalent, and keep static-class
subscriptions that retain closure state reported normally.
🪄 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: fd028009-ba76-4a72-929b-fa96028a9cc8

📥 Commits

Reviewing files that changed from the base of the PR and between ede20ce and 6dd6ad5.

📒 Files selected for processing (8)
  • corpus/oracle-fp-baseline.txt
  • corpus/real-world/subscription-static-class-host/after.cs
  • corpus/real-world/subscription-static-class-host/before.cs
  • corpus/real-world/subscription-static-class-host/case.own
  • corpus/real-world/subscription-static-class-host/expected-diagnostics.txt
  • corpus/real-world/subscription-static-class-host/notes.md
  • docs/notes/oracle-known-fps.md
  • frontend/roslyn/OwnSharp.Extractor/Program.cs

Comment on lines +1 to +9
// OwnLang model of the static-class shutdown-hook subscription. before.cs subscribes a
// `this`-capturing handler to a process-lived static event (Console.CancelKeyPress) from
// an INSTANCE host and never unsubscribes — a subscription leak. The real C# lands the
// REGION form (OWN014 escape, static source outlives the instance); here it is modelled
// in the equivalent TOKEN form as the generic OWN001 (a Subscription acquire with no
// release), the shape test_corpus.py checks — exactly as the sibling
// corpus/real-world/screentogif-systemevents-leak case does. The fix makes the host a
// `static class` (no instance to promote), which the extractor's static-class subscriber
// exemption drops; see notes.md for the recognition rule.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

This fixture doesn't assert the new extractor behavior.

tests/test_corpus.py:65-99 only checks case.own against expected-diagnostics.txt; before.cs and after.cs are just presence-checked. Since this model still encodes only the leaking instance-host path (OWN001), clsIsStatic can regress without this case failing. Please add an extractor-driven golden/assertion for the static-class variant being silent so the PR objective is covered end-to-end.

🤖 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 `@corpus/real-world/subscription-static-class-host/case.own` around lines 1 -
9, The fixture only validates the existing OWN001 leak model and does not
exercise the new static-class extractor behavior. Update the corpus test setup
around this case so it asserts extractor output for the `static class` variant
from `before.cs`/`after.cs`, and verify that the static-class subscriber path is
silent (no diagnostic) instead of only presence-checking the files. Use the
existing `tests/test_corpus.py` flow and the
`case.own`/`expected-diagnostics.txt` comparison to add a golden/assertion that
covers the `clsIsStatic` exemption end-to-end.

Comment thread frontend/roslyn/OwnSharp.Extractor/Program.cs Outdated
Both Codex and CodeRabbit independently flagged the clsIsStatic exemption as
unsound: it skipped EVERY static-source subscription in a static class without a
capture check, silently dropping real captured-local leaks — e.g.
  static class Foo { void Attach(VM vm) {
      SystemEvents.UserPreferenceChanged += (_, _) => vm.Refresh(); } }
where the process-lived event pins the captured vm forever (was OWN014).

A sound narrowing (suppress only when the handler captures nothing) would not
clear the CsvHelper ConsoleHost target either — its shutdown hooks capture
cts/resetEvent locals — so the static-class signal cannot soundly distinguish
that FP from a real captured-local leak. CsvHelper ConsoleHost therefore stays
baselined, same bucket as Newtonsoft serializer.Error.

Reverts the extractor change, the subscription-static-class-host fixture, the doc
update, and restores the two CsvHelper ConsoleHost baseline entries — branch
returns to main. PR #157 is closed (no net change). Caught pre-merge by review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019846YSZ35c7CdkWQ1qX5gm

Copy link
Copy Markdown
Owner Author

Closing — the clsIsStatic exemption is unsound, as Codex and CodeRabbit both correctly flagged. It skipped every static-source subscription in a static class without a capture check, so it would silently drop a real captured-local leak:

static class Foo {
    void Attach(VM vm) {
        SystemEvents.UserPreferenceChanged += (_, _) => vm.Refresh();  // process-lived event pins vm forever
    }
}

A sound narrowing (suppress only a capture-free handler) would not clear the CsvHelper ConsoleHost target either — its shutdown hooks capture cts/resetEvent locals — so the static class signal can't statically distinguish that FP from a genuine captured-local leak. CsvHelper ConsoleHost therefore stays in the oracle FP baseline, the same bucket as Newtonsoft serializer.Error (both honest "can't auto-distinguish without lifetime modelling").

Reverted on the branch (488d505) — it's back to main, no net change here. Caught pre-merge; thanks to both reviewers.


Generated by Claude Code

@PhysShell PhysShell closed this Jun 28, 2026
PhysShell pushed a commit that referenced this pull request Jun 28, 2026
Document the unsound `|| clsIsStatic` exemption attempted in #157 (reverted
in 488d505) so it is not retried. A static class only rules out an instance
`this`; a lambda handler can still capture a local that a process-lived event
pins for the whole process, so the exemption would silently swallow a real
captured-local leak. A capture-gated narrowing would be sound but still would
not clear CsvHelper ConsoleHost (it captures cts/resetEvent), which therefore
stays baselined.

- Program.cs: ANTI-PATTERN warning comment at the clsIsApp exemption site.
- oracle-known-fps.md: new "Rejected approaches" section with the why.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019846YSZ35c7CdkWQ1qX5gm
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