From b31da25f185c0b762d185aa23677d0051f092923 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 03:53:51 +0000 Subject: [PATCH 1/9] oracle(dev): wire push-trigger to this branch; target Newtonsoft.Json The integration token can't workflow_dispatch (403), so use the existing push-sentinel fallback to kick the cross-tool oracle. Adds this dev branch to the push allowlist and points corpus/oracle-target.txt at JamesNK/Newtonsoft.Json for a blind precision run. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_011YMngnax1jsy378fSAZb9r --- .github/workflows/oracle.yml | 1 + corpus/oracle-target.txt | 17 +++++------------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/.github/workflows/oracle.yml b/.github/workflows/oracle.yml index 0d64a80a..f051b5c4 100644 --- a/.github/workflows/oracle.yml +++ b/.github/workflows/oracle.yml @@ -45,6 +45,7 @@ on: push: branches: - claude/zen-pasteur-76hfs1 + - claude/own-net-tasks-3k6d07 paths: - corpus/oracle-target.txt diff --git a/corpus/oracle-target.txt b/corpus/oracle-target.txt index 52cb6d64..d625f363 100644 --- a/corpus/oracle-target.txt +++ b/corpus/oracle-target.txt @@ -3,15 +3,8 @@ # or "local:" (a fixture copied into the oracle's target/). # Optional lines: ref=, paths=, build=, include_tests=. Dev-branch only. # -# Cross-tool oracle on a Linux-buildable fixture, so ALL THREE tools run (Infer# -# included — ScreenToGif's WPF won't build on Linux). Re-run after the OWN014 -# migration (PR #35): the fixture's SUBSCRIPTION leak (#1 — SystemEvents.Display -# SettingsChanged += with no -=) is now lowered by the extractor to a `capture` -# fact and reported as OWN014 (region escape), not OWN001; OWN014 was added to the -# comparator's OWN_LEAK set. So #1 must STILL land "Own.NET-only" (CodeQL / Infer# -# have no event-subscription-leak query) — this run confirms the migration kept the -# differentiation, on real cross-tool output rather than the selftest's fixtures. -# Expect: #1 Own.NET-only (now OWN014), #2/#3/#4 dispose leaks in "Agree", 0 -# oracle-only. -local:corpus/fixtures/systemevents-console -build=SystemEventsLeak.csproj +# Blind cross-tool run on a mature, well-disciplined C# library — JSON readers/ +# writers are IDisposable (JsonTextReader/JsonTextWriter wrap StreamReader/Writer). +# Precision probe: expect own-only ~0; triage any oracle-only into field-notes +# per the oracle.md maintenance requirement. +JamesNK/Newtonsoft.Json From 5b3547e1063096963490d8f98f8eca4c297ea9ad Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 03:56:28 +0000 Subject: [PATCH 2/9] oracle(dev): target serilog/serilog Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_011YMngnax1jsy378fSAZb9r --- corpus/oracle-target.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/corpus/oracle-target.txt b/corpus/oracle-target.txt index d625f363..231f00f7 100644 --- a/corpus/oracle-target.txt +++ b/corpus/oracle-target.txt @@ -3,8 +3,8 @@ # or "local:" (a fixture copied into the oracle's target/). # Optional lines: ref=, paths=, build=, include_tests=. Dev-branch only. # -# Blind cross-tool run on a mature, well-disciplined C# library — JSON readers/ -# writers are IDisposable (JsonTextReader/JsonTextWriter wrap StreamReader/Writer). -# Precision probe: expect own-only ~0; triage any oracle-only into field-notes +# Blind cross-tool run on a logging library — file/rolling sinks wrap +# StreamWriter/TextWriter and buffers (resource-heavy paths). Higher chance of a +# real oracle-only recall finding than a pure data lib; triage into field-notes # per the oracle.md maintenance requirement. -JamesNK/Newtonsoft.Json +serilog/serilog From 295b9b0f003cb4c82f96d27a5b8e32b6ef8fb110 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 03:56:47 +0000 Subject: [PATCH 3/9] oracle(dev): target StackExchange.Redis Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_011YMngnax1jsy378fSAZb9r --- corpus/oracle-target.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/corpus/oracle-target.txt b/corpus/oracle-target.txt index 231f00f7..6148c6b3 100644 --- a/corpus/oracle-target.txt +++ b/corpus/oracle-target.txt @@ -3,8 +3,8 @@ # or "local:" (a fixture copied into the oracle's target/). # Optional lines: ref=, paths=, build=, include_tests=. Dev-branch only. # -# Blind cross-tool run on a logging library — file/rolling sinks wrap -# StreamWriter/TextWriter and buffers (resource-heavy paths). Higher chance of a -# real oracle-only recall finding than a pure data lib; triage into field-notes -# per the oracle.md maintenance requirement. -serilog/serilog +# Blind cross-tool run on a Redis client — sockets, pipes, connection +# multiplexers (lots of native/IDisposable resources, async/interprocedural). +# Large; Infer# build may be partial. Triage oracle-only into field-notes per +# the oracle.md maintenance requirement. +StackExchange/StackExchange.Redis From 67073f5818bc641e08f73f4e6644869dcb9e5e11 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 03:56:55 +0000 Subject: [PATCH 4/9] oracle(dev): target RestSharp/RestSharp Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_011YMngnax1jsy378fSAZb9r --- corpus/oracle-target.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/corpus/oracle-target.txt b/corpus/oracle-target.txt index 6148c6b3..963302dd 100644 --- a/corpus/oracle-target.txt +++ b/corpus/oracle-target.txt @@ -3,8 +3,8 @@ # or "local:" (a fixture copied into the oracle's target/). # Optional lines: ref=, paths=, build=, include_tests=. Dev-branch only. # -# Blind cross-tool run on a Redis client — sockets, pipes, connection -# multiplexers (lots of native/IDisposable resources, async/interprocedural). -# Large; Infer# build may be partial. Triage oracle-only into field-notes per -# the oracle.md maintenance requirement. -StackExchange/StackExchange.Redis +# Blind cross-tool run on an HTTP client lib — HttpClient/HttpRequestMessage +# ownership-transfer patterns (the Polly Bulkhead shape that exercised our +# escape analysis). Triage oracle-only into field-notes per the oracle.md +# maintenance requirement. +RestSharp/RestSharp From 0f6117b192623ce97d95c4dd7b7a048d21eec245 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 04:17:31 +0000 Subject: [PATCH 5/9] oracle: exclude dotted .NET test dirs (Newtonsoft.Json.Tests) from product diff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _is_test_path matched a path segment only as a whole word, so the .NET .Tests / Foo.Benchmarks convention slipped through: the dir 'Newtonsoft.Json.Tests' is one segment, never equal to 'tests', so ~485 test findings leaked into the product diff (own-only 489 vs the true 4). Match each dot-component of a segment instead — catching the 'tests' tail while leaving single-component product dirs (SnippetEngine, Documentation, Newtonsoft.Json) untouched. Adds selftest cases for the dotted dir and its product sibling. Verified on the recorded Newtonsoft oracle output: own-only 489 -> 4, oracle_other 903 -> 235. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_011YMngnax1jsy378fSAZb9r --- corpus/oracle-target.txt | 10 +++++----- scripts/oracle_compare.py | 36 ++++++++++++++++++++++++++---------- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/corpus/oracle-target.txt b/corpus/oracle-target.txt index 963302dd..2265aebd 100644 --- a/corpus/oracle-target.txt +++ b/corpus/oracle-target.txt @@ -3,8 +3,8 @@ # or "local:" (a fixture copied into the oracle's target/). # Optional lines: ref=, paths=, build=, include_tests=. Dev-branch only. # -# Blind cross-tool run on an HTTP client lib — HttpClient/HttpRequestMessage -# ownership-transfer patterns (the Polly Bulkhead shape that exercised our -# escape analysis). Triage oracle-only into field-notes per the oracle.md -# maintenance requirement. -RestSharp/RestSharp +# Clean re-run after fixing _is_test_path to drop dotted .NET test dirs +# (Newtonsoft.Json.Tests). The first run leaked ~485 test findings into the diff +# (own-only 489); with the fix the product picture is own-only 4. This confirms it +# on fresh cross-tool output, not just the local replay. +JamesNK/Newtonsoft.Json diff --git a/scripts/oracle_compare.py b/scripts/oracle_compare.py index 108c4794..672bb9e9 100644 --- a/scripts/oracle_compare.py +++ b/scripts/oracle_compare.py @@ -379,13 +379,18 @@ def _is_test_path(path: str) -> bool: examples are never disposed by design) — counting them as product leaks inflates the oracle-only recall gap with example code that was never meant to dispose.""" for seg in path.lower().split("/"): - # Exact match for short, collision-prone names — a `snippet` *prefix* would - # wrongly drop product dirs like `SnippetEngine`/`SnippetService` (Polly's - # `src/Snippets/Docs/*` is caught by the exact `snippets`/`docs` segments - # anyway). Prefix match only for the long, unambiguous plural-able ones. - if (seg in ("test", "tests", "doc", "docs", "snippet", "snippets") - or seg.startswith(("benchmark", "sample", "example"))): - return True + # Split each segment on '.' before matching, for the ubiquitous .NET + # `.Tests` / `Foo.Benchmarks` directory convention: the dir + # `Newtonsoft.Json.Tests` is ONE path segment, so a bare-segment check + # misses it (and silently drags ~485 test findings into the diff). Matching + # each dot-component catches the `tests` tail while keeping the original + # guards intact — exact match for short, collision-prone names (so a single + # component `SnippetEngine`/`Documentation` is NOT dropped, only an exact + # `snippet`/`doc`), prefix only for the long, unambiguous plural-able ones. + for part in seg.split("."): + if (part in ("test", "tests", "doc", "docs", "snippet", "snippets") + or part.startswith(("benchmark", "sample", "example"))): + return True return False @@ -560,10 +565,21 @@ def _selftest() -> int: ("tests/Foo/Bar.cs", "benchmarks/X/Y.cs", "src/Test/Z.cs", "src/Snippets/Docs/Fallback.cs", "src/MyLib/docs/Example.cs")): fails.append("_is_test_path should match test/benchmark/doc/snippet trees") + # The .NET `.Tests` / `Foo.Benchmarks` convention: the dotted dir is a + # single path segment, so it must be matched on its dot-components — else a repo + # like Newtonsoft.Json (test tree `Newtonsoft.Json.Tests`) leaks ~485 test + # findings into the product diff (own-only 489 vs the true 4). The sibling + # product dir `Newtonsoft.Json` must stay IN scope. + if not all(_is_test_path(p) for p in + ("Src/Newtonsoft.Json.Tests/Bson/BsonReaderTests.cs", + "src/Foo.Benchmarks/Bench.cs")): + fails.append("_is_test_path should match dotted .NET test/benchmark project dirs") # ...but a product dir whose name merely *starts with* a marker word is NOT - # excluded — exact match for snippet/doc guards against dropping real code. + # excluded — exact match for snippet/doc guards against dropping real code, and + # the dot-split must leave a non-test dotted product namespace untouched. if any(_is_test_path(p) for p in ("Dapper/SqlMapper.cs", "src/Lib/A.cs", - "src/SnippetEngine/Foo.cs", "src/Documentation/Api.cs")): + "src/SnippetEngine/Foo.cs", "src/Documentation/Api.cs", + "Src/Newtonsoft.Json/JsonSerializer.cs")): fails.append("_is_test_path should not match product paths") # the scope note is gated on mode, not count: a product-only run that excluded # nothing must still say so (else it reads like a full-scope run). @@ -607,7 +623,7 @@ def _selftest() -> int: fails.append(f"non-SARIF JSON masked as clean: {len(not_sarif)} findings, " f"{ns_drift} unparsed") - total = 24 + total = 25 for f in fails: print(f"ORACLE SELFTEST FAIL: {f}") print(f"oracle_compare selftest: {total - len(fails)}/{total} checks passed") From 7ab100971dd5176f03516775bc5395697b22e1b5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 04:50:56 +0000 Subject: [PATCH 6/9] docs(field-notes): record two own-only over-report idioms from the Newtonsoft run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First entries where Own.NET (not the oracle) over-reports, triaged from the 4 product own-only findings on JamesNK/Newtonsoft.Json: - #7 event '+=' on a freshly-created, RETURNED publisher (JsonSerializer:717): the dual of ownership transfer; bounded by the returned object's lifetime, so no '-=' needed. Already tiered as an advisory warning ('injected'), not a hard error — surfaces only under --severity warning. Proper fix is interprocedural publisher provenance. - #8 owning field whose IDisposable holds no unmanaged resource (TraceJsonReader/ Writer StringWriter/JsonTextWriter): a StringWriter is StringBuilder-backed, not a handle. Fixable by adding System.IO.StringWriter/StringReader to the existing IsDisposeOptional allowlist (mirrors Task/DataTable); the JsonTextWriter wrapper stays a low-value residual. Both verdicts: not real leaks, criticality ~0. CodeQL/Infer# correctly silent. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_011YMngnax1jsy378fSAZb9r --- docs/notes/field-notes-patterns.md | 98 +++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 3 deletions(-) diff --git a/docs/notes/field-notes-patterns.md b/docs/notes/field-notes-patterns.md index 9d88d511..9442177a 100644 --- a/docs/notes/field-notes-patterns.md +++ b/docs/notes/field-notes-patterns.md @@ -178,17 +178,109 @@ viewed through an adapter. **When a type wraps a disposable it doesn't own, the correct behaviour is to forward disposal, not perform it — and an analyzer must not count the wrapped allocation against the wrapper.** +## 7. Event subscription on a freshly-created, *returned* publisher + +**Seen in:** Newtonsoft.Json `Src/Newtonsoft.Json/JsonSerializer.cs:717` +(`ApplySerializerSettings`, commit `4f73e74`). + +A factory configures a new object and wires an event on it before handing it back; +the subscription is never `-=`'d, but it doesn't need to be. + +```csharp +public static JsonSerializer Create(JsonSerializerSettings? settings) +{ + JsonSerializer serializer = new JsonSerializer(); + if (settings != null) ApplySerializerSettings(serializer, settings); + return serializer; // publisher escapes to the caller +} +private static void ApplySerializerSettings(JsonSerializer serializer, JsonSerializerSettings settings) +{ + if (settings.Error != null) + serializer.Error += settings.Error; // <- flagged: "+= but never -=" +} +``` + +**Why:** the *publisher* (`serializer`) is the returned object. The handler lives +exactly as long as the serializer the caller now holds; when that is collected, the +subscription dies with it. No `-=` is needed — there is no longer-lived source +retaining a shorter-lived target (the dangerous direction our OWN001/OWN014 +subscription leak targets). It's the publisher itself that is short/caller-scoped. + +**Analyzer angle — this is an *own-only* over-report (the first in this notebook +where Own.NET, not the oracle, is too strict).** CodeQL/Infer# have no +event-subscription-leak query, so they stay silent (correctly). Own.NET fires +because, *inside* `ApplySerializerSettings`, `serializer` is an opaque **parameter** +— `SubscriptionSourceKind` can't see that the caller `new`s it and `return`s it, so +it conservatively tiers it `injected` and emits a **warning** (not a hard error; +P-004 severity tiering already hedges unknown-lifetime publishers). So the default +`error` posture is unaffected — it only surfaces under `--severity warning` (the +oracle's setting). **Subscription-leak analysis is the dual of ownership transfer: +a `+=` on a publisher that *escapes by return* is as bounded as a returned +`IDisposable` — the fix is the same "follow the reference" escape rule, but +interprocedural (the construct-and-return is in the caller), which is the hard part. +The honest interim posture — advisory warning, never a hard error — is already in +place.** + +## 8. Owning field whose IDisposable holds no unmanaged resource + +**Seen in:** Newtonsoft.Json `Src/Newtonsoft.Json/Serialization/TraceJsonReader.cs:37,38` +and `TraceJsonWriter.cs:39` (commit `4f73e74`). + +A type owns a disposable field but never disposes it — and that is fine, because the +field's `Dispose()` frees nothing real. + +```csharp +internal class TraceJsonReader : JsonReader // JsonReader : IDisposable, no Dispose override +{ + private readonly StringWriter _sw; // StringBuilder-backed: Dispose() is a no-op + private readonly JsonTextWriter _textWriter; // wraps _sw; at most returns a pooled char buffer + public TraceJsonReader(JsonReader inner) + { + _sw = new StringWriter(CultureInfo.InvariantCulture); + _textWriter = new JsonTextWriter(_sw); + } +} +``` + +**Why:** these are short-lived, per-call diagnostic helpers (created only when a +`TraceWriter` is attached at `Verbose`) that capture the JSON text into an in-memory +`StringWriter`. A `StringWriter` wraps a `StringBuilder` — no OS handle, no +unmanaged state; `Dispose()` just flips a closed flag. Not disposing it leaks +nothing the GC won't reclaim. + +**Analyzer angle — also an *own-only* over-report.** CodeQL/Infer# stay silent +(they model real resource handles; an undisposed `StringWriter` isn't one). Own.NET's +owning-field detector flags every `IDisposable` field equally. The lever already +exists: `IsOwnedDisposableType` exempts `IsDisposeOptional` types (Task/ValueTask/ +DataTable/DataSet/DataView — "Dispose is a no-op / only a lazy wait handle"). The +fix for the two `StringWriter` fields is to **add `System.IO.StringWriter`/ +`StringReader` to that exemption** — a one-liner mirroring the existing set. The +third field (`JsonTextWriter`, a third-party writer over the StringWriter, which on +Dispose returns a pooled char buffer) is not generically exemptable by name and +stays a low-value residual. **Rule of thumb: "owns an `IDisposable` field" is only a +leak when the field owns a *real* resource — a string/in-memory writer is not one, +and the `IsDisposeOptional` allowlist is where that knowledge belongs.** + --- ## The through-line -Five of six entries are the *same lesson from different angles*: **disposal +Entries 1–6 are the *same lesson from different angles*: **disposal responsibility travels with the reference** — out of a factory (1, 6), forward in time via a callback (2), into a pool (3), or down a `using` on a value type (4). Naive "every disposable needs a lexical `using`/`Dispose` on every path" checks misread all of them, which is why Infer#/CodeQL over-report here and a -transfer/escape-aware checker (Own.NET) correctly stays quiet. Worth learning as -C#; worth pinning as the precision frontier. +transfer/escape-aware checker (Own.NET) correctly stays quiet. + +Entries 7–8 are the **mirror image — the first cases where _Own.NET_ is the one +over-reporting and the oracle is correctly silent.** They map our own precision +frontier: a subscription on a publisher that *escapes by return* (7, the dual of +ownership transfer — bounded, but the construct-and-return is interprocedural), and +an owning field whose `IDisposable` holds no real resource (8, a `StringWriter` is +not a handle — belongs in the `IsDisposeOptional` allowlist). Same moral as 1–6, +pointed back at us: **a leak is about the *resource* and the *reference's +lifetime*, not the mere presence of an `IDisposable` and a missing `Dispose`/`-=`.** +Worth learning as C#; worth pinning as the precision frontier. ## Maintaining this notebook (a repo requirement) From dfb0835109ce692ce982e539ac56ebfc3c82fb7b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 05:11:24 +0000 Subject: [PATCH 7/9] extractor: treat StringWriter/StringReader fields as dispose-optional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A StringWriter is StringBuilder-backed and a StringReader reads a string — neither holds an OS handle or unmanaged resource, so Dispose() frees nothing real and an undisposed owning field of these is not a leak. Add System.IO StringWriter/StringReader to IsDisposeOptional (the same lever already used for Task/ValueTask/DataTable/DataSet/ DataView), so the owned-field detector stays silent on them. MemoryStream is deliberately NOT exempted — it can own a real buffer and still warns. Surfaced by the oracle on Newtonsoft.Json (field-notes #8): TraceJsonReader/TraceJsonWriter own undisposed StringWriter/JsonTextWriter fields; CodeQL/Infer# correctly stay silent on the StringWriter (not a real resource), Own.NET over-reported. Adds a HolderWithStringWriter control to ResolvedDisposableSample + a CI assertion it stays silent. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_011YMngnax1jsy378fSAZb9r --- .github/workflows/ci.yml | 6 ++++++ frontend/roslyn/OwnSharp.Extractor/Program.cs | 9 ++++++++- frontend/roslyn/samples/ResolvedDisposableSample.cs | 13 +++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66e7c0c4..60d4c9ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -257,6 +257,12 @@ jobs: if echo "$out" | grep -q "HolderWithDisposeOptional"; then echo "FAIL: a dispose-optional (Task/DataTable) field was wrongly flagged"; exit 1 fi + # the same rule for string-backed reader/writer fields (field-notes #8, Newtonsoft + # TraceJsonReader/Writer): a new'd, undisposed StringWriter/StringReader holds no + # unmanaged resource -> must stay SILENT (IsDisposeOptional, System.IO). + if echo "$out" | grep -q "HolderWithStringWriter"; then + echo "FAIL: a dispose-optional (StringWriter/StringReader) field was wrongly flagged"; exit 1 + fi # field release recognition (mined: ImageSharp). #2 null-conditional dispose # `field?.Dispose()` must be recognized -> silent; the undisposed control still warns. if echo "$out" | grep -q "DisposesViaConditional"; then diff --git a/frontend/roslyn/OwnSharp.Extractor/Program.cs b/frontend/roslyn/OwnSharp.Extractor/Program.cs index 9264a43d..e516cbd3 100644 --- a/frontend/roslyn/OwnSharp.Extractor/Program.cs +++ b/frontend/roslyn/OwnSharp.Extractor/Program.cs @@ -714,7 +714,14 @@ static bool IsDisposeOptional(ITypeSymbol t) { var ns = t.ContainingNamespace?.ToString(); return (ns == "System.Threading.Tasks" && t.Name is "Task" or "ValueTask") - || (ns == "System.Data" && t.Name is "DataTable" or "DataSet" or "DataView"); + || (ns == "System.Data" && t.Name is "DataTable" or "DataSet" or "DataView") + // System.IO string-backed reader/writer: a StringWriter wraps a StringBuilder + // and a StringReader reads a string — neither holds an OS handle / unmanaged + // resource, so Dispose() frees nothing real and an undisposed one is not a leak. + // (field-notes #8, mined on Newtonsoft.Json's TraceJsonReader/TraceJsonWriter, + // where these are owning fields the trace helpers never dispose.) MemoryStream is + // deliberately NOT here — it can own a real buffer, so it still warns. + || (ns == "System.IO" && t.Name is "StringWriter" or "StringReader"); } // A type that is System.Windows.Forms.Form or derives from it (semantic, walks the diff --git a/frontend/roslyn/samples/ResolvedDisposableSample.cs b/frontend/roslyn/samples/ResolvedDisposableSample.cs index 3c57b612..2d9c2b2f 100644 --- a/frontend/roslyn/samples/ResolvedDisposableSample.cs +++ b/frontend/roslyn/samples/ResolvedDisposableSample.cs @@ -52,3 +52,16 @@ public sealed class HolderWithDisposeOptional public int Use() => this.task.Id + this.table.Columns.Count; } + +// String-backed reader/writer dispose-optional control (field-notes #8, mined on +// Newtonsoft.Json's TraceJsonReader/TraceJsonWriter): a StringWriter wraps a +// StringBuilder and a StringReader reads a string — no unmanaged resource, so Dispose() +// frees nothing real. A new'd, undisposed field of these must stay SILENT (IsDisposeOptional). +// Contrast HolderWithRealDisposable's MemoryStream above, which still warns. +public sealed class HolderWithStringWriter +{ + private readonly StringWriter writer = new(); + private readonly StringReader reader = new("x"); + + public string Use() => this.writer.ToString() + this.reader.ReadToEnd(); +} From e661573746dd233bd04ff4fd9d5c2dc830a50b15 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 05:11:24 +0000 Subject: [PATCH 8/9] oracle(dev): revert push-trigger wiring before PR to main Removes this dev branch from the oracle workflow push allowlist and restores corpus/oracle-target.txt to the committed fixture. The wiring was a dev-loop mechanism (the automation token can't workflow_dispatch); the workflow comment flags it 'remove before merging to main'. Net diff of both files vs main is now zero. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_011YMngnax1jsy378fSAZb9r --- .github/workflows/oracle.yml | 1 - corpus/oracle-target.txt | 17 ++++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/oracle.yml b/.github/workflows/oracle.yml index f051b5c4..0d64a80a 100644 --- a/.github/workflows/oracle.yml +++ b/.github/workflows/oracle.yml @@ -45,7 +45,6 @@ on: push: branches: - claude/zen-pasteur-76hfs1 - - claude/own-net-tasks-3k6d07 paths: - corpus/oracle-target.txt diff --git a/corpus/oracle-target.txt b/corpus/oracle-target.txt index 2265aebd..52cb6d64 100644 --- a/corpus/oracle-target.txt +++ b/corpus/oracle-target.txt @@ -3,8 +3,15 @@ # or "local:" (a fixture copied into the oracle's target/). # Optional lines: ref=, paths=, build=, include_tests=. Dev-branch only. # -# Clean re-run after fixing _is_test_path to drop dotted .NET test dirs -# (Newtonsoft.Json.Tests). The first run leaked ~485 test findings into the diff -# (own-only 489); with the fix the product picture is own-only 4. This confirms it -# on fresh cross-tool output, not just the local replay. -JamesNK/Newtonsoft.Json +# Cross-tool oracle on a Linux-buildable fixture, so ALL THREE tools run (Infer# +# included — ScreenToGif's WPF won't build on Linux). Re-run after the OWN014 +# migration (PR #35): the fixture's SUBSCRIPTION leak (#1 — SystemEvents.Display +# SettingsChanged += with no -=) is now lowered by the extractor to a `capture` +# fact and reported as OWN014 (region escape), not OWN001; OWN014 was added to the +# comparator's OWN_LEAK set. So #1 must STILL land "Own.NET-only" (CodeQL / Infer# +# have no event-subscription-leak query) — this run confirms the migration kept the +# differentiation, on real cross-tool output rather than the selftest's fixtures. +# Expect: #1 Own.NET-only (now OWN014), #2/#3/#4 dispose leaks in "Agree", 0 +# oracle-only. +local:corpus/fixtures/systemevents-console +build=SystemEventsLeak.csproj From 19471e3e8bb5f8d8c8536ce4b9c976e9b5318d2a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 27 Jun 2026 05:21:40 +0000 Subject: [PATCH 9/9] fix(oracle): match test/doc markers on directory segments only, not the filename The dotted-component match in _is_test_path also examined the FILE name (the last path segment), so a product finding in a file literally named 'Test.cs' / 'Doc.cs' / 'Foo.Tests.cs' was dropped from the product diff (under-counting real findings). Iterate directory segments only (segs[:-1]); the file's own name no longer classifies it as non-product. Test-*directories* (Newtonsoft.Json.Tests, src/Test/) still drop. Adds selftest negatives for the marker-named product files. (Codex P2 on #147.) Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_011YMngnax1jsy378fSAZb9r --- scripts/oracle_compare.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/scripts/oracle_compare.py b/scripts/oracle_compare.py index 672bb9e9..32138354 100644 --- a/scripts/oracle_compare.py +++ b/scripts/oracle_compare.py @@ -378,8 +378,13 @@ def _is_test_path(path: str) -> bool: (e.g. Polly's `src/Snippets/Docs/*`, where ~20 `HttpResponseMessage`/`HttpClient` examples are never disposed by design) — counting them as product leaks inflates the oracle-only recall gap with example code that was never meant to dispose.""" - for seg in path.lower().split("/"): - # Split each segment on '.' before matching, for the ubiquitous .NET + # Only DIRECTORY segments classify a finding as non-product — NEVER the file's own + # name (the last segment). A product file literally named `Test.cs` / `Doc.cs` / + # `Foo.Tests.cs` is real product code and must NOT be dropped from the diff, or we + # under-count findings (Codex). So iterate the directory segments only (`segs[:-1]`). + segs = path.lower().split("/") + for seg in segs[:-1]: + # Split each directory segment on '.' before matching, for the ubiquitous .NET # `.Tests` / `Foo.Benchmarks` directory convention: the dir # `Newtonsoft.Json.Tests` is ONE path segment, so a bare-segment check # misses it (and silently drags ~485 test findings into the diff). Matching @@ -576,10 +581,13 @@ def _selftest() -> int: fails.append("_is_test_path should match dotted .NET test/benchmark project dirs") # ...but a product dir whose name merely *starts with* a marker word is NOT # excluded — exact match for snippet/doc guards against dropping real code, and - # the dot-split must leave a non-test dotted product namespace untouched. + # the dot-split must leave a non-test dotted product namespace untouched. Critically, + # the dot-split must NOT look at the FILE name: a product file literally named + # `Test.cs` / `Doc.cs` / `Foo.Tests.cs` is real code, not a test tree (Codex). if any(_is_test_path(p) for p in ("Dapper/SqlMapper.cs", "src/Lib/A.cs", "src/SnippetEngine/Foo.cs", "src/Documentation/Api.cs", - "Src/Newtonsoft.Json/JsonSerializer.cs")): + "Src/Newtonsoft.Json/JsonSerializer.cs", + "src/Test.cs", "src/Doc.cs", "src/Foo.Tests.cs")): fails.append("_is_test_path should not match product paths") # the scope note is gated on mode, not count: a product-only run that excluded # nothing must still say so (else it reads like a full-scope run).