Skip to content

fix(extractor): exempt EventSource-owned DiagnosticCounter fields from the field-disposable leak (mined Npgsql)#89

Merged
PhysShell merged 3 commits into
mainfrom
claude/fp-eventsource-counters
Jun 23, 2026
Merged

fix(extractor): exempt EventSource-owned DiagnosticCounter fields from the field-disposable leak (mined Npgsql)#89
PhysShell merged 3 commits into
mainfrom
claude/fp-eventsource-counters

Conversation

@PhysShell

@PhysShell PhysShell commented Jun 22, 2026

Copy link
Copy Markdown
Owner

What

An EventSource's diagnostic counters — EventCounter / PollingCounter / IncrementingEventCounter / IncrementingPollingCounter — are constructed with this (the parent EventSource) as their owner argument:

_bytesPerSecond = new IncrementingPollingCounter("bytes-per-second", this, () =>);

That registration hands the counter to the source: the runtime's CounterGroup pins the counter to the EventSource's lifetime, and an EventSource is the canonical process-lived static readonly Log singleton. So a counter is a process-lived diagnostic that is idiomatically never field-disposed — every BCL EventSource (RuntimeEventSource, the HTTP/ASP.NET counters) does exactly the same. Reporting such a field as an undisposed IDisposable was a false positive.

Mined

Npgsql's NpgsqlEventSource builds eight counters in OnEventCommand, none field-disposed → eight OWN001 FPs cleared. (This is the first of the Npgsql-mined fixes.)

How (narrow + sound)

A field is skipped only when both hold:

  1. the containing type derives from System.Diagnostics.Tracing.EventSource, and
  2. the field is assigned new <DiagnosticCounter-subclass>(… this …).

A counter is always built in a method body (OnEventCommand) — this is unavailable in a field initializer — so the assignment shape (the same one the constructed set already recognizes) is the only one that can match. Type checks are semantic (walk the base chain to DiagnosticCounter / EventSource); the BCL types resolve on the Linux runner.

Regression sample (EventSourceCountersSample.cs)

  • SampleEventSource's four counters (one of each kind) → SILENT.
  • Codex scope control _scratch — a plain owned IDisposable in the same EventSourceSTILL raises the OWN001 disposable-field leak, proving the exemption keys off the DiagnosticCounter type handed to this, not the EventSource class.

CI assertions added to the wpf-extractor job for both the silenced counters and the firing control. The corpus benchmark guards against any real-C# recall regression (the exemption is purely additive on a shape no corpus sample matches).

🤖 Generated with Claude Code


Generated by Claude Code

Summary by CodeRabbit

  • Tests

    • Expanded CI and Roslyn “OwnIR” assertions by introducing a new sample that exercises EventSource-owned DiagnosticCounter patterns and validates expected leak findings (including negative controls).
  • Bug Fixes

    • Updated the disposable-field leak detector to suppress undisposed-leak reports for DiagnosticCounter fields that are owned by a process-lived EventSource via this, while preserving detections for other owned disposables and mismatched ownership/type scenarios.

…m the field-disposable leak (mined Npgsql)

An EventSource's diagnostic counters — EventCounter / PollingCounter /
IncrementingEventCounter / IncrementingPollingCounter — are constructed with `this`
(the parent EventSource) as their owner argument. That registration hands the counter
to the source: the runtime's CounterGroup pins it to the EventSource's lifetime, and an
EventSource is a process-lived `static readonly Log` singleton, so a counter is a
process-lived diagnostic that is idiomatically NEVER field-disposed (every BCL
EventSource — RuntimeEventSource, the HTTP/ASP.NET counters — does the same). Reporting
such a field as an undisposed IDisposable was a false positive.

Mined: Npgsql's NpgsqlEventSource builds eight counters in OnEventCommand, none
field-disposed — eight OWN001 FPs cleared.

Scope (narrow, sound): the field is skipped only when the containing type derives from
System.Diagnostics.Tracing.EventSource AND the field is assigned `new <DiagnosticCounter
subclass>(... this ...)`. A counter is always built in a method body (OnEventCommand) —
`this` is unavailable in a field initializer — so the assignment shape (matching the
`constructed` set) is the only one that can match.

Regression sample EventSourceCountersSample.cs: SampleEventSource's four counters stay
SILENT; the Codex control `_scratch` — a plain owned IDisposable in the SAME EventSource —
STILL raises the OWN001 disposable-field leak, proving the exemption keys off the
DiagnosticCounter type handed to `this`, not the EventSource class.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
Comment thread frontend/roslyn/samples/EventSourceCountersSample.cs Fixed
@coderabbitai

coderabbitai Bot commented Jun 22, 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: 775bce84-afd4-474f-a456-6e4f231acba7

📥 Commits

Reviewing files that changed from the base of the PR and between 08c11e4 and 95e993d.

📒 Files selected for processing (2)
  • .github/workflows/ci.yml
  • frontend/roslyn/OwnSharp.Extractor/Program.cs
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/roslyn/OwnSharp.Extractor/Program.cs
  • .github/workflows/ci.yml

📝 Walkthrough

Walkthrough

Adds a false-positive exemption to the Roslyn disposable-field leak detector: DiagnosticCounter fields on EventSource subclasses that are constructed with this are excluded from undisposed-leak reporting. A new sample file (EventSourceCountersSample.cs) provides the test subject, and CI assertions verify the exemption does not extend to other owned disposable fields.

Changes

EventSource DiagnosticCounter exemption

Layer / File(s) Summary
EventSource sample and OwnedScratch
frontend/roslyn/samples/EventSourceCountersSample.cs
Defines SampleEventSource with four DiagnosticCounter fields initialized with this in OnEventCommand, a RecordBytes method, an OwnedScratch IDisposable field (_scratch) intended to still produce OWN001, and a _mixed field reassigned from MemoryStream to EventCounter to test declaration-type vs. assignment-type scoping.
Extractor helpers, scanning, and suppression guard
frontend/roslyn/OwnSharp.Extractor/Program.cs
Adds IsDerivesFromEventSource, IsDerivesFromDiagnosticCounter, and IsEventSourceOwnedCounter predicates; populates an eventSourceCounters set during field-assignment scanning in EventSource-derived classes; guards the per-field disposable fact emission to skip fields in that set when their declared type also derives from DiagnosticCounter.
CI wiring and output assertions
.github/workflows/ci.yml
Adds EventSourceCountersSample.cs to the extractor input list and inserts assertion blocks verifying counter backing fields (_bytesPerSecond, _totalBytes, _commandDuration, _totalCommands) are absent from findings while _scratch and _mixed still trigger OWN001.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • PhysShell/Own.NET#86: Both PRs extend the same disposable-field detection logic in Program.cs with new exemption patterns; PR #86 adds null-conditional and release-site recognition rules adjacent to the DiagnosticCounter suppression guard added here.

Poem

🐰 A counter for bytes, a counter for speed,
EventSource lifetime is all that you need.
No leak shall be flagged when this holds the key,
But _scratch still fires—OWN001, you see!
The rabbit exempts what belongs to the source,
And tests prove the rule stays on course. 🎉

🚥 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 accurately describes the main change: exempting EventSource-owned DiagnosticCounter fields from the field-disposable leak detection rule, which is the core purpose of this PR.
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-eventsource-counters

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: 72b6927bc5

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

continue;
// an EventSource's own DiagnosticCounter (registered to `this`) is process-lived
// and never field-disposed -> not an owned leak (mined: Npgsql NpgsqlEventSource).
if (eventSourceCounters.Contains(v.Identifier.Text))

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 Limit the counter exemption to counter fields

When an EventSource has an owned IDisposable field that is also ever assigned a counter, this name-only skip suppresses the whole field instead of just the counter construction. For example, an IDisposable _resource = new MemoryStream(); later assigned _resource = new EventCounter(..., this) would now produce no disposable-field fact at all, hiding the non-counter leak; the exemption should bind the assignment to this field and/or require the declared field to be a DiagnosticCounter-only field before continuing.

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: 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 `@frontend/roslyn/OwnSharp.Extractor/Program.cs`:
- Around line 2197-2202: The FieldName method used in the assignment loop
matches any field regardless of the receiver instance, which incorrectly
populates eventSourceCounters with fields from other-instance assignments like
other._counter. Replace the FieldName(a.Left) call with ThisFieldName(a.Left) to
ensure only current-instance field assignments (_field or this._field) are added
to eventSourceCounters. Apply this same fix to the similar code pattern at lines
2222-2225 that also uses FieldName to prevent incorrectly suppressing fields
that should not be exempted.
🪄 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: 786c438d-d266-473f-8d6d-f64fdd18d077

📥 Commits

Reviewing files that changed from the base of the PR and between 78c234b and 72b6927.

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

Comment thread frontend/roslyn/OwnSharp.Extractor/Program.cs
…nter fields (Codex P2 + CodeRabbit)

Two review findings on the over-broad first cut, both the over-suppression class:

- Codex P2 — require the field's DECLARED type to derive from DiagnosticCounter
  before skipping. A field declared as a plain IDisposable that is merely assigned
  a counter once (`IDisposable _mixed = new MemoryStream(); _mixed = new
  EventCounter(..., this);`) would otherwise have its OTHER resource (the
  MemoryStream) silently suppressed by the name-only skip. The declared-type guard
  binds the exemption to actual counter fields (Npgsql declares them as
  `IncrementingPollingCounter?` etc.), so the non-counter leak still fires.

- CodeRabbit (major) — populate the exemption set with ThisFieldName, not
  FieldName: the latter also matches `other._counter`, which could exempt this
  class's same-named field from another instance's assignment. Tightening a
  suppression to THIS instance's field (`_f` / `this._f`) is strictly safer.

Regression: EventSourceCountersSample gains `_mixed` — declared IDisposable,
initialized `new MemoryStream()` and later assigned `new EventCounter(..., this)`
— which must STILL raise OWN001 on the MemoryStream. The four genuine counter
fields stay SILENT; `_scratch` (non-counter) and `_mixed` (non-counter-declared)
both warn. CI asserts all three.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
// Codex control: a NON-counter owned IDisposable field in the same EventSource is NOT exempt.
// The exemption keys off the DiagnosticCounter type handed to `this`, not the containing class,
// so this new'd-but-never-disposed resource STILL raises OWN001 ('disposable field').
private readonly OwnedScratch _scratch = new OwnedScratch();
// must STILL leak. The exemption requires the DECLARED field type to be a DiagnosticCounter, so
// the `new MemoryStream()` owned here is not hidden by the later `_mixed = new EventCounter(...)`
// (a name-only skip would wrongly suppress the whole field). -> OWN001 on the MemoryStream.
private IDisposable _mixed = new MemoryStream();
@PhysShell PhysShell merged commit 43a4709 into main Jun 23, 2026
22 checks passed
PhysShell pushed a commit that referenced this pull request Jun 23, 2026
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