Skip to content

fix(P-004): capture-aware static-lambda tier — non-capturing lambda is not a region escape#211

Merged
PhysShell merged 2 commits into
mainfrom
claude/own-net-issue-199-lambda
Jul 10, 2026
Merged

fix(P-004): capture-aware static-lambda tier — non-capturing lambda is not a region escape#211
PhysShell merged 2 commits into
mainfrom
claude/own-net-issue-199-lambda

Conversation

@PhysShell

@PhysShell PhysShell commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Что и зачем

Issue #199 (subscription precision, lambda-handler tier). Проверив поведение против реального экстрактора локально (поднял .NET SDK в песочнице), обнаружил, что четыре тира по времени жизни источника уже работают для лямбд (static→OWN014, injected→OWN001 warning, bounded/local→silent), но общий static-путь флагал OWN014 на non-capturing лямбде (SomeBus.Pinged += (_,_) => StaticHandle()), хотя она не удерживает ни одного инстанса — это false positive против нашей же записанной политики (subscription-leaks-and-profiles.md: «static event + capturing handler → error») и несогласовано с исключением static-method (non-capturing method group уже молчит).

Почему меняется запиненное поведение — с ссылкой на политику: OWN014-посылка «сильная подписка пиннит подписчика к времени жизни источника» не выполняется для хендлера, который ничего не удерживает (static method с null-target, либо non-capturing лямбда — closure-аналог static-method исключения). Захватывающий (this или локал — CsvHelper cts/resetEvent) остаётся OWN014.

Состав:

  • Program.cs — гейт source=="static" && HandlerRetainsNoInstance. Строго консервативен: любой недоказуемо-non-retaining случай (захваченный локал, непрозрачный delegate-typed value) → считается retaining → OWN014. Никогда не расширяется синтаксически (не clsIsStatic, не «все лямбды»).
  • AppDomainShutdownSample.csNonAppDomainSubscriber расщеплён (не удалён): теперь захватывающая лямбда (cts) → OWN014 (сохраняет намерение, которое защищал Codex — CsvHelper-шейп); новый NonCapturingStaticSubscriber → silent (FP, который убираем).
  • ownir.py — ветки OWN014 (capture + DI-escape) теперь несут «inline lambda — no -= handle», как OWN001 (extractor уже ставит lambda:true). Method-group captures без изменений.
  • LambdaTiersSample.cs — first-class молчаливые тиры: bounded/local + self-owned лямбда (шейп -=-impossible-but-bounded).
  • test_ownir.py — пин: лямбда static capture → OWN014 с нотой (250/250).
  • ci.yml — соответствующие ассерты wpf-extractor (расщепил + добавил).
  • subscription-leaks-and-profiles.md — заледжерил решение; field-notes-patterns.md — записал осознанный non-goal: рост invocation-list от повторных non-capturing подписок (call-count шейп, который region-модель не выражает; не покрывался и для static-method исключения → не регрессия).

Безопасность (условие «ничего запиненное не исчезает молча»): CsvHelper oracle-baseline записи — это захватывающий cts/resetEvent шейп (остаётся OWN014); каждый OWN014 corpus-кейс (systemevents-region-escape, screentogif-systemevents-leak) — method-group instance handler (retaining) и остаётся OWN014 (перепроверил с материализованным WindowsDesktop ref pack); единственный переклассифицированный кейс (NonAppDomainSubscriber) явно виден в диффе.

Про CI: PR трогает .github/workflows/ci.yml; #204 (тоже правил ci.yml) уже влит в main, ветка от свежего main — конфликта нет.

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

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

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

  • python tests/run_tests.py — зелено (ownir 250/250, wpf 9/9, corpus 32/32)
  • ruff check . и mypy — чисто
  • Реальный экстрактор локально по всем 29 CI-сэмплам: non-capturing static лямбда → silent; capturing (this/локал) → OWN014 с нотой; injected → OWN001 warning; bounded/local/self-owned → silent
  • Регрессия OWN014-корпуса перепроверена с WindowsDesktop ref pack (оба SystemEvents-кейса остаются OWN014)

Связанные issue

Closes #199

Чеклист

  • изменение покрыто тестом/фикстурой (test_ownir пин + сэмплы + CI-ассерты)
  • README/docs обновлены (заледжерил в subscription-leaks-and-profiles.md + non-goal в field-notes-patterns.md)
  • коммиты в conventional-commit стиле (fix:)

🤖 Generated with Claude Code

https://claude.ai/code/session_01Cg6DVXahkyY68Ruu7f76rG


Generated by Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Static-event subscriptions using non-capturing lambdas are no longer incorrectly flagged for region-escape issues.
    • Capturing lambdas continue to receive appropriate lifetime warnings.
    • Inline-lambda warnings now explain when no -= handle is available for detachment.
  • Documentation

    • Clarified severity guidance for subscription-related findings.
    • Documented the distinction between subscriber lifetime leaks and repeated subscription growth.
  • Tests

    • Expanded coverage for lambda capture behavior and static-event scenarios.

…s not OWN014

Issue #199 (subscription precision, lambda-handler tier). The four source-lifetime
tiers already routed lambdas correctly (static->OWN014, injected->OWN001 warning,
bounded/local->silent), but the general static path fired OWN014 on a NON-capturing
lambda (`SomeBus.Pinged += (_,_) => StaticHandle()`) even though it retains no
instance — a false positive against our own recorded policy
(subscription-leaks-and-profiles.md: "static event + *capturing* handler -> error")
and inconsistent with the static-METHOD exemption (a non-capturing method group is
already silent).

Fix — extractor (verified end to end with a local .NET SDK, not CI-only): gate the
general static path through the existing conservative `HandlerRetainsNoInstance`, so
a non-capturing lambda (the closure analog of a static-method handler) goes silent.
It stays STRICTLY conservative: a lambda capturing `this` OR any enclosing local (the
CsvHelper cts/resetEvent shape), or any delegate-typed value it cannot prove, is
treated as retaining and STILL raises OWN014. Never widened syntactically (no
`clsIsStatic`, no "all lambdas").

  * Program.cs — the `source=="static" && HandlerRetainsNoInstance` drop.
  * AppDomainShutdownSample.cs — `NonAppDomainSubscriber` SPLIT (not deleted):
    now a CAPTURING lambda (captures `cts`) -> OWN014 (preserves the Codex-defended
    intent); new `NonCapturingStaticSubscriber` -> silent (the FP this removes).
  * ownir.py — the OWN014 (capture + DI-escape) branches now carry the "inline
    lambda — no '-=' handle" note, like OWN001 (the extractor already stamps
    lambda:true). Method-group captures are unchanged (no note).
  * LambdaTiersSample.cs — first-class silent tiers: bounded/local + self-owned
    lambda (the `-=`-impossible-but-bounded shape).
  * test_ownir.py — pins a lambda static capture -> OWN014 with the note (250/250).
  * ci.yml — split/added the wpf-extractor assertions accordingly.
  * subscription-leaks-and-profiles.md — ledgers "static + non-retaining -> silent;
    static + retaining -> OWN014"; field-notes-patterns.md records the deliberate
    non-goal (invocation-list growth from repeated non-capturing subscriptions —
    a call-count shape the region model does not, and should not, express; never
    covered for the static-method exemption either, so not a regression).

Safety: no baselined/pinned finding vanishes silently. The CsvHelper oracle-baseline
entries are the CAPTURING cts/resetEvent shape (still OWN014); every OWN014 corpus
case (systemevents-region-escape, screentogif-systemevents-leak) is a method-group
instance handler (retaining) and stays OWN014 (re-confirmed with the WindowsDesktop
ref pack materialized); the one reclassified case (NonAppDomainSubscriber) is
explicit in the diff.

Closes #199

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cg6DVXahkyY68Ruu7f76rG
public NonAppDomainSubscriber(System.Threading.CancellationTokenSource cts)
{
SomeBus.Pinged += (_, _) => Handle(); // static event, lambda, not AppDomain -> OWN014
SomeBus.Pinged += (_, _) => cts.Cancel(); // captures `cts` -> OWN014
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@PhysShell, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 49 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7679e99f-151f-455b-8455-6af83782a0cf

📥 Commits

Reviewing files that changed from the base of the PR and between 0e51b4e and e48fc02.

📒 Files selected for processing (2)
  • .github/workflows/ci.yml
  • frontend/roslyn/samples/LambdaTiersSample.cs
📝 Walkthrough

Walkthrough

This change refines event-subscription analysis for lambda handlers. Static events now remain silent for proven non-retaining lambdas, while retaining handlers continue producing OWN014. Diagnostics identify inline lambdas without detachable -= handles, with fixtures, bridge tests, CI checks, and documentation updated.

Changes

Lambda subscription precision

Layer / File(s) Summary
Subscription fixtures and extraction
frontend/roslyn/samples/AppDomainShutdownSample.cs, frontend/roslyn/samples/LambdaTiersSample.cs, frontend/roslyn/OwnSharp.Extractor/Program.cs
Capturing, non-capturing, bounded, and self-owned lambda subscriptions are represented in samples; proven non-retaining static handlers no longer emit subscription facts.
OWN014 bridge and regressions
ownlang/ownir.py, tests/test_ownir.py
OWN014 messages include the missing -= handle explanation for inline lambdas, with regression coverage for static captures.
Validation and precision notes
.github/workflows/ci.yml, docs/notes/subscription-leaks-and-profiles.md, docs/notes/field-notes-patterns.md
CI validates capturing and silent lambda tiers; documentation records static-handler severity rules and separates repeated invocation-list growth from OWN014.0

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RoslynSamples
  participant OwnSharpExtractor
  participant OwnIR
  participant CI
  RoslynSamples->>OwnSharpExtractor: Analyze lambda event subscriptions
  OwnSharpExtractor->>OwnIR: Emit retaining subscription facts
  OwnSharpExtractor-->>OwnIR: Suppress non-retaining static subscriptions
  OwnIR->>CI: Produce OWN014 diagnostics and messages
  CI->>CI: Validate capture-aware and silent cases
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly describes the main change: suppressing OWN014 for non-capturing static lambdas while keeping capture-aware tiering.
Description check ✅ Passed The description follows the required template and includes the purpose, change type, verification, linked issue, and checklist sections.
Linked Issues check ✅ Passed The PR matches #199 by tiering inline lambdas by source lifetime and keeping capturing or unprovable cases conservative.
Out of Scope Changes check ✅ Passed The changes stay within the issue scope, adding supporting samples, tests, CI assertions, and documentation for the new lambda tiering behavior.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/own-net-issue-199-lambda

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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 @.github/workflows/ci.yml:
- Around line 466-482: Update the CI checks around the silent fixtures to first
verify that NonCapturingStaticSubscriber, LocalBoundedLambda, and
SelfOwnedFieldLambda are present in $RUNNER_TEMP/facts.json, failing if any were
not extracted; then retain the existing assertions that their diagnostics are
absent from $out.
🪄 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: 713fecbd-97ab-48ba-abe0-032a6dec7dd8

📥 Commits

Reviewing files that changed from the base of the PR and between 9fc8c27 and 0e51b4e.

📒 Files selected for processing (8)
  • .github/workflows/ci.yml
  • docs/notes/field-notes-patterns.md
  • docs/notes/subscription-leaks-and-profiles.md
  • frontend/roslyn/OwnSharp.Extractor/Program.cs
  • frontend/roslyn/samples/AppDomainShutdownSample.cs
  • frontend/roslyn/samples/LambdaTiersSample.cs
  • ownlang/ownir.py
  • tests/test_ownir.py

Comment thread .github/workflows/ci.yml
…ambda

The silent-tier fixtures (LocalBoundedLambda, SelfOwnedFieldLambda) emit no
OwnIR component by design, so their negative CI greps passed vacuously if
LambdaTiersSample.cs ever stopped being extracted (CodeRabbit, PR #211). The
file had no positive artifact to prove it reached the extractor.

CodeRabbit's suggested jq presence-guard on facts.json components cannot work
here: a silent subscription produces zero components, so the guard would fail
CI. Instead, follow the codebase idiom (SemaphoreFieldSample, VoidSubscribeSample)
and add a positive control in the same file:

- InjectedSourceLambda: a lambda on a ctor-param (injected) publisher whose
  lifetime is unknown -> OWN001 warning, the injected tier that was missing
  from this file. It is the only component the extractor emits here, so its
  OWN001 assert proves the file was extracted and the two silent negatives are
  no longer vacuous. It also exercises the inline-lambda no-'-=' note on OWN001
  (previously pinned only on OWN014).

NonCapturingStaticSubscriber was already anchored: it shares
AppDomainShutdownSample.cs with the positively-asserted NonAppDomainSubscriber
/ CapturingShutdownSubscriber OWN014s.

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

public InjectedSourceLambda(Publisher pub)
{
pub.Changed += (s, e) => _n++; // injected source -> OWN001 warning
@PhysShell
PhysShell merged commit 6d7d875 into main Jul 10, 2026
33 checks passed
PhysShell pushed a commit that referenced this pull request Jul 11, 2026
…abbit)

Blanket `|| true` + 2>/dev/null swallowed a real ownir error (rc >= 2):
the jq assertion would still fail the job, but the log showed only the
assertion text, not the Python error. Mirror the established rc-capture
pattern (same file, proj.sarif step): keep stderr, allow the expected
rc<=1, fail loud on rc>=2. Per the PR #211 learning.

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

Subscription precision: lambda-handler tier (inline handlers, tiered by source lifetime)

3 participants