Repository navigation
Tests - Enforce the Stop-Function flow-control invariants over all commands - #10755
Conversation
…mmands Adds the "command structure" Describe to dbatools.Tests.ps1 (Compliance tag, no SQL Server needed), as agreed in #10655: - rule 1: a Stop-Function that can continue (-Continue, or -SilentlyContinue under EnableException) has a loop or switch in the same function or scriptblock, and -ContinueLabel names one of them - rule 3: a begin block that can set the command's interrupt flag is followed by if (Test-FunctionInterrupt) { return } as the first statement of process - every Stop-Function parameter binds (names, aliases, unique prefixes) - every command file parses The analyzer is tests\dbatools.CommandStructure.ps1; the "command structure analyzer" Describe covers it with positive and negative fixtures. The 23 intentional dynamic-continue sites are listed as exceptions with their reason and exact site count. The invariants and the guidance for rules 2, 4, 5 and 6 are in CLAUDE.md, linked from tests\CLAUDE.md. (do Stop-Function) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#10756 replaces the Stop-Function -Continue in the two mount point helpers with a warning, so their three sites are no longer dynamic continue exceptions. (do Stop-Function) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#10757 makes every stop in the export-cert helper return from the helper, so it no longer relies on a dynamic continue. (do Stop-Function) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
potatoqualitee
left a comment
There was a problem hiding this comment.
Three blocking false negatives let commands violate the very flow-control invariants this PR is intended to enforce:
-
ests/dbatools.CommandStructure.ps1:155-166 resolves a splat from the last lexical variable assignment anywhere earlier in the function. It ignores later member mutation and includes assignments in nested or nonexecuted paths. For example, $splatStop = @{ Continue = False }; .Continue = True; Stop-Function @splatStop produces zero findings, yet the runtime continue can escape the caller's loop. Resolve only demonstrably reaching definitions in the actual execution scope, detect mutations, and treat uncertainty as unresolved; add negative fixtures for mutation, uncalled nested helpers, and conditional assignments.
-
Line 301 exempts every statically true -Continue from rule 3. With -EnableException True, real Stop-Function sets the interrupt flag and throws before any continue; if begin catches that exception, process runs unless it has the guard. A begin-block ry { Stop-Function -Continue -EnableException True } catch {} currently produces zero findings while process work executes with the flag set. Require the guard when that throwing path can resume, or report the path for review; add a caught-throw fixture.
-
Lines 280-291 recognize a guard from only the command name and return body. if (Test-FunctionInterrupt > ) { return } is accepted, but the true value is discarded, the condition is false, and process work runs. Unsupported arguments are likewise accepted. Reject success-output redirection and arguments when recognizing the guard, and add negative fixtures.
These are material misleading-compliance failures: the new analyzer and green test suite certify unsafe commands. All three paths were independently confirmed from the exact analyzer and the real Stop-Function/Test-FunctionInterrupt semantics.
(do dbatools) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…allowlist its forwarded -Continue (do dbatools) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks, all three confirmed: each of your examples produced zero findings. Fixed in f1e5d0f and 599a710. 1. Splat resolution. A splat now counts as resolved only from the definition that certainly reaches the call:
It is unresolved when any of these happens:
Unresolved splats report an "Arguments" finding. Every switch then counts as unknown, including EnableException, so they also count for rules 1 and 3. Reusing one splat name for several literals, each directly before its own call, stays resolved (Install-DbaAgentAdminAlert does that). 2. Caught throw in begin. A
Rule 3 then requires the guard. The CLAUDE.md text of rule 3 says so now. 3. Guard recognition. Fixtures. Negative:
Positive: the reused splat name, and a caught What it found. The stricter splat rule found one site that the old one had certified: the private helper
Lab: both "command structure" Describes 34/34 on Windows PowerShell 5.1 and PowerShell 7.6.3. This text was created by Claude and reviewed by Andreas Jordan. |
potatoqualitee
left a comment
There was a problem hiding this comment.
Reviewed the exact current head. The three prior analyzer false negatives are addressed: uncertain/mutated splats are no longer certified, caught EnableException paths from begin require the process guard, and redirected/argument-bearing Test-FunctionInterrupt calls no longer count as guards. The new fixtures cover those failure modes and the current compliance CI is green. I found no remaining material defect.
|
very nice 💯 |
Implements the enforcement and documentation agreed in #10655: AST enforcement for rules 1 and 3, the other rules as guidance.
What it adds
tests/dbatools.CommandStructure.ps1: the analyzerGet-DbaCommandStructureFinding, which needs no SQL Server and does not load the module.tests/dbatools.Tests.ps1, next to the test-file checks (tagCompliance). It parses the 887 files inpublic/andprivate/functions/in about 10 seconds and reportsfile:line function - message:switchto continue in the same function or scriptblock, and-ContinueLabelnames one of them;if (Test-FunctionInterrupt) { return }as the first statement of process.CLAUDE.md: a COMMAND INVARIANTS section with rules 1-6. Rules 1 and 3 are marked as enforced, rule 2 and rules 4-6 are guidance, in the wording of the review on the issue. It is linked from a new section intests/CLAUDE.md.How the review points are handled
switchboth count as targets.-ContinueLabelmust match the label of an enclosing loop orswitch.ForEach-Objectcallback does not count; a callback with its own loop does.-SilentlyContinueis not treated like-Continue.-Continuecontinues in the default mode,-SilentlyContinueunder EnableException.-Continueis exempt.-SilentlyContinuealone sets the flag in the default mode, so it requires the guard.beginreaches the command'sprocessfor a direct call,ForEach-Object,Where-Object,. { }and.ForEach()/.Where(). It does not for& { },Invoke-Command,.Invoke()or a helper function. Any other invocation is reported for review.-Continue:$variable, a splat that is not a hashtable literal assigned earlier in the same function, and a-ContinueLabelthat is not a constant all count as "may continue". A parameter that binds to nothing (checked by name, alias and unique prefix, like PowerShell) is reported.-Continue:$false,-Continue:$variable);-EnableExceptiontaking a value.Findings on development, fixed in their own PRs
The first run found six defects that the earlier text sweeps missed. Each is fixed in its own draft PR, lab-tested on both editions:
-ContinueLabel mainwith no loop carrying that label. Measured on 5.1 and 7.6, a labeledcontinuethat matches nothing ends the whole calling script silently with exit code 0. Against the old commands, their new tests ended the entire test run.-ErroRecordbinds to nothing, so the catch failed with a parameter binding error instead of warning.?. Its two entries are dropped from the exception list here.return, so the export was attempted anyway; every stop in its helper now returns instead of relying on a dynamic continue. Its entry is dropped from the exception list here.This PR stays red until those seven merge. With all seven merged onto this branch locally, the two Describes pass 26/26 on Windows PowerShell 5.1 and PowerShell 7.6.3. The existing style and test-file-structure checks pass with the new files.
Closes #10655
This text was created by Claude and reviewed by Andreas Jordan.
🤖 Generated with Claude Code