diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 670df2a3..1875e068 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -135,6 +135,7 @@ jobs: frontend/roslyn/samples/WhenAnyValueViewModel.cs \ frontend/roslyn/samples/DiCaptiveSample.cs \ frontend/roslyn/samples/SampleTypes.cs \ + frontend/roslyn/samples/PipeFieldsSample.cs \ -o "$RUNNER_TEMP/facts.json" cat "$RUNNER_TEMP/facts.json" - name: Check facts through the core @@ -154,6 +155,13 @@ jobs: if echo "$out" | grep -q "OrdersViewModel.cs"; then echo "FAIL: disposed subscription wrongly reported"; exit 1 fi + # Mined FP regression (Pipelines.Sockets.Unofficial): System.IO.Pipelines PipeReader/PipeWriter + # END WITH Reader/Writer but are NOT IDisposable (they finish via Complete(), not Dispose()), so + # an undisposed PipeReader/PipeWriter FIELD must NOT be flagged as a leak — + # IsNonDisposableReaderWriter excludes them from the field-disposable name heuristic. + if echo "$out" | grep -q "PipeFieldsSample.cs"; then + echo "FAIL: PipeReader/PipeWriter field wrongly reported as an undisposed-disposable leak"; exit 1 + fi # a lambda handler has no stored delegate, so it can NEVER be `-=`'d — the # finding says so. (Same injected source as Customer -> also a warning.) echo "$out" | grep -qE "LambdaHandlerViewModel\.cs:[0-9]+: warning: \[OWN001\]" \ diff --git a/frontend/roslyn/OwnSharp.Extractor/Program.cs b/frontend/roslyn/OwnSharp.Extractor/Program.cs index 44a48903..c8b9a307 100644 --- a/frontend/roslyn/OwnSharp.Extractor/Program.cs +++ b/frontend/roslyn/OwnSharp.Extractor/Program.cs @@ -1412,8 +1412,19 @@ static bool DisposesLocal(SyntaxNode body, string name) static bool IsDisposableType(string t) => t is "IDisposable" or "IAsyncDisposable" or "CancellationTokenSource" or "HttpClient" or "SerialPort" or "SqlConnection" - || t.EndsWith("Stream") || t.EndsWith("Reader") || t.EndsWith("Writer") - || t.EndsWith("Subscription"); + || ((t.EndsWith("Stream") || t.EndsWith("Reader") || t.EndsWith("Writer") + || t.EndsWith("Subscription")) + && !IsNonDisposableReaderWriter(t)); + +// BCL `…Reader`/`…Writer` types that the EndsWith name heuristic above matches but that are NOT +// IDisposable: System.IO.Pipelines `PipeReader`/`PipeWriter` finish via `Complete()`, not `Dispose()`. +// Excluding them stops the field-disposable detector flagging an undisposed PipeReader/PipeWriter +// field as a leak — a FALSE POSITIVE mined on Pipelines.Sockets.Unofficial (SocketConnection's +// `_input`/`_output`). Matched on the exact bare or `System.IO.Pipelines`-qualified spelling — NOT +// any simple-name match, so a project's own disposable `MyLib.PipeReader` is still flagged (Codex). +static bool IsNonDisposableReaderWriter(string t) => + t is "PipeReader" or "PipeWriter" + or "System.IO.Pipelines.PipeReader" or "System.IO.Pipelines.PipeWriter"; // --- P-006: DI registration + constructor graph (DI001 captive dependency) --- // A syntactic pass over the same trees, independent of the event/disposable diff --git a/frontend/roslyn/samples/PipeFieldsSample.cs b/frontend/roslyn/samples/PipeFieldsSample.cs new file mode 100644 index 00000000..0337264b --- /dev/null +++ b/frontend/roslyn/samples/PipeFieldsSample.cs @@ -0,0 +1,47 @@ +using System; +using System.IO.Pipelines; +using System.Threading; +using System.Threading.Tasks; + +// Mined false-positive regression guard (Pipelines.Sockets.Unofficial). `System.IO.Pipelines` +// PipeReader/PipeWriter END WITH "Reader"/"Writer", so the field-disposable NAME heuristic used to +// classify them as IDisposable and flag an undisposed PipeReader/PipeWriter FIELD as a leak. But they +// are NOT IDisposable — they finish via Complete(), not Dispose(). A class that constructs such fields +// and never disposes them must produce NO finding (mirrors SocketConnection._input/_output, which the +// checker wrongly reported before IsNonDisposableReaderWriter excluded these types). +public sealed class PipeHolder +{ + private readonly PipeReader _input; + private readonly PipeWriter _output; + + public PipeHolder() + { + _input = new NullReader(); // constructed (a `new`) -> a disposable-field candidate before the fix + _output = new NullWriter(); + } + + // No Dispose: PipeReader/PipeWriter are completed by the consumer, not disposed -> not a leak. +} + +// Minimal PipeReader/PipeWriter implementations so the sample is self-contained (the abstract BCL +// types resolve from the framework reference set). PipeReader/PipeWriter expose Complete(), not +// Dispose() — they are not IDisposable. +internal sealed class NullReader : PipeReader +{ + public override void AdvanceTo(SequencePosition consumed) => throw new NotImplementedException(); + public override void AdvanceTo(SequencePosition consumed, SequencePosition examined) => throw new NotImplementedException(); + public override void CancelPendingRead() => throw new NotImplementedException(); + public override void Complete(Exception exception = null) => throw new NotImplementedException(); + public override ValueTask ReadAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public override bool TryRead(out ReadResult result) => throw new NotImplementedException(); +} + +internal sealed class NullWriter : PipeWriter +{ + public override void Advance(int bytes) => throw new NotImplementedException(); + public override void CancelPendingFlush() => throw new NotImplementedException(); + public override void Complete(Exception exception = null) => throw new NotImplementedException(); + public override ValueTask FlushAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public override Memory GetMemory(int sizeHint = 0) => throw new NotImplementedException(); + public override Span GetSpan(int sizeHint = 0) => throw new NotImplementedException(); +}