Part of #2874 - #2938
Part of #2874#2938
Conversation
| - **Lite's portable ZIP is self-contained, which HALVED it** ([#2501]) - `Publish Lite` is now `-r win-x64 --self-contained` in both `build.yml` and `nightly.yml`, so neither Lite artifact has a .NET prerequisite any more and the failure [#2489] documented stops existing: a tester who unzips onto a stock Windows Server no longer meets the .NET host's bare `You must install .NET to run this application` before a line of our code runs. **The size went the opposite way from what bundling a runtime suggests.** The old publish was RID-agnostic, so it copied every platform its packages ship - **537 MB of `runtimes\` on a 565 MB tree** (osx 130, linux-x64 116, linux-arm64 70, win-arm64 56, then win-x86, musl, loongarch64 and riscv64), of which only the **52 MB `win-x64`** folder could ever load on Windows. `DuckDB.NET.Bindings.Full` is most of it, SkiaSharp and SqlClient behind it. Dropping ~485 MB of unloadable native payload beats the cost of bundling .NET, WPF and ASP.NET Core by roughly two to one: measured on one commit and one SDK, **565 MB tree / 212.7 MB zipped becomes 277 MB / 114.2 MB**. It matters most for the **nightly** ZIP, which is the UAT download and is not offered as a `Setup.exe` at all. **A RID-specific publish needed two more files than the flag.** `Lite/packages.lock.json` had only a `net10.0-windows7.0` target, and a RID restore adds `net10.0-windows7.0/win-x64` to it - after which the `dotnet restore --locked-mode` that BOTH workflows run before the publish fails `NU1004: the project's runtime identifiers have changed`, because locked mode compares the PROJECT's RID set (empty) against the lock file's (win-x64). Reproduced locally; that is a red CI run on every PR, not the future `--no-restore` trap it was filed as. The fix is `<RuntimeIdentifiers>win-x64</RuntimeIdentifiers>` in `PerformanceMonitorLite.csproj`, so the project itself asks for that graph and one committed lock file satisfies the RID-less locked-mode restore and the RID publish alike; `RuntimeIdentifiers` (plural) sets no RID on the build, so a plain `dotnet build` stays RID-agnostic and `Lite.Tests` is untouched. **SignPath needed nothing** - the `Lite` artifact-configuration slug already receives both shapes today, and the signed re-zip reads `signed/Lite/*`, inheriting whatever shape `publish/Lite` has. Auto-update is unaffected; the ZIP is not a Velopack channel. `LiteRuntimePrerequisiteDocsTests` went red on the flag alone (3 of its 7 facts) and was rewritten to state every claim BOTH ways round: [#2499]'s version asserted only that the docs DID name the runtimes, so two of its facts stayed green while the prose went stale. It now also derives the lock file's RID coverage from the `-r` flags in the workflows, and every new assertion was proven red with its fix reverted. | ||
|
|
||
| ### Fixed | ||
| - **The timeout pins could not report an untimed command that had a deadline-shaped neighbour within two statements, and could report a comment as one** ([#2874]) - `CSharpSourceWalker.StatementSpanFrom` let its bracket depth go negative and keep counting, so a construction inside a `using (...) { }` whose body holds ONE statement spent the two-statement window on that statement and on the statement AFTER the block, and the FOLLOWING command's initializer satisfied the scan; the same leak ran out through the closing brace of any block a construction was the last statement of. The span now ends with the SCOPE it started in, and follows a statement header into the block that header governs - load-bearing in the other direction, because **fourteen** sites across the four scanned projects set the deadline as that block's first statement and a header-only span reports every one of them. Two further layouts no scope bound can reach - an untimed construction directly ahead of a timed one, and an untimed header whose block OPENS with a timed sibling - are closed by asking the question in two halves: a new `ConstructionSpanFrom` covers the site and its own initializer, and only a MEMBER assignment (`command.CommandTimeout = ...`, the one placement a `CreateCommand` result allows) is read from the surrounding statement span. Cutting the statement span at the next construction instead is the tempting one-liner and it is wrong: the conditional in `ViewerDataService.FinOps.Locking.cs` has two constructions sharing one deadline, a shape the viewer pin already pinned. The mirror-image defect goes with it - the `.Storage` and `.Viewer` construction scans read RAW source, so a construction named in a comment or a literal was reported as an untimed site at a line no edit could fix, and both now match stripped text and recognise the qualified `new Npgsql.NpgsqlCommand(` shape that no pattern in this sweep could see. **What this means for the figures already published: the merged groups' "zero untimed sites" claims were UNVERIFIED for these layouts rather than wrong.** Re-measuring all **1,848** command constructions in the repository with the corrected scan moves exactly **one** verdict - `DarlingRetention.PurgeOneAsync` lifts a TimescaleDB decompression rail through an untimed `using (...)` whose single body statement let the timed delete behind it stand in, a site outside every pin's reach that now carries `DeleteTimeoutSeconds` - and leaves the pinned scopes at 124/124, 193/193, 73/73 and 45/45 timed with nothing newly reported. |
There was a problem hiding this comment.
Count mismatch: this entry says the corrected scan re-measures 1,848 command constructions repository-wide, but the PR description's summary table says 1,849 (| whole repository | 1,849 | — | 1 (fixed here) | 81, none in scope |). The same "1,848" figure is also baked into a test comment in CSharpSourceWalkerTests.cs ("the only site in 1,848 across the repository..."). Given this PR's whole premise is that previously-published figures need to be exact and verified, it's worth reconciling which count is actually correct before merging — a one-off discrepancy here undercuts the "re-measured and verified" claim.
| return constructed < 0 ? manufactured | ||
| : manufactured < 0 ? constructed | ||
| : Math.Min(constructed, manufactured); | ||
| } |
There was a problem hiding this comment.
Nit: missing blank line between FirstConstruction's closing brace and the next member's XML doc comment (AStatementSpanCannotLeakPastACharLiteralQuote's preceding doc). Every other member in this file is separated by a blank line — this one pair reads as glued together.
Review summaryScope: this PR only touches Correctness — traced the new
One residual gap worth knowing about (not a blocker): Left two inline nits: a count discrepancy between this PR's CHANGELOG entry (1,848) and the PR description's summary table (1,849), and a minor missing-blank-line style nit in the new test file. No security, injection, or Lite/Darling parity concerns found. |
ecc157c to
b92f435
Compare
b92f435 to
7def267
Compare
7def267 to
b1c36fd
Compare
| /// <summary> | ||
| /// The other side of the same bound, and the reason it is the SCOPE rather than the header: fourteen | ||
| /// sites across the four projects these pins scan construct their command in a <c>using (...)</c> header | ||
| /// and set its deadline as the block's first statement. A span that stopped at the header would report | ||
| /// every one of them, and a false positive fails a green build on correct code. | ||
| /// </summary> | ||
| /// <summary> | ||
| /// The same bound for a header whose embedded statement carries NO braces. The brace stop cannot reach | ||
| /// this one — there is no block to end at — so the span ends with the embedded statement instead, and the | ||
| /// braced and braceless spellings of the same <c>using</c> are judged alike. The legacy walk is the | ||
| /// positive control: it runs on to the deadline set after the scope has closed. | ||
| /// </summary> | ||
| [Fact] | ||
| public void AStatementSpanEndsWithABracelessHeadersOwnStatement() |
There was a problem hiding this comment.
Doc-comment mix-up: this <summary> (407-412, "fourteen sites ... set its deadline as the block's first statement") describes AStatementSpanStillReachesADeadlineSetInsideTheBlockItsHeaderGoverns below (line 443), not AStatementSpanEndsWithABracelessHeadersOwnStatement (line 420), which already has its own correct summary right after (413-418). The result is two consecutive <summary> blocks stacked on one method — the first one orphaned (not attached to any member, likely a CS1587 warning) — while AStatementSpanStillReachesADeadlineSetInsideTheBlockItsHeaderGoverns ends up with no doc comment at all.
Given how much this PR (and this file in particular) leans on precise per-test doc comments to explain exactly what each fixture proves, this block should move down to sit directly above AStatementSpanStillReachesADeadlineSetInsideTheBlockItsHeaderGoverns.
|
Reviewed. This PR is scoped entirely to the Darling test-scanner tooling ( I traced the new One real finding, left as an inline comment: a doc-comment mix-up in |
b1c36fd to
cb40bea
Compare
|
Reviewed. This PR is Darling-only: a one-line production fix plus the test-infrastructure change that let it hide. Production fix (
Can't actually run Parity — no Lite counterpart needed here. Lite's retention ( Security/perf — no user input involved (these are build/test-time static scanners over the repo's own source), no injection surface, no perf regression (test-only code + a no-op-cost timeout on production No findings to post inline. |
cb40bea to
67c6a53
Compare
|
Reviewed. This is Darling-only (C#/Npgsql test tooling + a one-line production fix); no T-SQL and no Lite counterpart to check for parity — Lite never uses Npgsql, so there's no drift here. Production fix ( Scanner changes ( One theoretical (not live) blind spot worth noting for the backlog rather than blocking this PR: Also confirmed: the |
67c6a53 to
7597d61
Compare
| private static bool SetsAnExplicitDeadline(string code, int index) | ||
| => s_setsTimeout.IsMatch(CSharpSourceWalker.ConstructionSpanFrom(code, index)) | ||
| || s_assignsTimeout.IsMatch(CSharpSourceWalker.StatementSpanFrom(code, index, statements: 2)); |
There was a problem hiding this comment.
SetsAnExplicitDeadline is called uniformly for every match of s_commandCtor (line 135), including the third alternative \.CreateCommand\s*[,);] — the bare method-group hand-off, which by design has no argument list directly attached (e.g. builder.Register(factory.CreateCommand, options);).
ConstructionSpanFrom (CSharpSourceWalker.cs:253) assumes its start index is immediately followed (modulo whitespace/dots/identifiers) by the construction's own (. For a bare method-group match there is no such paren, so its unbounded forward scan —
while (open < text.Length && !(code[open] && text[open] == '(')) open++;— walks straight past the current statement into whatever code comes next, picks up the next unrelated construction's (...)/{...} (including its initializer), and hands that span to s_setsTimeout.IsMatch(...). If that neighboring construction happens to set CommandTimeout (very likely in these files, since ~all sites now do), the bare hand-off is reported as "timed" even though it is untimed — a false negative in exactly the class of blind spot this PR is otherwise closing.
Concretely, for
builder.Register(factory.CreateCommand, options);
var other = new NpgsqlCommand(Sql, connection) { CommandTimeout = 10 };ConstructionSpanFrom starting at the .CreateCommand, match walks forward, finds the ( of new NpgsqlCommand(Sql, connection) on the next line, and returns a span running through { CommandTimeout = 10 } — so SetsAnExplicitDeadline returns true for the handoff site, which has no timeout of its own.
Contrast with ViewerCommandTimeoutTests/AnalysisPassCommandTimeoutTests, which handle this same shape via a dedicated s_commandFactoryHandoff regex + a NoXCommand_IsCreatedByABareMethodGroupHandoff test that forbids the shape outright, rather than trying to validate a deadline on it. This file instead folds the shape straight into s_commandCtor and lets it flow through ConstructionSpanFrom/SetsAnExplicitDeadline, which isn't sound for it.
Not live today (no such hand-off currently exists in the alert-pass scope, per the comment at line 130), so it doesn't break CI now — but it's a silent hole: someone can add an untimed .CreateCommand method-group hand-off in DarlingWorker.cs's reachable set later and have this guard report it clean by accident, which is the exact failure mode (#2786-shaped) this PR's narrative is about eliminating. Worth either dropping the third alternative from this file's s_commandCtor in favor of a forbid-the-shape test like the other two files, or bounding ConstructionSpanFrom's forward scan to the enclosing statement/scope so it can't run past its start's own statement when no ( immediately follows.
Review summaryScope: this PR touches only Production fix (
One correctness finding (posted inline on No security, input-handling, or performance concerns — this is all static-analysis test tooling plus one timeout constant. |
7597d61 to
8404469
Compare
| /* A parenthesis or bracket that was open before `start` has closed, so `start` sat inside a | ||
| statement HEADER or an argument list. Neither ends the block, so the walk carries on at | ||
| depth zero; if a block follows, it is the header's and the span ends with it. */ | ||
| depth = 0; | ||
| header = true; | ||
|
|
||
| continue; |
There was a problem hiding this comment.
Correctness: the "header" fallback also fires for a construction nested inside an unrelated call's argument list, silently shrinking the two-statement window to one statement.
header = true is set here whenever any (/[ that was open before start closes — the comment even says "a statement HEADER or an argument list". But the code below treats every header == true the same way as a genuine using (...)/if (...) header: at the very next ; at the floor depth, if body < 0 it returns immediately (lines 213-221), ignoring the statements parameter entirely.
That's correct for a real statement header (the scope really does end there), but wrong when the "open bracket that closes" is just an ordinary function call wrapping the construction, e.g.:
var command = Configure(connection.CreateCommand());
command.CommandTimeout = 5;Trace: start is at the .CreateCommand( match, which sits inside Configure('s still-open (. Walking forward, CreateCommand()'s own parens balance to depth 0, then Configure(...)'s outer ) closes — this goes negative, so header = true is set (the "argument list" branch of the comment above, not a real header). The next ; is hit at depth == floor == 0 with body < 0, so the walk returns text[start..(i+1)] = ".CreateCommand());" only — the following command.CommandTimeout = 5; statement is excluded from the span.
Every one of these pins' SetsAnExplicitDeadline checks s_assignsTimeout against exactly this span (StatementSpanFrom(code, index, statements: 2)), so a command constructed as a bare argument to another call — a totally ordinary shape (Wrap(x.CreateCommand()), Foo(new NpgsqlCommand(...))) — with its deadline assigned on the following line would be reported as untimed, even though it's correctly timed. That's a false positive on correct code, exactly the failure mode this whole PR is built to eliminate (see the "worse than a miss" reasoning in the doc comments above).
I don't see this shape in the currently-scanned files (grepped for CreateCommand() nested in another call's parens — only the one genuine using (...) case in ViewerDataService.Blocking.cs), which is why the census stays green today. But it's untested and will misfire the moment someone writes a factory/wrapper that takes a freshly-created command as an argument.
Worth either: distinguishing a real statement-header close (only using/if/while/for/foreach/lock/switch/catch parens, which are always immediately followed by {, a braceless embedded statement, or nothing else on the line) from an ordinary argument-list close, or dropping the early-return-at-first-; and letting the normal seen >= statements counting run even when header is true but no body block ever appears.
ReviewScope: a one-line production fix ( The production fix itself is correct and minimal. I traced through the new No other correctness, security, or performance issues found. The regex widening (qualified |
| /* A parenthesis or bracket that was open before `start` has closed, so `start` sat inside a | ||
| statement HEADER or an argument list. Neither ends the block, so the walk carries on at | ||
| depth zero; if a block follows, it is the header's and the span ends with it. */ | ||
| depth = 0; | ||
| header = true; | ||
|
|
||
| continue; |
There was a problem hiding this comment.
Correctness: the "header" fallback also fires for a construction nested inside an unrelated call's argument list, silently shrinking the two-statement window to one statement.
header = true is set here whenever any (/[ that was open before start closes — the comment even says "a statement HEADER or an argument list". But the code below treats every header == true the same way as a genuine using (...)/if (...) header: at the very next ; at the floor depth, if body < 0 it returns immediately (line 213-221), ignoring the statements parameter entirely.
That's correct for a real statement header (the scope really does end there), but wrong when the "open bracket that closes" is just an ordinary function call wrapping the construction, e.g.:
var command = Configure(connection.CreateCommand());
command.CommandTimeout = 5;Trace: start is at the .CreateCommand( match, which sits inside Configure('s still-open (. Walking forward, CreateCommand()'s own parens balance to depth 0, then Configure(...)'s outer ) closes — this goes negative, so header = true is set (the "argument list" branch of the comment above, not a real header). The next ; is hit at depth == floor == 0 with body < 0, so the walk returns text[start..(i+1)] = ".CreateCommand());" only — the following command.CommandTimeout = 5; statement is excluded from the span.
Every one of these pins' SetsAnExplicitDeadline checks s_assignsTimeout against exactly this span (StatementSpanFrom(code, index, statements: 2)), so a command constructed as a bare argument to another call — a totally ordinary shape (Wrap(x.CreateCommand()), Foo(new NpgsqlCommand(...))) — with its deadline assigned on the following line would be reported as untimed, even though it's correctly timed. That's a false positive on correct code, exactly the failure mode this whole PR is built to eliminate (see the "worse than a miss" reasoning in the doc comments above).
I don't see this shape in the currently-scanned files (grepped for CreateCommand() nested in another call's parens — only the one genuine using (...) case in ViewerDataService.Blocking.cs), which is why the census stays green today. But it's untested and will misfire the moment someone writes a factory/wrapper that takes a freshly-created command as an argument.
Worth either: distinguishing a real statement-header close (only using/if/while/for/foreach/lock/switch/catch parens, which are always immediately followed by {, a braceless embedded statement, or nothing else on the line) from an ordinary argument-list close, or dropping the early-return-at-first-; and letting the normal seen >= statements counting run even when header is true but no body block ever appears.
18a6deb to
9feb8f6
Compare
| private static bool SetsAnExplicitDeadline(string code, int index) | ||
| => s_setsTimeout.IsMatch(CSharpSourceWalker.ConstructionSpanFrom(code, index)) | ||
| || s_assignsTimeout.IsMatch(CSharpSourceWalker.StatementSpanFrom(code, index, statements: 2)); |
There was a problem hiding this comment.
Simplification: SetsAnExplicitDeadline (and the s_setsTimeout / s_assignsTimeout regex pair it uses) is copy-pasted byte-for-byte across all five pins touched by this PR (AlertPassCommandTimeoutTests, AnalysisPassCommandTimeoutTests, FactCollectorCommandTimeoutTests, StorageCommandTimeoutTests, ViewerCommandTimeoutTests), along with a near-identical TheScanner_JudgesTheSiteItself_NotItsNeighbours theory in each.
That's exactly the shape of problem CSharpSourceWalker itself was extracted to solve (per its own doc comment: "Five source-walking pins were each carrying their own copy of this (#2913); this is the one implementation they now share"). This PR adds a brand-new two-regex/one-method unit and re-introduces the same five-way duplication for it rather than hoisting it onto the shared walker (e.g. as CSharpSourceWalker.SetsAnExplicitDeadline(code, index, timeoutFieldName) or similar). Worth folding in now while the logic is fresh, since the walker's own history shows how these copies drift once they've shipped.
Not a correctness issue — all five copies are currently identical — just flagging the maintenance risk this codebase has already paid down once.
ReviewThe production fix ( The scanner logic ( Parity: no Lite counterpart needed here — this is Darling/Npgsql-specific static-analysis test tooling with no equivalent scanning infrastructure on the Lite/SqlCommand side, so there's no drift introduced. Security/perf: none of this touches runtime request paths; it's build-time test tooling plus one bounded timeout on an existing internal command. No injection surface (regexes run over the repo's own source files, not external input). One simplification note left as an inline comment: |
9feb8f6 to
d6141e9
Compare
|
Reviewed. This PR is mostly test-infrastructure (the Production fix (
No correctness, parity, security, or performance issues found. Nothing here should trip the missing-index-DMV concern (not applicable — no T-SQL changed). Style (XML doc comments on internal helpers, block comments) is consistent with |
| <<<<<<< HEAD | ||
| [InlineData( | ||
| "using (var untimed = new NpgsqlCommand(Sql, connection))\n" | ||
| + "{\n" | ||
| + " await untimed.ExecuteNonQueryAsync(cancellationToken);\n" | ||
| + "}\n" | ||
| + "using var next = new NpgsqlCommand(OtherSql, connection) { CommandTimeout = 10 };\n", | ||
| false)] | ||
| [InlineData( | ||
| "using var untimed = new NpgsqlCommand(Sql, connection);\n" | ||
| + "using var next = new NpgsqlCommand(OtherSql, connection) { CommandTimeout = 10 };\n", | ||
| false)] | ||
| [InlineData( | ||
| "var command = _postgres.CreateCommand(Sql);\n" | ||
| + "/* the deadline used to be command.CommandTimeout = 10 here */\n" | ||
| ======= | ||
| /* A comment that NAMES a deadline is not one. This case was green before the value regex moved onto | ||
| the stripped span: the construction match excluded comments while the deadline test did not, so | ||
| prose could satisfy it. False-negative direction - it reported success on the defect. */ | ||
| [InlineData( | ||
| "var command = new NpgsqlCommand(Sql, connection);\n" | ||
| + "/* no deadline needed here; CommandTimeout = 10 is applied by the caller. */\n" | ||
| >>>>>>> origin/dev | ||
| + "await command.ExecuteNonQueryAsync();\n", | ||
| false)] | ||
| public void TheScanner_SeesADeadlineThroughCommentsAndVerbatimSql(string source, bool expectedTimed) | ||
| { | ||
| <<<<<<< HEAD | ||
| ======= | ||
| /* Stripped, exactly as ScanForUntimedCommands does it - a fixture that walked the raw text would | ||
| pass while the shipped scan failed, which is how this defect survived its own theory. */ | ||
| >>>>>>> origin/dev | ||
| var code = CSharpSourceWalker.StripCommentsAndStrings(source); | ||
| var ctor = s_commandCtor.Match(code); | ||
| Assert.True(ctor.Success, "the fixture did not contain a command construction"); | ||
|
|
||
| <<<<<<< HEAD | ||
| Assert.Equal(expectedTimed, CommandDeadlineScanner.SetsAnExplicitDeadline(code, ctor.Index)); | ||
| ======= | ||
| var span = CSharpSourceWalker.StatementSpanFrom(code, ctor.Index, statements: 2); | ||
|
|
||
| Assert.Equal(expectedTimed, s_setsTimeout.IsMatch(span)); | ||
| >>>>>>> origin/dev |
There was a problem hiding this comment.
Blocking: unresolved merge-conflict markers committed to the branch. This range still has literal <<<<<<< HEAD / ======= / >>>>>>> origin/dev markers (lines 308, 323, 330, 335/336, 339, 344, 346, 350). This is not a diff-rendering artifact — grep -n "<<<<<<<" Darling/Darling.Tests/AlertPassCommandTimeoutTests.cs on the checked-out branch confirms they're in the file as committed.
This won't compile: Darling.Tests will fail with parser errors (<<<<<<< isn't valid C#), so CI must be red on this branch, or at minimum any build run against it will fail.
Worth noting the two conflict sides aren't even reconcilable as-is: the origin/dev side (lines 324-330, 347-349) still calls s_setsTimeout directly against a StatementSpanFrom window, but s_setsTimeout was deleted from this file earlier in the same diff (in favor of CommandDeadlineScanner.SetsAnExplicitDeadline). The HEAD side (309-322, 344-345) is the version that matches the rest of the PR's refactor (adds the three new InlineData cases for the scope/neighbour/comment blind spots and calls CommandDeadlineScanner.SetsAnExplicitDeadline). This needs a real conflict resolution — keep the HEAD side's test cases and drop the origin/dev side entirely — before this can merge.
Review summaryBlocking issue: The rest of the change looks sound, modulo not being able to verify it builds given the above:
Once the conflict markers are resolved and the build is confirmed green, this looks mergeable. |
6dd39ef to
6883a0a
Compare
| ArgumentNullException.ThrowIfNull(code); | ||
|
|
||
| return s_setsTimeout.IsMatch(CSharpSourceWalker.ConstructionSpanFrom(code, index)) | ||
| || s_assignsTimeout.IsMatch(CSharpSourceWalker.StatementSpanFrom(code, index, statements: 2)); |
There was a problem hiding this comment.
The "assignment belongs to the statement span" half never checks which command the assignment names, and the header-follows-into-block rule in StatementSpanFrom can hand it a sibling's assignment instead.
Repro (matches the "untimed header whose block opens with a timed sibling" shape this PR explicitly sets out to close, e.g. StorageCommandTimeoutTests.TheScanner_JudgesTheSiteItself_NotItsNeighbours):
using (var untimed = connection.CreateCommand())
{
using var sibling = connection.CreateCommand();
sibling.CommandTimeout = 10;
await untimed.ExecuteNonQueryAsync(cancellationToken);
}Trace through StatementSpanFrom(code, indexOfUntimedCreateCommand, statements: 2):
- the
)closing theusing (...)header goes negative →header = true - the following
{setsbody/floorto the block's depth, andseenis still 0 at that point (the header itself contained no top-level;) - inside the block, the two statements it now counts against the outer
untimedconstruction areusing var sibling = connection.CreateCommand();(seen=1) andsibling.CommandTimeout = 10;(seen=2 → span returned)
The returned span contains sibling.CommandTimeout = 10, which matches s_assignsTimeout (\.\s*CommandTimeout\s*=) — so SetsAnExplicitDeadline reports untimed as timed, even though untimed never gets a CommandTimeout and sibling's assignment belongs to a completely different variable.
Every existing "block opens with a timed sibling" fixture (in this file's tests, ViewerCommandTimeoutTests, AnalysisPassCommandTimeoutTests, FactCollectorCommandTimeoutTests) only exercises the sibling via an object initializer ({ CommandTimeout = 10 }), which has no leading dot and so doesn't hit s_assignsTimeout. None of them exercise the sibling via the CreateCommand() + member-assignment spelling — which the PR's own numbers say is the dominant shape in this codebase (112 of 119 MCP sites). That's exactly the combination that slips through here.
Since the whole point of this pass is closing "the scan answering about something other than the construction it's pointing at," it seems worth pinning: the statement-span check would need to confirm the assignment's target identifier is the same variable the construction under test was bound to, not just that some .CommandTimeout = appears in the window.
|
Reviewed this branch (the The production fix itself is correct and minimal — the Left one inline finding on Otherwise this is a solid, well-tested change: the mutation-tested walker logic, the stripped-source handling for both construction enumeration and deadline matching, and the consolidation of the duplicated regex/judgement into |
DarlingRetention.PurgeOneAsync lifts a TimescaleDB decompression rail through an
untimed `using (...)` whose single body statement let the timed delete behind it
stand in, so it inherited Npgsql's 30 s default. It now carries
DeleteTimeoutSeconds. It is the only such site in the repository and it sits
outside every pin's scan scope, so no pin's published figure was false - but the
class was live in shipped code.
Three independent blind spots let it hide, all of them a scan answering about
something other than the construction it points at.
StatementSpanFrom let its bracket depth go negative and keep counting, so a
construction in a `using (...) { }` with a one-statement body spent the window on
that statement and on the statement after the block. The span now ends with the
scope it started in - a closing BRACE ends it, a parenthesis does not - and still
follows a header into the block or the braceless statement it governs, which
fourteen sites need.
Two layouts no scope bound can reach, an untimed construction directly ahead of a
timed one and an untimed header whose block opens with a timed sibling, are closed
by splitting the question: ConstructionSpanFrom covers the site and its own
initializer, and only a member assignment is read from the statement span.
Truncating the span at the next construction instead breaks the conditional in
ViewerDataService.FinOps.Locking.cs, where two constructions share one deadline.
Both halves are now read over STRIPPED source, so neither a construction named in
prose nor a deadline merely spelled in a comment can decide a verdict. The
.Analysis and PgFactCollector pins move off a raw three-line window onto the same
rule, and all five take the alert pass's fully-qualified construction pattern.
Every scope re-measured and unchanged: .Storage 124/124, .Viewer 193/193,
.Analysis 73/73, PgFactCollector 32/32, alert pass 45/45, MCP reads 126/126.
6883a0a to
29d90d7
Compare
ReviewThe However, the PR's central claim — "bound every timeout scan to the site it is pointing at," with the two-part 1.
|
…ed green CI Four groups append constants to ServiceCommandDeadlines.cs, and each group's block continues a <summary> its predecessor opened ABOVE the conflict hunk -- so a keep-both-sides resolution leaves the second block with a closing tag and no opening one. Group E hit it and reopened the element; this group hit it and shipped it: all six checks passed on a file with 8 openings against 9 closings. DocCommentHygieneTests cannot see it. That rule counts <summary> OPENINGS per doc run and fails on two or more, which is right for the displaced-block defect it was written for; a run with ZERO openings satisfies it vacuously. Proven on the same mutation -- this guard goes red naming the nesting depth, that rule stays green. Widening the shipped rule to require balance finds exactly ONE other offender across 2,108 .cs files (Lite.Tests/FindingStoreTests.cs:656, the identical opens=0 closes=1 shape), so the repo-wide version is worth adding and is tracked separately rather than folded into a deadline change. This is the narrow version: the one file where the trap is structural, and no cross-project surface. Also routes the nine sites through #2938's shared CommandDeadlineScanner as a second, additive guard. It answers a weaker question than the relational check -- 'is there a deadline' rather than 'is it this regime's constant' -- but answers it better, reading the construction's initializer separately and qualifying the assignment by the name bound to the construction, so a sibling's deadline cannot be borrowed. Proven to catch what the relational check alone accepts: a deadline attributed to an alias of the command fails only the scanner guard.
… prose The count of pins routing through CommandDeadlineScanner was stated in doc comments and checked by nothing, so it went stale three times: #2938 wrote five, #2940 made it six, #2966 seven, and this branch nine. The enumerated list beside the count rotted the same way, which is the worse half - a reader looking for the adopters found four names and no hint that others existed. CommandDeadlineScannerAdoptionTests globs the pins and re-derives the set from the tree, against a declared adopter list and a declared abstainer list. The abstainer half is what earns the test: a new *CommandTimeoutTests.cs arriving with its own private copy of the rule lands in neither list and fails asking which it is, which is how the two holdouts consolidated here came to exist. StartupCommandTimeoutTests is the one declared abstainer, because it judges every site relationally against its own bootstrap constant and the shared scanner cannot express which constant a site must take. The membership test reads STRIPPED source, so prose naming the method does not count as routing through it - every file in this family discusses the shared judgement at length, and a raw read would pass on the commentary. The prose counts in CommandPlaneCommandTimeoutTests and CSharpSourceWalkerTests are dropped rather than corrected, since a number nothing asserts is what failed here. CommandDeadlineScanner.cs's own stale count is left alone to avoid colliding with the deletion in flight against that file.
#2981) CommandDeadlineScanner's class summary opened with a count and a roster of the pins routing through it. Nothing checked either, and both drifted: the count was written five, then six, then seven, then nine over #2938, #2940, #2966 and #2972, and the enumerated list never moved at all. CommandDeadlineScannerAdoptionTests already re-derives the adopting set from the tree and declares the deliberate abstainers beside it, so the summary now points there instead of restating it. Nothing left in the comment is countable, so there is no figure left to go stale. The same file quoted the MCP read surface's census as "112 of the MCP surface's 119 sites" to argue that the assignment spelling dominates. That numerator no longer holds - every site in McpReadCommandTimeoutTests' scope is assignment-spelled today - so the claim keeps its point and hands the census back to the pin that maintains it. The adoption pin's own rationale counted the names in the roster it replaced. That figure described a list this change removes, so it goes too.
A retention command was inheriting the 30 s default, and the pins that exist to say so could not see it
DarlingRetention.PurgeOneAsynclifts a TimescaleDB decompression rail before a batched purge:The
liftcommand set no deadline, so it ran on Npgsql's undocumented 30 s default — the defect class #2874 exists to eliminate. Four of that file's five constructions already carriedDeleteTimeoutSeconds; this one was missed, and the scan reported it clean because the two-statement window reached past the block's closing brace and found the initializer on thedeletebehind it. It now carriesDeleteTimeoutSeconds.It is the only such site in the repository's command constructions, and it sits outside every #2874 pin's scan scope — so no pin's published figure was false, but the class was live in shipped code rather than theoretical.
Three independent blind spots let it hide
All three are the same mistake: the scan answering about something other than the construction it is pointing at.
1. The span ran out of the scope it started in (false negative)
CSharpSourceWalker.StatementSpanFromlet its bracket depth go negative and keep counting semicolons. A construction in ausing (...)header whose block body holds ONE statement spends the two-statement window on that statement and on the statement AFTER the block, so the FOLLOWING command's initializer satisfies the scan. The same leak runs out through the closing brace of any block a construction is the last statement of.Reproduced against #2935's branch, one mutation at a time, each restored byte-identically:
using (...)block followed by a timed construction —Total: 6, Failed: 0, silentusing var, nothing timed after it —1 storage command(s) inherit Npgsql's 30s default ... PgMigrations.cs:4370, firesThe pin was directional: it saw an untimed site only when no timed construction followed within two statements.
TheScanner_JudgesTheSiteItself_NotItsNeighbours, written to prevent exactly this, had no case with a one-statement body.The span now ends with the SCOPE it started in. A closing BRACE that was open beforehand ends it; a parenthesis or bracket does not, because it delimits an expression and an expression that opened earlier does not change which block the construction lives in.
It still follows a statement header into the block — or the braceless statement — that header governs, and that half is load-bearing. Fourteen sites across the scanned projects set the deadline as that block's FIRST statement. Stopping at the header, the smaller change, reports every one of them; confirmed by mutation, which fails four pins on real code.
Judging only the construction and its initializer, the other candidate, is worse: all 193 viewer sites and 51 of
.Storage's 124 set the deadline in the following statement, because aCreateCommandresult cannot take an initializer. That is what the two-statement allowance exists for.2. The neighbour no scope bound can exclude (false negative)
Two layouts survive the scope fix, because the sibling is in the same scope: an untimed construction directly ahead of a timed one, and an untimed
using (...)header whose block OPENS with a timed sibling.Both are closed by asking the question in two halves. A new
ConstructionSpanFromreturns the site and its own object initializer, and the bareCommandTimeout =pattern is read from THAT and nothing wider. Only a MEMBER assignment —command.CommandTimeout = ..., the one placement aCreateCommandresult allows — is read from the surrounding statement span. A value in an initializer belongs to the construction it is attached to; a value assigned through a member access belongs to whatever it names.Cutting the statement span at the next construction instead is the tempting one-liner, and it is wrong. Two constructions can share one deadline — the conditional in
ViewerDataService.FinOps.Locking.csdoes — and cutting there reports the first arm as an offender. That shape was already a pinned case in the viewer pin, so the one-liner would have turned an existing green case red. It is now a case in all five pins.3. Both sides of the question read RAW source (false positive and false negative)
The scans matched raw text when enumerating constructions, so a construction named in a comment or a string was reportable as an untimed site at a line where no edit could ever fix it. Demonstrated on real code: with a construction named in a comment in
StorageVersion.cs, the landed pin reportsStorageVersion.cs:19— a comment — and the fixed pin reports nothing.The mirror of it is worse, and the two-part rule above does not close it on its own: the deadline check also read raw text, so a deadline merely SPELLED in a comment satisfied a site that had none. Reproduced against this branch before fixing it — a block comment
/* the deadline used to be command.CommandTimeout = 10 here */, a line comment, and a string literal all read as timed; all three now read untimed, and both legitimate controls (a real assignment with a comment in the gap, and an initializer) still read timed.Both halves now route through one stripped
codeper file. The doc remarks that used to justify cutting spans from the ORIGINAL text — in the walker and in every pin — now say what the code does instead; those remarks were the thing that would otherwise let this be re-introduced, because a walk made literal-aware and then handed raw text throws the awareness away at the last step.This codebase quotes code in its prose constantly, so both directions were live. Not one phantom sits inside any pin's scan scope, which is the figure that means something — a repo-wide count would go stale on the next commit, and this change itself adds more of them to the test suite.
The defects also interact: a phantom planted next to a timed statement is itself hidden by the over-wide window.
All five scans, not just the two that were reported
.AnalysisandPgFactCollectorjudged a site by joining the next three RAW source lines, which can neither tell a construction from the same words in a comment nor tell this site's deadline from the next site's. The alert pass, consolidated to a single scan helper by #2934, judged a single raw span. All now use the same two-part rule over stripped source.The clearest evidence is one run, one file, two pins: with an untimed construction planted immediately ahead of a timed one in
PgFactCollector.Activity.cs, the landed three-line pin says clean while the migrated scan reportsPgFactCollector.Activity.cs:61. Both pins read that exact file.All five also take the alert pass's fully-qualified construction pattern —
new\s+(?:[A-Za-z_][A-Za-z0-9_]*\s*\.\s*)*NpgsqlCommand\s*\(— which is strictly more general than theNpgsql.-only form and sees a construction qualified by any namespace. There is one qualified occurrence in the solution, inDarlingWorker.TryRefreshPgStatementTextAsync, a member the merged collection-sweep census enlists — so that census counts 13 where 14 constructions exist. It is already timed, and whether a monitored-target read belongs in that budget is that group's call, so it is flagged rather than changed here.Every figure, re-measured after the fix
Counts are as measured at this head;
devadds command sites steadily, so the load-bearing claim is the last two columns rather than the first. Every figure below is the SHIPPED judgement, not a stand-in for it — the measuring tool compilesCommandDeadlineScanneritself..Storage.Viewer.AnalysisPgFactCollector.*.csThe MCP read surface is the densest population of the affected shape — 112 of its 119
Mcp/sites place the deadline in a following statement, structurally, because.CreateCommand(returns a command. It measures clean under four independent rules: the landed span, the scope-bounded span, the shipped two-part rule, and a deliberately stricter control that additionally requires the assignment to name the identifier the construction was bound to. That control resolves 119 of 119 names, so the shipped rule's one residual — an assignment on a different variable inside the window — is empirically empty there.Two pins in the family are NOT fixed by this
Stated plainly, because the headline would otherwise overreach. Five pins route through the shared judgement:
.Storage,.Viewer,.Analysis,PgFactCollectorand the alert pass. Two do not, and both still reproduce blind spots this closes elsewhere:CollectionSweepCommandTimeoutTestsstill cuts its span from RAW member source and matches an unqualified value regex over it, so a deadline spelled in a comment satisfies a site, and a sibling's assignment satisfies one too. Untouched here on purpose — it is another group's just-landed pin and its census constant is a scope judgement I should not make unilaterally.StoreSelfMetricsTimeoutTestsis a whole-file count-equality pin over one file, not a per-site scan at all. Relocating a deadline from one command to another leaves its counts invariant, so it cannot see a move. All five of that file's sites currently carry the initializer, so it reports truthfully today.Adopting the shared judgement in either is a one-line change now that it exists.
What this does NOT establish
The enumeration is regex-based, and that is a design limit, not an oversight. These pins find a construction by matching its written form, so a construction whose TYPE NAME is not at the site is invisible to them however the pattern is widened. Target-typed
newis exactly that shape: inreturn new(...)or=> new(...)the type lives in the member's return type, and in= new(...)it lives on the declaration. No pattern over the construction site can see it.So the claim this PR supports is "every construction these patterns can see is judged correctly", not "every construction is judged". The gap is bounded rather than open, though — measured, not assumed:
new(...)sites in the repository, over stripped source so prose cannot contributeCommandPlan,AlertHistoryRecord,PlanForceActionRecord,MonitoredServerRow,PlanDimRecompression.Result, the two target-provider singletons, aConcurrentDictionaryand aList<string>So the shape is a footnote today, not a hole — but it is a footnote a widened regex cannot keep true, and the next command written as
return new(...)would be invisible to every pin. Closing it properly needs the target type resolved, which is a different tool from this one.Two other things worth stating plainly. The
.CreateCommandbare method group is matched by the pins that assert it is ABSENT and not by the five that judge deadlines, because a method group has no argument list for either pattern to anchor on. And the two-statement allowance is a syntactic claim — the construction's own statement plus the one after it — not a tuned window; it is now bounded by scope on one side and by the construction span on the other, so its size is no longer what decides a verdict.4. A match with no argument list ran away (false negative)
Caught in review on this branch, and it was mine: two pins match a bare
.CreateCommandMETHOD GROUP on purpose, andConstructionSpanFromassumed its start was followed by the construction's own(. A method group has none, so the scan walked out of the statement, found the NEXT construction's parentheses, and returned that span with its initializer — a hand-off with no deadline of its own reading as timed.Reproduced before fixing:
builder.Register(factory.CreateCommand, options);followed by a timed construction read as timed, as did theFunc<NpgsqlCommand> f = connection.CreateCommand;form. The walk to the opening parenthesis may now only cross what a construction's head is made of — thenewkeyword, a qualified name, generic arguments — so a match with nothing attached spans the reference alone and carries no deadline. Both hand-off forms now read untimed; both legitimate called forms still read timed.5. A header and an argument list are not the same closing parenthesis (false positive)
Also caught in review, also mine, and introduced by the fix for the braceless case above. Once the span may end at a header's embedded statement, it has to know a header's
)from an argument list's. It did not, soWrap(connection.CreateCommand())— a construction nested in an ordinary call — had its span cut off before the assignment on the next line, reporting a correctly-timed command.The two are told apart by what follows the parenthesis: a statement header is followed by the statement it governs, an argument list by the rest of its own expression. Reproduced before fixing (both the plain and the
.Innermember-access form read as untimed), and both directions are now pinned — conflating them fails the new argument-list witness, and never recognising a header fails the braceless and scope witnesses plus the landed witnesses inMcpReadCommandTimeoutTestsandCollectionSweepCommandTimeoutTests, so the header path is load-bearing for two other groups' pins as well.Two of the five defects here were mine, both found by review rather than by my own mutations. The pattern in both: a bound added for one shape changed the verdict for a shape I had already reasoned about and stopped re-checking. The fifteen-case matrix in the walker's tests now covers all of them together rather than one at a time.
6. The assignment half never checked WHICH command it named (false negative)
The last one, and the sharpest:
\.CommandTimeout\s*=over the statement span accepts a SIBLING's assignment. An untimedusing (...)header whose block opens withusing var sibling = conn.CreateCommand();andsibling.CommandTimeout = 10;spends both counted statements on the sibling, and the outer command reads as timed.Every sibling fixture in this family — mine included — exercised the sibling through an object INITIALIZER, which has no leading dot and so never reached that regex. The assignment spelling it missed is the dominant one: 112 of the MCP surface's 119 sites. So the hole sat precisely where the fixtures did not look.
The assignment must now name the identifier the construction was bound to, taken as the identifier before the FIRST
=of the construction's statement — the binder in a declaration, a header declaration and a field assignment alike, and absent exactly where there is no binder (return new ...,Wrap(conn.CreateCommand())), where the initializer is then the only place a deadline could be. First rather than last so a conditional whose arms are each a construction still resolves to the one variable they share.Dropping the qualification fails the new witness in all five pins. And the shipped judgement now measures zero offenders in every scope, the viewer's 193 included — the conditional in
ViewerDataService.FinOps.Locking.csresolves correctly, which an earlier and cruder name walk of my own did not.One judgement, shared
Review also pointed out that the two regexes and the judgement they feed were copy-pasted byte-for-byte into all five pins — the same duplication
CSharpSourceWalkerwas extracted to end in #2913 (five copies of the source walk, already drifted, only one of them hardened) andBraceBalancedin #2923/#2927. Fair, and paid down here rather than left to drift:CommandDeadlineScannerowns both regexes and the two-part judgement, and all five pins call it. Neutering one regex in that one file now fails 17 tests across all five pins, which is the evidence that they genuinely share it.CSharpSourceWalkerstays generic — it walks C# source and knows nothing about deadlines. The domain judgement lives in its own type, which is the patternIlCallSiteScanneralready set for the IL walk.Verification
Every guard proved red first, one mutation at a time, each asserted applied and compiling, each moving the behaviour under test, each restored by copy with a dirty-file count logged. Each mutation killed a different set:
ConstructionSpanFromreturning the whole statement span fails every neighbour witness across all five pins; dropping the initializer fails the verbatim-SQL and comment-gap controlsEnd to end on real code, same planted mutation both sides:
devreportsFailed: 0, the fix reportsPgMigrations.cs:4144.DarlingRetentionTestsslicesDarlingRetention.csby string anchor, once by a fixed character window and once brace-matched from inside the method this change edits. Both anchors still resolve, both still hold, and a planted-phrase control confirms each can still fail.Darling.Testsisnet10.0-windowsand cannot run on macOS, so CI is the arbiter. Locally, the real test files compiled by absolute path into anet10.0xunit host staged inside the repo tree run 254 tests against 195 for the same harness ondev, with the same single pre-existing failure on both sides (LivePostgresCollectionHygieneTests, which fails identically without this change).