Skip to content

Claude/fp field release recognition#86

Merged
PhysShell merged 5 commits into
mainfrom
claude/fp-field-release-recognition
Jun 22, 2026
Merged

Claude/fp field release recognition#86
PhysShell merged 5 commits into
mainfrom
claude/fp-field-release-recognition

Conversation

@PhysShell

@PhysShell PhysShell commented Jun 22, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • Bug Fixes

    • Fixed handling of null-conditional disposal patterns (null-conditional operators on Dispose methods).
    • Enhanced detection of pooled buffer leaks when returns occur across multiple class methods.
    • Corrected static handler exemption evaluation for event subscriptions.
  • Tests

    • Added test samples covering field disposal scenarios and pooled buffer patterns.
    • Added verification tests for static class event handling.

claude and others added 5 commits June 22, 2026 11:09
…l release (mined ImageSharp #2,#3/4)

Two more field-detector precision fixes from the ImageSharp mining run.

#2 — null-conditional disposal. The disposed-field set matched only a plain
`field.Dispose()`; the dominant `field?.Dispose()` (a ConditionalAccess whose WhenNotNull
is the .Dispose() invocation) was missed, so a field disposed that way was reported as a
leak. Mined across ImageSharp: `this.memoryStream?.Dispose()` (ZipExrCompressor,
DeflateCompressor, IccDataWriter) and the BufferedStreams benchmark's `[GlobalCleanup]`.

#3 — cross-member pool release. The per-member pool pass required the Return in the same
member as the Rent, but a FIELD buffer is rented in the ctor and returned in Dispose
(different members). A field is now also released if `pool.Return(field)` appears ANYWHERE
in the class (a field name is class-unique, so no cross-masking; locals keep per-member
scoping), or if the buffer is TRANSFERRED into a `new Guard(field)` that the object stores
in a field — the #80 escaping-ctor transfer at field level. Mined: BufferedReadStream
(returns this.readBuffer in Dispose(bool)) and SharedArrayPoolBuffer (LifetimeGuard).

Regression sample FieldReleaseSample.cs: `field?.Dispose()` and the cross-member /
guard-transferred pooled fields stay silent; an undisposed field and a rented-never-returned
pooled field still warn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
…eturn only (Codex)

Codex P2: treating a rented field passed to ANY field-stored `new X(field)` as released
wrongly suppresses a real leak for NON-owning wrappers (`_view = new ReadOnlyMemory<byte>(_buf)`
and other cached views), where the array is never returned. Soundly distinguishing an owning
guard (that Returns the buffer) from a non-owning view is not worth it for the single
SharedArrayPoolBuffer FP, so the guard-transfer is removed entirely.

Fix #3 keeps only the sound, high-value part: a pooled FIELD is released if `pool.Return(field)`
appears anywhere in the class (cross-member ctor-rent + Dispose-return — BufferedReadStream).
SharedArrayPoolBuffer's indirect release via its LifetimeGuard is left as an honest known
limitation. Sample/CI drop the PoolFieldTransferredToGuard case accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
…ned ImageSharp FP #4/4)

A `static class` has no instance, so a static-source subscription from it cannot promote an
instance to the source's lifetime — the OWN014 region escape is vacuous. Mined:
SixLabors/ImageSharp's MemoryAllocatorValidator, a `static class` whose static ctor hooks the
static `MemoryDiagnostics.MemoryAllocated`/`MemoryReleased` events, was wrongly reported as
"promotes MemoryAllocatorValidator to process lifetime" — there is no instance to promote.

The static-source escape skip now also fires when the enclosing type is a static class (next
to the existing process-lived `App` case). Scoped to the OWN014 escape only — OWN001 token
leaks are untouched — and to non-timers, like the App case.

(The two MemoryDiagnosticsTests OWN014s — lambdas in `static void RunTest` local functions of
an INSTANCE class — are left honest: no `this` is captured so the named-instance claim is
wrong, but the closure is retained, so this is the murky static-context territory, not a clean
static-class drop.)

Regression sample StaticClassEscapeSample.cs: a static class subscribing a LAMBDA (not covered
by the static-method-handler exemption) to a static event stays silent; the existing
StaticEventEscapeViewModel proves an INSTANCE class on the same shape still raises OWN014.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
…op static-class escape suppression (Codex)

Codex P2: blanket-suppressing OWN014 for ALL static-source subscriptions in a static class is
unsound — a CAPTURING lambda still retains its compiler-generated display-class instance for the
process, even with no `this`. Codex's pointer is right: the mined case (MemoryAllocatorValidator)
uses static-METHOD handlers, which the existing IsStaticHandler is meant to exempt; it missed them
because a method group's symbol can surface as a member group (Symbol == null, CandidateSymbols
populated).

So: make IsStaticHandler fall back to CandidateSymbols (requiring ALL candidates static, so a
mixed overload set is not wrongly exempted), and revert the static-class suppression entirely. A
static-method handler in a static class is now exempted by IsStaticHandler (null target, no
instance), while a capturing lambda there still escapes — sound.

Sample now uses a static-METHOD handler on a static event from a static class's static ctor
(replicating MemoryAllocatorValidator) and must stay silent; StaticEventEscapeViewModel still
proves an instance handler on a static event escapes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
fix(extractor): resolve member-group symbols in IsStaticHandler (mined ImageSharp FP #4/4)
// #2 control: an IDisposable field the class new's but never disposes -> must WARN.
public sealed class NeverDisposesField
{
private readonly MemoryStream stream = new();
{
private readonly byte[] leakedBuf;

public PoolFieldLeaked(int n) => this.leakedBuf = ArrayPool<byte>.Shared.Rent(n);
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Three extractor logic fixes are added to Program.cs: IsStaticHandler falls back to CandidateSymbols when no bound symbol resolves; null-conditional field?.Dispose() patterns are recognized as disposed fields; and POOL001 rent analysis tracks field-backed buffers returned anywhere in a class. Two fixture files and corresponding CI assertions validate all new behaviors.

Changes

Extractor Logic, Fixtures, and CI Coverage

Layer / File(s) Summary
Fixture files and CI extractor inputs
frontend/roslyn/samples/FieldReleaseSample.cs, frontend/roslyn/samples/StaticClassEscapeSample.cs, .github/workflows/ci.yml
Adds FieldReleaseSample.cs with four classes covering null-conditional disposal, undisposed field, pooled-buffer return, and pooled-buffer leak; adds StaticClassEscapeSample.cs with StaticDiagnosticsBus and StaticAllocationCounter; wires both into the CI Roslyn extractor command.
IsStaticHandler candidate-symbol fallback and null-conditional disposal
frontend/roslyn/OwnSharp.Extractor/Program.cs
IsStaticHandler adds a two-phase check: a bound IMethodSymbol must have IsStatic, otherwise all CandidateSymbols must be static. Null-conditional ConditionalAccessExpressionSyntax nodes for field?.Dispose()/DisposeAsync() are now recognized as releasing that field.
POOL001 cross-member field-backed rent/return tracking
frontend/roslyn/OwnSharp.Extractor/Program.cs
Introduces a class-wide fieldReleased set from pool.Return(field) calls; rent collection records an IsField flag; the emitted released value is true when a local return exists in the member or the field name appears in fieldReleased.
CI assertions for new behaviors
.github/workflows/ci.yml
Adds shell assertions: DisposesViaConditional is silent, NeverDisposesField warns, returnedBuf is silent, leakedBuf warns, and StaticAllocationCounter does not produce an OWN014 region-escape finding.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • PhysShell/Own.NET#20: Modifies the same disposal-leak/field-release detection logic in Program.cs (WPF003), extending how DisposeAsync and disposal operations are recognized and when fields are exempted.
  • PhysShell/Own.NET#52: Updates the same POOL001 rent/return flow-engine and pooled-buffer handling in Program.cs that this PR extends with field-backed cross-member tracking.
  • PhysShell/Own.NET#53: Modifies the same Rent/Return detection in Program.cs, switching to SemanticModel-based recognition, directly preceding the field-tracking logic added here.

Poem

🐇 Hop hop, the rabbit checks each field,
null-conditional ?.Dispose() is now healed!
Static handlers ask their candidates' view,
pooled buffers returned across members too.
The CI asserts with a confident cheer —
no leaks shall escape, my fluffy career! 🌿

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Claude/fp field release recognition' directly references the branch name and describes the main feature being added (field release recognition for pooled buffers and null-conditional disposal patterns).
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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/fp-field-release-recognition

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

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

ℹ️ 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 on lines +2391 to +2394
if (inv.Expression is MemberAccessExpressionSyntax rm
&& rm.Name.Identifier.Text == "Return"
&& inv.ArgumentList.Arguments.Count > 0
&& model.GetSymbolInfo(inv.ArgumentList.Arguments[0].Expression).Symbol is IFieldSymbol rfs)

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 Require ArrayPool.Return for field pool releases

When a field rented from ArrayPool<T> is assigned in one member, this class-wide release set treats any invocation named Return with that field as the first argument as a release, without verifying that the receiver is actually System.Buffers.ArrayPool<T>. In a class that calls something like someCache.Return(this._buf) or another unrelated Return(_buf), fieldReleased marks _buf released and the later pool fact is suppressed, so a real rented-but-never-returned buffer is missed. Please reuse the semantic ArrayPool check used for rents/PoolReturnBuffer before adding the field to this set.

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.

🧹 Nitpick comments (1)
frontend/roslyn/OwnSharp.Extractor/Program.cs (1)

2399-2415: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider renaming isField for clarity.

The isField flag is set to true for all AssignmentExpressionSyntax matches, but FieldName() extracts names syntactically without semantic verification. A local variable assigned via buf = pool.Rent(...) would also set isField=true.

The current behavior is correct in practice because fieldReleased only contains actual fields (the IFieldSymbol check at line 2394 ensures this), so a local won't match. However, there's a rare edge case: if a local shadows a field name, and that field is returned elsewhere, the local's rent would be incorrectly marked released.

Consider either:

  1. Renaming to isAssignment to reflect actual semantics, or
  2. Using semantic resolution on the LHS to verify it's actually a field
🤖 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 `@frontend/roslyn/OwnSharp.Extractor/Program.cs` around lines 2399 - 2415, The
variable name isField is misleading because the flag is set to true for all
AssignmentExpressionSyntax cases regardless of whether the assignment target is
actually a field or a local variable - the FieldName() method only extracts
names syntactically without semantic verification. To fix this, either rename
isField to isAssignment throughout this block (in the tuple definition, the
switch expression, and the rented.Add call) to accurately reflect that it
indicates an assignment expression rather than a guaranteed field, or
alternatively implement semantic resolution on the assignment's left-hand side
using the model parameter to verify that it actually refers to an IFieldSymbol
before setting the flag to true.
🤖 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 `@frontend/roslyn/OwnSharp.Extractor/Program.cs`:
- Around line 2399-2415: The variable name isField is misleading because the
flag is set to true for all AssignmentExpressionSyntax cases regardless of
whether the assignment target is actually a field or a local variable - the
FieldName() method only extracts names syntactically without semantic
verification. To fix this, either rename isField to isAssignment throughout this
block (in the tuple definition, the switch expression, and the rented.Add call)
to accurately reflect that it indicates an assignment expression rather than a
guaranteed field, or alternatively implement semantic resolution on the
assignment's left-hand side using the model parameter to verify that it actually
refers to an IFieldSymbol before setting the flag to true.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9c9e73a1-7ca3-4b99-b9bc-17d2555c6130

📥 Commits

Reviewing files that changed from the base of the PR and between b787ffc and eeafdb6.

📒 Files selected for processing (4)
  • .github/workflows/ci.yml
  • frontend/roslyn/OwnSharp.Extractor/Program.cs
  • frontend/roslyn/samples/FieldReleaseSample.cs
  • frontend/roslyn/samples/StaticClassEscapeSample.cs

@PhysShell
PhysShell merged commit ea15d74 into main Jun 22, 2026
22 checks passed
PhysShell pushed a commit that referenced this pull request Jun 22, 2026
…handlers (Codex P1)

Codex: the first cut keyed only off the EVENT (the AppDomain source), ignoring the handler —
so it also dropped a real region escape, e.g. `AppDomain.CurrentDomain.ProcessExit += (_,_) =>
_field++` or an instance-method handler, whose delegate target is the subscriber and is pinned
to the process until shutdown. The Npgsql case is safe only because its lambda is non-capturing
(a static `ClearAll()` call).

HandlerRetainsNoInstance now gates the exemption: a static method group (null target) or a
lambda that captures neither `this` (explicit, or implicit via an instance member) nor an
enclosing local/parameter. A capturing handler stays OWN014.

Sample: ShutdownCleanup gains the 4th event (FirstChanceException, CodeRabbit) and a new
CapturingShutdownSubscriber (`(_,_) => _count++`) that must STILL warn. CI: assert no OWN014
anywhere in the sample file (format-insensitive, CodeRabbit) for the exempt cases, and OWN014
for both the non-AppDomain and the capturing controls.

(Rebased onto current main — #86/#87 landed; resolved the ci.yml sample-list conflict.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
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.

3 participants