diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4f8144f..7a236668 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -625,10 +625,12 @@ jobs: - name: Score the corpus on real C# # Precision is gated absolutely (every fix silent, zero false positives); # recall is pinned at the measured floor and ratchets up as the extractor - # improves. Now 6/9 — pooled buffers are routed through the path-sensitive - # flow engine (Rent = acquire, Return = release), so double-return (OWN003) - # and use-after-return (OWN002) join the already-caught subscription/region - # class. Remaining backlog: interprocedural handoff, cross-method - # use-after-dispose, a region-escape shape. A drop below the floor is a regression. - run: python scripts/benchmark.py --min-recall 6 + # improves. Now 7/10 — pooled buffers ride the path-sensitive flow engine + # (Rent = acquire, Return = release: OWN003/OWN002), and pool recognition is + # now resolved through the Roslyn SemanticModel, so a double-return reached via + # an ALIASED pool receiver (`var p = ArrayPool.Shared; p.Return(buf)`) — a + # miss for the old text heuristic — is caught. Remaining backlog: interprocedural + # handoff, cross-method use-after-dispose, a region-escape shape. A drop below + # the floor is a regression. + run: python scripts/benchmark.py --min-recall 7 diff --git a/corpus/real-world/arraypool-aliased-receiver/after.cs b/corpus/real-world/arraypool-aliased-receiver/after.cs new file mode 100644 index 00000000..d36d794e --- /dev/null +++ b/corpus/real-world/arraypool-aliased-receiver/after.cs @@ -0,0 +1,22 @@ +// AFTER (fixed): return exactly once, in finally — through the same aliased receiver. +// (Wrapped in a class so the extractor's per-class flow pass visits it; Work stubbed.) +using System.Buffers; + +static class PoolAliasedReceiver +{ + static void Use(int n) + { + ArrayPool p = ArrayPool.Shared; + int[] rented = p.Rent(n); + try + { + Work(rented); + } + finally + { + p.Return(rented); // returned exactly once + } + } + + static void Work(int[] buffer) { } +} diff --git a/corpus/real-world/arraypool-aliased-receiver/before.cs b/corpus/real-world/arraypool-aliased-receiver/before.cs new file mode 100644 index 00000000..3ee0cbfd --- /dev/null +++ b/corpus/real-world/arraypool-aliased-receiver/before.cs @@ -0,0 +1,34 @@ +// BEFORE (buggy). The same double-return bug as arraypool-double-return, but the pool +// is reached through an ALIASED receiver: `ArrayPool p = ArrayPool.Shared;` +// then `p.Rent(...)` / `p.Return(...)`. Caching the pool in a local (or a field) rather +// than repeating `ArrayPool.Shared` at every call site is common real C#. +// +// This case is the proof for semantic-model pool recognition. A purely TEXTUAL detector +// keyed on the receiver spelling ("Pool"/"pool") cannot see `p.Rent` — the receiver `p` +// carries no "Pool" — so the buffer was invisible and this double-return was MISSED. +// Binding the call to System.Buffers.ArrayPool via the Roslyn SemanticModel resolves +// the pool no matter how the receiver is spelled; the path-sensitive flow engine then +// flags the double release (OWN003). +// +// Wrapped in a class so the extractor's per-class flow pass visits it; Work stubbed. +using System.Buffers; + +static class PoolAliasedReceiver +{ + static void Use(int n) + { + ArrayPool p = ArrayPool.Shared; // the pool, via an aliased receiver + int[] rented = p.Rent(n); + try + { + Work(rented); + p.Return(rented); // returned here ... + } + finally + { + p.Return(rented); // <-- ... and again here (double) -> OWN003 + } + } + + static void Work(int[] buffer) { } +} diff --git a/corpus/real-world/arraypool-aliased-receiver/case.own b/corpus/real-world/arraypool-aliased-receiver/case.own new file mode 100644 index 00000000..28b0bef8 --- /dev/null +++ b/corpus/real-world/arraypool-aliased-receiver/case.own @@ -0,0 +1,17 @@ +// OwnLang model: the same buffer returned twice is a double `release` -> OWN003. The C# +// reduction reaches the pool through an aliased receiver (`ArrayPool p = ArrayPool< +// int>.Shared`), which the extractor binds via the SemanticModel; the ownership logic is +// identical to arraypool-double-return. +module Corpus +resource Buffer { + acquire rent + release give + emit_type "int[]" + emit_acquire "p.Rent({args})" + emit_release "p.Return({0})" +} +fn run(n: int) { + let rented = acquire Buffer(n); // p.Rent + release rented; // p.Return + release rented; // returned again -> OWN003 +} diff --git a/corpus/real-world/arraypool-aliased-receiver/expected-diagnostics.txt b/corpus/real-world/arraypool-aliased-receiver/expected-diagnostics.txt new file mode 100644 index 00000000..574370e9 --- /dev/null +++ b/corpus/real-world/arraypool-aliased-receiver/expected-diagnostics.txt @@ -0,0 +1 @@ +OWN003 diff --git a/corpus/real-world/arraypool-aliased-receiver/notes.md b/corpus/real-world/arraypool-aliased-receiver/notes.md new file mode 100644 index 00000000..a8b679cb --- /dev/null +++ b/corpus/real-world/arraypool-aliased-receiver/notes.md @@ -0,0 +1,24 @@ +# ArrayPool double-return through an aliased receiver + +**Pattern:** the same rented array is returned to the pool twice (a `Return` on the +normal path plus another in `finally`), exactly like `arraypool-double-return` — but the +pool is reached through an **aliased receiver**: `ArrayPool p = ArrayPool.Shared;` +then `p.Rent(...)` / `p.Return(...)`. Caching the pool in a local or field is common real +C#; the bug is the double `Return`, which corrupts the pool (the array can be handed to +two renters at once). See dotnet/runtime#33767. + +**What the checker says:** the OwnLang model trips **OWN003** (double release). + +**Why this case exists (the semantic-model proof).** A purely *textual* pool detector +keyed on the receiver spelling (`Contains("Pool")`) cannot recognise `p.Rent`/`p.Return` — +the receiver `p` carries no "Pool" — so this buffer was invisible and the double-return was +**MISSED**. Binding the call to `System.Buffers.ArrayPool` via the Roslyn SemanticModel +resolves the pool regardless of how the receiver is spelled, and the path-sensitive flow +engine then flags the double release. This fixture both proves and pins that upgrade — it +is a miss under the old text heuristic and a catch (OWN003) under the semantic one. + +**Honesty / scope.** As with the other cases, `case.own` is a faithful hand reduction of +the pattern (not C# ingested by the checker); `before.cs` / `after.cs` are representative +of the bug and its fix, not a verbatim PR diff. + +Reference: https://github.com/dotnet/runtime/issues/33767 diff --git a/docs/notes/corpus-benchmark.md b/docs/notes/corpus-benchmark.md index e9b71f54..db6a353a 100644 --- a/docs/notes/corpus-benchmark.md +++ b/docs/notes/corpus-benchmark.md @@ -26,7 +26,7 @@ correct code), and **recall was 3/9** — the three caught are exactly the subscription/region class the extractor is strongest at (`zombie-viewmodel` → OWN001, two static-event escapes → OWN014). -## Ratchet → 6/9 (two ratchets) +## Ratchet → 7/10 (three ratchets) ### → 4/9: a fixture was understating us @@ -59,6 +59,22 @@ a *use*. The core's CFG analysis then flags the double-release and the use-after ArrayPool convention is the renter returns), and the syntactic `POOL001` is suppressed under `--flow-locals` so there is no double-report. **Recall is now 6/9.** +### → 7/10: pool recognition goes semantic + +The pool pass recognised a `Rent`/`Return` by the **text** of the receiver — `Contains("Pool")` — +which is structurally blind to an *aliased* receiver: `ArrayPool p = ArrayPool.Shared; +p.Rent(...); p.Return(buf);` spells the receiver `p`, with no "Pool" in it, so the buffer was +invisible and a double-return through it was a **miss** (the symmetric hazard is a false match on +an unrelated `_connectionPool.Rent()`). Both are gone now that `IsPoolRent` / `PoolReturnBuffer` +bind the call through the **Roslyn SemanticModel** to `System.Buffers.ArrayPool` — the receiver +may be spelled any way. That one definition is shared by the syntactic `POOL001` pass and the flow +engine (no divergence between the two), and it is ArrayPool-specific on purpose: `MemoryPool.Rent` +hands back an `IMemoryOwner` released by `Dispose`, so there is no `Return` to model. A new +fixture — `arraypool-aliased-receiver`, a double-return reached through `var p = ArrayPool.Shared` +— is a *miss* under the old text heuristic and a *catch* (`OWN003`) under the semantic one. The +heuristic's failure mode was recall-leaning (a missed alias is a missed catch, never a false alarm), +so the upgrade only adds — precision stays absolute. **Recall is now 7/10.** + The remaining three misses are genuine **frontend extraction gaps** — the interprocedural ownership-handoff (`OWN001`+`OWN002`), a field/cross-method use-after-dispose, and a region-escape shape — the `.own` reductions all catch them, the C# extractor does not yet. diff --git a/docs/proposals/P-012-bug-corpus-mining.md b/docs/proposals/P-012-bug-corpus-mining.md index 3ac50d90..83f33a6b 100644 --- a/docs/proposals/P-012-bug-corpus-mining.md +++ b/docs/proposals/P-012-bug-corpus-mining.md @@ -6,12 +6,16 @@ (the bug is caught) and specificity (the fix is silent), gated in the `corpus-benchmark` CI job. This is the measurement spine — the defensible number, and the verifiable reward for any future learning loop. First measurement **3/9 - caught · 9/9 clean · 0 FP**, ratcheted to **6/9** over two steps: (1) a *fixture* + caught · 9/9 clean · 0 FP**, ratcheted to **7/10** over three steps: (1) a *fixture* was understating us — `screentogif-loaded-subscription` referenced an undeclared VM type → `OWN050`, fixed by making it self-contained; (2) a real *capability* — pooled buffers are now routed through the path-sensitive flow engine (Rent = acquire, Return = release), so double-return (`OWN003`) and use-after-return - (`OWN002`) are caught *soundly* (not "count Returns", which FPs). Perfect precision + (`OWN002`) are caught *soundly* (not "count Returns", which FPs); (3) pool + recognition moved off the receiver-text heuristic onto the **Roslyn SemanticModel** + (binding `System.Buffers.ArrayPool`), so a double-return through an *aliased* pool + receiver (`var p = ArrayPool.Shared; p.Return(buf)`) — a miss for the text + heuristic — is caught (`arraypool-aliased-receiver`, +1 row). Perfect precision throughout. The remaining 3 misses are genuine frontend extraction gaps (interprocedural handoff, a cross-method use-after-dispose, a region-escape shape) — the tracked recall backlog the floor ratchets up to. Still ahead: those, GitHub diff --git a/frontend/roslyn/OwnSharp.Extractor/Program.cs b/frontend/roslyn/OwnSharp.Extractor/Program.cs index e672de0d..45671bb9 100644 --- a/frontend/roslyn/OwnSharp.Extractor/Program.cs +++ b/frontend/roslyn/OwnSharp.Extractor/Program.cs @@ -408,11 +408,11 @@ cc.Filter is null // `tracked` local IDisposables. Returns null on any UNMODELLED statement (a `goto`, labeled // statement, local function, `lock`/`fixed`, …): the method is then honestly skipped, not // guessed. Loops, `try`, `do` and `switch` ARE modelled below. -static List? LowerFlowBody(BlockSyntax block, HashSet tracked) +static List? LowerFlowBody(BlockSyntax block, HashSet tracked, SemanticModel model) { var nodes = new List(); foreach (var st in block.Statements) - if (!LowerFlowStmt(st, tracked, nodes)) + if (!LowerFlowStmt(st, tracked, model, nodes)) return null; return nodes; } @@ -424,7 +424,7 @@ cc.Filter is null // a `return` here runs FIRST — the enclosing `finally`(s), then the exit — so a resource a // finally disposes is released on the return path; null = a bare return (outside any try). // Defaults are the method-body context: throws escape, nothing is injected, returns are bare. -static bool LowerFlowStmt(StatementSyntax st, HashSet tracked, List nodes, +static bool LowerFlowStmt(StatementSyntax st, HashSet tracked, SemanticModel model, List nodes, bool canEscape = true, List? onThrow = null, List? onReturn = null) { @@ -432,7 +432,7 @@ static bool LowerFlowStmt(StatementSyntax st, HashSet tracked, List tracked, List Rent nodes.Add(new { op = "acquire", var = v.Identifier.Text, line = LineOf(v) }); return true; case ExpressionStatementSyntax es: InjectThrowEdge(es, nodes, onThrow); - EmitFlowExpr(es.Expression, tracked, nodes); + EmitFlowExpr(es.Expression, tracked, model, nodes); return true; case IfStatementSyntax ifs: { var thenNodes = new List(); - if (!LowerFlowStmt(ifs.Statement, tracked, thenNodes, canEscape, onThrow, onReturn)) + if (!LowerFlowStmt(ifs.Statement, tracked, model, thenNodes, canEscape, onThrow, onReturn)) return false; var elseNodes = new List(); - if (ifs.Else is { } e && !LowerFlowStmt(e.Statement, tracked, elseNodes, canEscape, onThrow, onReturn)) + if (ifs.Else is { } e && !LowerFlowStmt(e.Statement, tracked, model, elseNodes, canEscape, onThrow, onReturn)) return false; nodes.Add(new { op = "if", line = LineOf(ifs), then = thenNodes, @else = elseNodes }); return true; @@ -463,7 +463,7 @@ or ImplicitObjectCreationExpressionSyntax case UsingStatementSyntax us: // using(...) {body}: the using local is auto-disposed (untracked); still // lower the body so a tracked plain local used inside is seen. - return us.Statement is null || LowerFlowStmt(us.Statement, tracked, nodes, canEscape, onThrow, onReturn); + return us.Statement is null || LowerFlowStmt(us.Statement, tracked, model, nodes, canEscape, onThrow, onReturn); case ReturnStatementSyntax rs: // A tracked local READ in the return value is a use at the return point — // e.g. `return BuildResult(buf)` after `pool.Return(buf)` is a use-after- @@ -473,7 +473,7 @@ or ImplicitObjectCreationExpressionSyntax // — threaded as `onReturn`, so a resource the finally releases is released // on the return path — then exits; outside a try it is a bare CFG exit. if (rs.Expression is { } rexpr) - EmitFlowExpr(rexpr, tracked, nodes); + EmitFlowExpr(rexpr, tracked, model, nodes); if (onReturn is not null) nodes.AddRange(onReturn); else @@ -487,7 +487,7 @@ or ImplicitObjectCreationExpressionSyntax // double-release). The condition is opaque (we model control flow, not // values). If the body has an unmodelled statement, bail the method. var bodyNodes = new List(); - if (ws.Statement is null || !LowerFlowStmt(ws.Statement, tracked, bodyNodes, canEscape, onThrow, onReturn)) + if (ws.Statement is null || !LowerFlowStmt(ws.Statement, tracked, model, bodyNodes, canEscape, onThrow, onReturn)) return false; nodes.Add(new { op = "while", line = LineOf(ws), body = bodyNodes }); return true; @@ -500,7 +500,7 @@ or ImplicitObjectCreationExpressionSyntax // body as a `while` is sound. (`for` and `do` are handled below; `do` runs 1+ // times, so it is desugared rather than modelled as a bare 0+-trip `while`.) var bodyNodes = new List(); - if (fes.Statement is null || !LowerFlowStmt(fes.Statement, tracked, bodyNodes, canEscape, onThrow, onReturn)) + if (fes.Statement is null || !LowerFlowStmt(fes.Statement, tracked, model, bodyNodes, canEscape, onThrow, onReturn)) return false; nodes.Add(new { op = "while", line = LineOf(fes), body = bodyNodes }); return true; @@ -514,7 +514,7 @@ or ImplicitObjectCreationExpressionSyntax // method-body local, so it is never a tracked candidate — no soundness // concern, just a separate (rare) recall gap. var bodyNodes = new List(); - if (fors.Statement is null || !LowerFlowStmt(fors.Statement, tracked, bodyNodes, canEscape, onThrow, onReturn)) + if (fors.Statement is null || !LowerFlowStmt(fors.Statement, tracked, model, bodyNodes, canEscape, onThrow, onReturn)) return false; nodes.Add(new { op = "while", line = LineOf(fors), body = bodyNodes }); return true; @@ -537,7 +537,7 @@ or ImplicitObjectCreationExpressionSyntax if (cc.Block.DescendantNodes().OfType().Any(IsDisposeShaped)) return false; var finallyNodes = new List(); - if (trys.Finally is { } fin && !LowerFlowStmt(fin.Block, tracked, finallyNodes)) + if (trys.Finally is { } fin && !LowerFlowStmt(fin.Block, tracked, model, finallyNodes)) return false; // Does a throw in THIS body escape to method exit (so the edge — leave running // only the finally — models a real execution)? Sound when there is no catch (it @@ -573,7 +573,7 @@ or ImplicitObjectCreationExpressionSyntax bodyOnReturn.AddRange(onReturn ?? new List { new { op = "return", var = (string?)null, line = LineOf(trys) } }); foreach (var stmt in trys.Block.Statements) - if (!LowerFlowStmt(stmt, tracked, nodes, bodyCanEscape, bodyOnThrow, bodyOnReturn)) + if (!LowerFlowStmt(stmt, tracked, model, nodes, bodyCanEscape, bodyOnThrow, bodyOnReturn)) return false; nodes.AddRange(finallyNodes); // normal completion runs the finally return true; @@ -586,10 +586,10 @@ or ImplicitObjectCreationExpressionSyntax // only in the body but acquired before the loop would falsely leak on the phantom // 0-trip path. Bail (like the loops) if the body has an unmodelled statement. if (dos.Statement is null - || !LowerFlowStmt(dos.Statement, tracked, nodes, canEscape, onThrow, onReturn)) + || !LowerFlowStmt(dos.Statement, tracked, model, nodes, canEscape, onThrow, onReturn)) return false; var bodyNodes = new List(); - if (!LowerFlowStmt(dos.Statement, tracked, bodyNodes, canEscape, onThrow, onReturn)) + if (!LowerFlowStmt(dos.Statement, tracked, model, bodyNodes, canEscape, onThrow, onReturn)) return false; nodes.Add(new { op = "while", line = LineOf(dos), body = bodyNodes }); return true; @@ -606,7 +606,7 @@ or ImplicitObjectCreationExpressionSyntax foreach (var section in sw.Sections) { var secNodes = new List(); - if (!LowerSwitchSection(section, tracked, secNodes, canEscape, onThrow, onReturn)) + if (!LowerSwitchSection(section, tracked, model, secNodes, canEscape, onThrow, onReturn)) return false; if (section.Labels.Any(l => l is DefaultSwitchLabelSyntax)) defaultNodes = secNodes; @@ -645,20 +645,20 @@ or ImplicitObjectCreationExpressionSyntax // flow model doesn't handle here — including a `break` nested inside an `if`/loop, which reaches // the unmodelled default and conservatively skips the whole method. static bool LowerSwitchSection(SwitchSectionSyntax section, HashSet tracked, - List nodes, bool canEscape, + SemanticModel model, List nodes, bool canEscape, List? onThrow, List? onReturn) { foreach (var stmt in section.Statements) { if (stmt is BreakStatementSyntax) break; - if (!LowerFlowStmt(stmt, tracked, nodes, canEscape, onThrow, onReturn)) + if (!LowerFlowStmt(stmt, tracked, model, nodes, canEscape, onThrow, onReturn)) return false; } return true; } -static void EmitFlowExpr(ExpressionSyntax expr, HashSet tracked, List nodes) +static void EmitFlowExpr(ExpressionSyntax expr, HashSet tracked, SemanticModel model, List nodes) { // `await x.DisposeAsync()` is the IAsyncDisposable release — look through the // await to the inner call so it counts as disposal, not a bare use. @@ -698,7 +698,7 @@ static void EmitFlowExpr(ExpressionSyntax expr, HashSet tracked, List release. The buffer is the // ARGUMENT (the pool is the receiver), unlike Dispose where the local is the // receiver; `return` early so the argument is not also counted as a use. - if (PoolReturnBuffer(expr) is { } pbuf && tracked.Contains(pbuf)) + if (PoolReturnBuffer(expr, model) is { } pbuf && tracked.Contains(pbuf)) { nodes.Add(new { op = "release", var = pbuf, line = LineOf(expr) }); return; @@ -720,22 +720,40 @@ static void EmitFlowExpr(ExpressionSyntax expr, HashSet tracked, List null, }; -// An ArrayPool/MemoryPool `Rent(...)` call — the acquire of a pooled buffer. The -// receiver carries "Pool" (`ArrayPool.Shared`, `MemoryPool.Shared`, `_pool`). -static bool IsPoolRent(ExpressionSyntax? e) => +// Is `t` the System.Buffers.ArrayPool type — the Return-based pool we model? +// Checked on the resolved SYMBOL, not the receiver's text, so an aliased receiver +// (`ArrayPool p = ArrayPool.Shared; p.Rent(n)`) binds correctly and an +// unrelated API with "pool" in its name does not false-match. ArrayPool-specific by +// design: MemoryPool.Rent hands back an IMemoryOwner released by Dispose (there +// is no MemoryPool.Return), so a pooled MemoryPool owner rides the IDisposable path, +// not this Return-based one. +static bool IsArrayPoolType(INamedTypeSymbol? t) +{ + if (t is null || t.Name != "ArrayPool") + return false; + INamespaceSymbol? ns = t.ContainingNamespace; + if (ns is null || ns.Name != "Buffers") + return false; + ns = ns.ContainingNamespace; // System.Buffers -> System + return ns is not null && ns.Name == "System" + && ns.ContainingNamespace is { IsGlobalNamespace: true }; +} + +// An ArrayPool `Rent(...)` call — the acquire of a pooled buffer. Resolved via the +// SemanticModel (`model`), so the receiver may be any expression of ArrayPool type. +static bool IsPoolRent(ExpressionSyntax? e, SemanticModel model) => e is InvocationExpressionSyntax i - && i.Expression is MemberAccessExpressionSyntax m - && m.Name.Identifier.Text == "Rent" - && (m.Expression.ToString().Contains("Pool") || m.Expression.ToString().Contains("pool")); - -// An ArrayPool/MemoryPool `Return(buf)` call — the RELEASE of the pooled buffer -// `buf`. Unlike Dispose (where the tracked local is the receiver), the buffer is -// the first ARGUMENT and the pool is the receiver. Returns the buffer name or null. -static string? PoolReturnBuffer(ExpressionSyntax e) => + && model.GetSymbolInfo(i).Symbol is IMethodSymbol { Name: "Rent" } sym + && IsArrayPoolType(sym.ContainingType); + +// An ArrayPool `Return(buf)` call — the RELEASE of the pooled buffer `buf`. Unlike +// Dispose (where the tracked local is the receiver), the buffer is the first ARGUMENT +// and the pool is the receiver. Resolved via the SemanticModel. Returns the buffer +// name (a plain local) or null. +static string? PoolReturnBuffer(ExpressionSyntax e, SemanticModel model) => e is InvocationExpressionSyntax i - && i.Expression is MemberAccessExpressionSyntax m - && m.Name.Identifier.Text == "Return" - && (m.Expression.ToString().Contains("Pool") || m.Expression.ToString().Contains("pool")) + && model.GetSymbolInfo(i).Symbol is IMethodSymbol { Name: "Return" } sym + && IsArrayPoolType(sym.ContainingType) && i.ArgumentList.Arguments.Count > 0 && i.ArgumentList.Arguments[0].Expression is IdentifierNameSyntax buf ? buf.Identifier.Text : null; @@ -1152,8 +1170,11 @@ or ImplicitObjectCreationExpressionSyntax source = IsSelfRootedWhenAny(m.Expression) ? "self" : null, }); - // POOL001: an ArrayPool/MemoryPool buffer `Rent`ed but never `Return`ed, - // matched per member so a `buf` returned in one method does not mask a + // POOL001: an ArrayPool buffer `Rent`ed but never `Return`ed, the Rent + // recognised via the shared semantic `IsPoolRent` (so an aliased pool receiver + // binds and a non-pool `.Rent` does not false-match — one definition, the flow + // pass below is the other consumer). Matched per member so a `buf` returned in + // one method does not mask a // leak of a same-named `buf` in another. Under --flow-locals the // path-sensitive flow detector supersedes this for buffers held in LOCALS // (and additionally catches double-return / use-after-return) — but it only @@ -1164,10 +1185,7 @@ or ImplicitObjectCreationExpressionSyntax { var rented = new List<(string Name, int Line)>(); foreach (var inv in member.DescendantNodes().OfType()) - if (inv.Expression is MemberAccessExpressionSyntax m - && m.Name.Identifier.Text == "Rent" - && (m.Expression.ToString().Contains("Pool") - || m.Expression.ToString().Contains("pool"))) + if (IsPoolRent(inv, model)) { string? name = inv.Parent switch { @@ -1267,7 +1285,7 @@ or ImplicitObjectCreationExpressionSyntax if (method.Body is not { } mbody) continue; var candidates = new HashSet(); - var poolBuffers = new HashSet(); // candidates that are ArrayPool/MemoryPool buffers + var poolBuffers = new HashSet(); // candidates that are ArrayPool buffers foreach (var ld in mbody.DescendantNodes().OfType()) { if (ld.UsingKeyword != default) @@ -1278,7 +1296,7 @@ or ImplicitObjectCreationExpressionSyntax } init && model.GetTypeInfo(init.Value).Type is { } dt && ImplementsIDisposable(dt) && !IsDisposeOptional(dt)) candidates.Add(v.Identifier.Text); - else if (IsPoolRent(v.Initializer?.Value)) // an ArrayPool/MemoryPool buffer + else if (IsPoolRent(v.Initializer?.Value, model)) // an ArrayPool buffer { candidates.Add(v.Identifier.Text); poolBuffers.Add(v.Identifier.Text); @@ -1308,7 +1326,7 @@ or ImplicitObjectCreationExpressionSyntax } init if (tracked.Count == 0) continue; statMethodsWithLocal++; - var fbody = LowerFlowBody(mbody, tracked); + var fbody = LowerFlowBody(mbody, tracked, model); if (fbody is null || fbody.Count == 0) { statMethodsSkipped++; // unmodelled construct -> honestly skipped