< Summary - Kestrun — Combined Coverage

Information
Class: Private.Variable.Get-KrAssignedVariables
Assembly: Kestrun.PowerShell.Private
File(s): /home/runner/work/Kestrun/Kestrun/src/PowerShell/Kestrun/Private/Variable/Get-KrAssignedVariables.ps1
Tag: Kestrun/Kestrun@ca54e35c77799b76774b3805b6f075cdbc0c5fbe
Line coverage
85%
Covered lines: 274
Uncovered lines: 47
Coverable lines: 321
Total lines: 761
Line coverage: 85.3%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 09/08/2025 - 20:34:03 Line coverage: 67.4% (58/86) Total lines: 185 Tag: Kestrun/Kestrun@3790ee5884494a7a2a829344a47743e0bf492e7209/12/2025 - 03:43:11 Line coverage: 67.4% (58/86) Total lines: 184 Tag: Kestrun/Kestrun@d160286e3020330b1eb862d66a37db2e26fc904209/15/2025 - 19:16:35 Line coverage: 67.8% (59/87) Total lines: 185 Tag: Kestrun/Kestrun@bfb58693b9baaed61644ace5b29e014d9ffacbc910/13/2025 - 16:52:37 Line coverage: 0% (0/87) Total lines: 185 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e12/25/2025 - 19:20:44 Line coverage: 0% (0/91) Total lines: 201 Tag: Kestrun/Kestrun@5251f12f253e29f8a1dfb77edc2ef50b90a4f26f01/08/2026 - 02:20:28 Line coverage: 85.3% (274/321) Total lines: 761 Tag: Kestrun/Kestrun@4bc17b7e465c315de6386907c417e44fcb0fd3eb

Metrics

File(s)

/home/runner/work/Kestrun/Kestrun/src/PowerShell/Kestrun/Private/Variable/Get-KrAssignedVariables.ps1

#LineLine coverage
 1<#
 2    .SYNOPSIS
 3      Find variables that are *defined/assigned* in a scriptblock and (optionally) fetch their values.
 4    .DESCRIPTION
 5      Scans the AST for AssignmentStatementAst where the LHS is a VariableExpressionAst,
 6      and optionally Set-Variable/New-Variable calls. Can resolve current values from
 7      the appropriate scope (local/script/global) in the *current* runspace.
 8    .PARAMETER ScriptBlock
 9      ScriptBlock to scan. If omitted, use -Current to inspect the currently running block.
 10    .PARAMETER FromParent
 11      Inspect the *currently executing* scriptblock (via $MyInvocation.MyCommand.ScriptBlock).
 12    .PARAMETER IncludeSetVariable
 13      Also detect variables created via Set-Variable/New-Variable (-Name … [-Scope …]).
 14    .PARAMETER ResolveValues
 15      Resolve current values from variable:<scope>:<name> and include Type/Value.
 16    .PARAMETER DefaultScope
 17      Scope to assume when the LHS had no explicit scope (default: Local).
 18    .PARAMETER ExcludeVariables
 19      Array of variable names to exclude from the results.
 20    .PARAMETER WithoutAttributesOnly
 21      If specified, only include variables that were defined without attributes.
 22    .PARAMETER OutputStructure
 23      Output format: 'Array' (list of objects), 'Dictionary' (name->object), or
 24      'StringObjectMap' (name->value).
 25    .OUTPUTS
 26      PSCustomObject with properties: Name, ScopeHint, ProviderPath, Source, Operator,
 27        Type, Value.
 28    .EXAMPLE
 29      Get-KrAssignedVariable -FromParent -ResolveValues
 30      Scans the currently executing scriptblock for assigned variables and resolves their values.
 31    .NOTES
 32      This function is used by Enable-KrConfiguration to capture user-defined variables
 33      from the caller script and inject them into the Kestrun server's shared state.
 34#>
 35function Get-KrAssignedVariable {
 36    [CmdletBinding(DefaultParameterSetName = 'Given')]
 37    [OutputType([object[]])]
 38    [OutputType([System.Collections.Generic.Dictionary[string, object]])]
 39    param(
 40        [Parameter(ParameterSetName = 'Given', Position = 0)]
 41        [scriptblock]$ScriptBlock,
 42
 43        # NEW: use the caller's scriptblock (parent frame)
 44        [Parameter(ParameterSetName = 'FromParent', Mandatory)]
 45        [switch]$FromParent,
 46
 47        # How many frames up to climb (1 = immediate caller)
 48        [Parameter(ParameterSetName = 'FromParent')]
 49        [int]$Up = 1,
 50
 51        # Optional: skip frames from these modules when searching
 52        [Parameter(ParameterSetName = 'FromParent')]
 153        [string[]]$ExcludeModules = @('Kestrun'),
 54
 55        [Parameter()]
 56        [switch]$IncludeSetVariable,
 57
 58        [Parameter()]
 59        [switch]$ResolveValues,
 60
 61        [Parameter()]
 62        [ValidateSet('Array', 'Dictionary', 'StringObjectMap')]
 63        [string]$OutputStructure = 'Array',
 64
 65        [Parameter()]
 66        [ValidateSet('Local', 'Script', 'Global')]
 67        [string]$DefaultScope = 'Script',
 68
 69        [Parameter()]
 70        [string[]]$ExcludeVariables,
 71
 72        [Parameter()]
 73        [switch]$WithoutAttributesOnly
 74    )
 75
 176    $excludeSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
 377    $excludeList = if ($null -ne $ExcludeVariables) { $ExcludeVariables } else { @() }
 178    foreach ($ev in $excludeList) {
 179        if ([string]::IsNullOrWhiteSpace($ev)) { continue }
 180        $n = $ev.Trim()
 281        if ($n.StartsWith('$')) { $n = $n.Substring(1) }
 182        if (-not [string]::IsNullOrWhiteSpace($n)) {
 183            [void]$excludeSet.Add($n)
 84        }
 85    }
 86
 187    $rows = [System.Collections.Generic.List[object]]::new()
 88
 89    <#
 90    .SYNOPSIS
 91         Extracts the variable name and scope from a VariablePath.
 92    .DESCRIPTION
 93         This function analyzes a VariablePath object to determine the variable's name and scope.
 94    .PARAMETER variablePath
 95         The VariablePath object to analyze.
 96    .OUTPUTS
 97         PSCustomObject with properties: Name, ScopeHint, ProviderPath.
 98    #>
 99    function _GetScopeAndNameFromVariablePath([System.Management.Automation.VariablePath] $variablePath) {
 1100        $raw = $variablePath.UserPath
 1101        if (-not $raw) {
 0102            $raw = $variablePath.UnqualifiedPath
 103        }
 1104        if (-not $raw) {
 0105            return $null
 106        }
 107
 1108        $name = $raw
 1109        $scopeHint = $DefaultScope
 110
 1111        if ($name -match '^(?<scope>global|script|local|private):(?<rest>.+)$') {
 2112            $scopeHint = ($Matches.scope.Substring(0, 1).ToUpperInvariant() + $Matches.scope.Substring(1).ToLowerInvaria
 1113            $name = $Matches.rest
 114        }
 115
 116        # ignore member/index forms and ${foo}
 1117        if ($name.Contains('.') -or $name.Contains('[')) {
 0118            return $null
 119        }
 1120        if ($name.StartsWith('{') -and $name.EndsWith('}')) {
 0121            $name = $name.Substring(1, $name.Length - 2)
 122        }
 123
 1124        if (-not $name) {
 0125            return $null
 126        }
 127
 1128        [pscustomobject]@{
 1129            Name = $name
 1130            ScopeHint = $scopeHint
 2131            ProviderPath = "variable:$($scopeHint):$name"
 132        }
 133    }
 134
 135    <#
 136    .SYNOPSIS
 137        Attempts to resolve the value of a variable by name and scope.
 138    .DESCRIPTION
 139        This function tries to get the value of a variable from the specified scope.
 140        If -ResolveValues is not specified, it returns $null.
 141    .PARAMETER name
 142        The name of the variable to resolve.
 143    .PARAMETER scopeHint
 144        The scope hint (Local, Script, Global) to use for resolution.
 145    .OUTPUTS
 146        The value of the variable if found; otherwise, $null.
 147    #>
 148    function _TryResolveValue([string] $name, [string] $scopeHint) {
 1149        if (-not $ResolveValues.IsPresent) {
 0150            return $null
 151        }
 152
 153        # When using -FromParent, prefer numeric scope resolution so we can see the caller's values.
 1154        if ($FromParent.IsPresent) {
 155            # Use Get-Variable (not provider) so we can distinguish "not found" from "$null".
 156            # Scope depth can vary depending on wrapper frames (VS Code, Invoke-Command, etc.).
 157            # Try a small window around the computed scopeUp first, then scan a bounded range.
 1158            $scopeCandidates = [System.Collections.Generic.List[int]]::new()
 159
 1160            if ($scopeUp -ge 0) {
 1161                $scopeCandidates.Add([int]$scopeUp)
 162            }
 1163            if ($scopeUp -ge 0) {
 2164                $scopeCandidates.Add([int]($scopeUp + 1))
 165            }
 1166            if ($scopeUp -gt 0) {
 2167                $scopeCandidates.Add([int]($scopeUp - 1))
 168            }
 169
 1170            $maxAdditionalScopes = 25
 2171            $start = [Math]::Max(1, [int]($scopeUp - 2))
 2172            $end = [Math]::Max($start, [int]($scopeUp + $maxAdditionalScopes))
 3173            for ($s = $start; $s -le $end; $s++) {
 1174                $scopeCandidates.Add([int]$s)
 175            }
 176
 3177            foreach ($s in ($scopeCandidates | Select-Object -Unique)) {
 178                try {
 1179                    $v = Get-Variable -Name $name -Scope $s -ErrorAction SilentlyContinue
 1180                    if ($null -ne $v) {
 1181                        return $v.Value
 182                    }
 183                } catch {
 184                    # ignore
 0185                    Write-krLog -Level Verbose -Message "Variable '$name' not found at scope depth $s."
 186                }
 187            }
 188
 0189            return $null
 190        }
 191
 192        try {
 2193            return (Get-Item -ErrorAction SilentlyContinue "variable:${scopeHint}:$name").Value
 194        } catch {
 0195            return $null
 196        }
 197    }
 198
 199    <#
 200    .SYNOPSIS
 201        Attempts to extract the initializer value from an assignment AST node.
 202    .DESCRIPTION
 203        This function analyzes an AssignmentStatementAst to extract the value being assigned,
 204        if possible. It supports simple constant expressions, arrays, hashtables, and some
 205        common expression types.
 206    .PARAMETER assignmentAst
 207        The AssignmentStatementAst node to analyze.
 208    .OUTPUTS
 209        The extracted value if resolvable; otherwise, $null.
 210    #>
 211    function _TryGetInitializerValueFromAssignment([System.Management.Automation.Language.AssignmentStatementAst] $assig
 1212        if (-not $ResolveValues.IsPresent) {
 0213            return $null
 214        }
 1215        if ($assignmentAst.Operator -ne [System.Management.Automation.Language.TokenKind]::Equals) {
 1216            return $null
 217        }
 218
 1219        $expr = $null
 1220        if ($assignmentAst.Right -is [System.Management.Automation.Language.CommandExpressionAst]) {
 1221            $expr = $assignmentAst.Right.Expression
 0222        } elseif ($assignmentAst.Right -is [System.Management.Automation.Language.PipelineAst]) {
 0223            $p = $assignmentAst.Right
 0224            if ($p.PipelineElements.Count -eq 1 -and $p.PipelineElements[0] -is [System.Management.Automation.Language.C
 0225                $expr = $p.PipelineElements[0].Expression
 226            }
 227        }
 228
 229        <#
 230        .SYNOPSIS
 231            Evaluates a StatementAst to extract its value.
 232        .DESCRIPTION
 233            This function analyzes a StatementAst node to extract its value, if possible.
 234        .PARAMETER s
 235            The StatementAst node to analyze.
 236        .OUTPUTS
 237            The extracted value if resolvable; otherwise, $null.
 238        #>
 239        function _EvalStatement([System.Management.Automation.Language.StatementAst] $s) {
 1240            if ($null -eq $s) { return $null }
 241
 1242            if ($s -is [System.Management.Automation.Language.CommandExpressionAst]) {
 0243                return _EvalExpr $s.Expression
 244            }
 245
 1246            if ($s -is [System.Management.Automation.Language.PipelineAst]) {
 1247                if ($s.PipelineElements.Count -eq 1 -and $s.PipelineElements[0] -is [System.Management.Automation.Langua
 1248                    return _EvalExpr $s.PipelineElements[0].Expression
 249                }
 250            }
 251
 0252            return $null
 253        }
 254
 255        <#
 256        .SYNOPSIS
 257            Evaluates a key or value part of a hashtable or key-value pair.
 258        .DESCRIPTION
 259            This function analyzes an AST node representing a key or value part
 260            to extract its value, if possible.
 261        .PARAMETER node
 262            The AST node to analyze.
 263        .OUTPUTS
 264            The extracted value if resolvable; otherwise, $null.
 265        #>
 266        function _EvalKeyValuePart($node) {
 1267            if ($null -eq $node) { return $null }
 268
 1269            if ($node -is [System.Management.Automation.Language.ExpressionAst]) {
 1270                return _EvalExpr $node
 271            }
 272
 1273            if ($node -is [System.Management.Automation.Language.StatementAst]) {
 1274                return _EvalStatement $node
 275            }
 276
 0277            return $null
 278        }
 279        <#
 280        .SYNOPSIS
 281            Evaluates an ExpressionAst to extract its value.
 282        .DESCRIPTION
 283            This function analyzes an ExpressionAst node to extract its value, if possible.
 284        .PARAMETER e
 285            The ExpressionAst node to analyze.
 286        .OUTPUTS
 287            The extracted value if resolvable; otherwise, $null.
 288        #>
 289        function _EvalExpr([System.Management.Automation.Language.ExpressionAst] $e) {
 1290            if ($null -eq $e) { return $null }
 291
 1292            switch ($e.GetType().FullName) {
 1293                'System.Management.Automation.Language.ConstantExpressionAst' { return $e.Value }
 1294                'System.Management.Automation.Language.StringConstantExpressionAst' { return $e.Value }
 0295                'System.Management.Automation.Language.ExpandableStringExpressionAst' { return $e.Value }
 0296                'System.Management.Automation.Language.TypeExpressionAst' { return $e.TypeName.FullName }
 297                'System.Management.Automation.Language.ParenthesisExpressionAst' {
 0298                    if ($e.Pipeline -and $e.Pipeline.PipelineElements.Count -eq 1 -and $e.Pipeline.PipelineElements[0] -
 0299                        return _EvalExpr $e.Pipeline.PipelineElements[0].Expression
 300                    }
 0301                    return $null
 302                }
 303                'System.Management.Automation.Language.ConvertExpressionAst' {
 304                    # e.g. [int]42 or [ordered]@{...}
 1305                    if ($e.Child -is [System.Management.Automation.Language.ExpressionAst]) {
 1306                        $childValue = _EvalExpr $e.Child
 3307                        if ($e.Type -and $e.Type.TypeName -and ($e.Type.TypeName.FullName -ieq 'ordered') -and ($e.Child
 308                            # Rebuild as an ordered dictionary
 1309                            $ordered = [ordered]@{}
 1310                            foreach ($kv in $e.Child.KeyValuePairs) {
 1311                                $k = _EvalKeyValuePart $kv.Item1
 1312                                $v = _EvalKeyValuePart $kv.Item2
 1313                                if ($null -eq $k) { return $null }
 1314                                $ordered[$k] = $v
 315                            }
 1316                            return $ordered
 317                        }
 318
 0319                        return $childValue
 320                    }
 0321                    return $null
 322                }
 323                'System.Management.Automation.Language.ArrayExpressionAst' {
 324                    # @( ... )
 1325                    if (-not $e.SubExpression) { return @() }
 1326                    $items = @()
 1327                    foreach ($st in $e.SubExpression.Statements) {
 1328                        $vv = _EvalStatement $st
 1329                        if ($null -eq $vv) { return $null }
 1330                        $items += $vv
 331                    }
 1332                    return , $items
 333                }
 334                'System.Management.Automation.Language.SubExpressionAst' {
 335                    # $( ... )
 0336                    $items = @()
 0337                    foreach ($st in $e.Statements) {
 0338                        $vv = _EvalStatement $st
 0339                        if ($null -eq $vv) { return $null }
 0340                        $items += $vv
 341                    }
 0342                    return , $items
 343                }
 344                'System.Management.Automation.Language.UnaryExpressionAst' {
 0345                    $v = $null
 0346                    if ($e.Child -is [System.Management.Automation.Language.ExpressionAst]) {
 0347                        $v = _EvalExpr $e.Child
 348                    }
 0349                    if ($e.TokenKind -eq [System.Management.Automation.Language.TokenKind]::Minus -and $v -is [ValueType
 0350                        try { return -1 * $v } catch { return $null }
 351                    }
 0352                    return $null
 353                }
 354                'System.Management.Automation.Language.ArrayLiteralAst' {
 1355                    $arr = @()
 1356                    foreach ($el in $e.Elements) {
 1357                        if ($el -isnot [System.Management.Automation.Language.ExpressionAst]) { return $null }
 1358                        $vv = _EvalExpr $el
 2359                        if ($null -eq $vv -and ($el -isnot [System.Management.Automation.Language.ConstantExpressionAst]
 1360                        $arr += $vv
 361                    }
 1362                    return , $arr
 363                }
 364                'System.Management.Automation.Language.HashtableAst' {
 1365                    $ht = @{}
 1366                    foreach ($kv in $e.KeyValuePairs) {
 1367                        $k = _EvalKeyValuePart $kv.Item1
 1368                        $v = _EvalKeyValuePart $kv.Item2
 1369                        if ($null -eq $k) { return $null }
 1370                        $ht[$k] = $v
 371                    }
 1372                    return $ht
 373                }
 0374                default { return $null }
 375            }
 376        }
 377
 1378        if ($expr -is [System.Management.Automation.Language.ExpressionAst]) {
 1379            return _EvalExpr $expr
 380        }
 381
 0382        return $null
 383    }
 384
 385    <#
 386    .SYNOPSIS
 387        Extracts type information from a declared type string.
 388    .DESCRIPTION
 389        This function analyzes a declared type string to determine the underlying type,
 390        whether it is nullable, and returns relevant metadata.
 391    .PARAMETER declaredType
 392        The declared type string to analyze.
 393    .OUTPUTS
 394        PSCustomObject with properties: Type, DeclaredType, IsNullable.
 395    #>
 396    function _GetTypeInfoFromDeclaredType([string] $declaredType) {
 1397        $underlyingName = $declaredType
 1398        $isNullable = $false
 399
 1400        if (-not [string]::IsNullOrWhiteSpace($declaredType)) {
 401            # Typical PowerShell nullable syntax: Nullable[datetime]
 1402            if ($declaredType -match '^(System\.)?Nullable(`1)?\[(?<inner>.+)\]$') {
 1403                $underlyingName = $Matches.inner
 1404                $isNullable = $true
 405            }
 406        }
 407
 1408        $resolvedType = $null
 1409        if (-not [string]::IsNullOrWhiteSpace($underlyingName)) {
 410            try {
 1411                $resolvedType = [System.Management.Automation.PSTypeName]::new($underlyingName).Type
 412            } catch {
 0413                $resolvedType = $null
 414            }
 415        }
 416
 1417        return [pscustomobject]@{
 1418            Type = $resolvedType
 1419            DeclaredType = $declaredType
 1420            IsNullable = $isNullable
 421        }
 422    }
 423
 424    # ---------- resolve $ScriptBlock source ----------
 1425    if ($FromParent.IsPresent) {
 1426        $allFrames = Get-PSCallStack
 427        # 0 = this function, 1 = immediate caller, 2+ = higher parents
 2428        $frames = $allFrames | Select-Object -Skip 1
 429
 1430        if ($ExcludeModules.Count) {
 2431            $frames = $frames | Where-Object {
 1432                $mn = $_.InvocationInfo.MyCommand.ModuleName
 3433                -not ($mn -and ($mn -in $ExcludeModules))
 434            }
 435        }
 436
 437        # pick the desired parent frame
 3438        $frame = $frames | Select-Object -Skip ($Up - 1) -First 1
 1439        if (-not $frame) { throw "No parent frame found (Up=$Up)." }
 440
 441        # Numeric scope depth for Get-Variable (0=this function, 1=caller, ...).
 442        # The frame index in Get-PSCallStack already matches the numeric scope depth.
 1443        $scopeUp = $allFrames.IndexOf($frame)
 1444        if ($scopeUp -lt 1) { throw 'Parent frame not found.' }
 445
 446        # prefer its live ScriptBlock; if null, rebuild from file
 1447        $ScriptBlock = $frame.InvocationInfo.MyCommand.ScriptBlock
 1448        if (-not $ScriptBlock -and $frame.ScriptName) {
 0449            $ScriptBlock = [scriptblock]::Create((Get-Content -Raw -LiteralPath $frame.ScriptName))
 450        }
 1451        if (-not $ScriptBlock) { throw 'Parent frame has no scriptblock or script file to parse.' }
 452    }
 453
 1454    if (-not $ScriptBlock) {
 0455        throw 'No scriptblock provided. Use -FromParent or pass a ScriptBlock.'
 456    }
 457    # Use the original script text so offsets match exactly
 1458    $scriptText = $ScriptBlock.Ast.Extent.Text
 459
 460    # Find the first *actual command* invocation named Enable-KrConfiguration
 1461    $enableCmd = $ScriptBlock.Ast.FindAll({
 462            param($node)
 463
 2464            if ($node -isnot [System.Management.Automation.Language.CommandAst]) { return $false }
 465
 1466            $name = $node.GetCommandName()
 2467            return $name -and ($name -ieq 'Enable-KrConfiguration')
 1468        }, $true) | Select-Object -First 1
 469
 1470    if ($enableCmd) {
 471        # Cut everything before that command
 0472        $pre = $scriptText.Substring(0, $enableCmd.Extent.StartOffset).TrimEnd()
 473
 474        # Preserve your brace-closing hack
 0475        if ($pre.TrimStart().StartsWith('{')) {
 0476            $pre += "`n}"
 477        }
 478
 0479        $ScriptBlock = [scriptblock]::Create($pre)
 480    }
 481
 482
 483
 484    <#
 485   .SYNOPSIS
 486       Checks if a given AST node is inside a function.
 487   .DESCRIPTION
 488       This function traverses the parent nodes of the given AST node to determine if it is
 489       located within a function definition.
 490    .PARAMETER node
 491       The AST node to check.
 492    .OUTPUTS
 493       [bool] Returns true if the node is inside a function, false otherwise.
 494   #>
 495    function _IsInFunction([System.Management.Automation.Language.Ast] $node) {
 1496        $p = $node.Parent
 1497        while ($p) {
 1498            if ($p -is [System.Management.Automation.Language.FunctionDefinitionAst]) { return $true }
 1499            if ($p -is [System.Management.Automation.Language.ScriptBlockAst]) { break }
 1500            $p = $p.Parent
 501        }
 1502        return $false
 503    }
 504
 505    <#
 506    .SYNOPSIS
 507         Checks if a given AST node is inside an assignment.
 508    .DESCRIPTION
 509         This function traverses the parent nodes of the given AST node to determine if it is
 510         located within an assignment statement.
 511    .PARAMETER node
 512         The AST node to check.
 513    .OUTPUTS
 514         [bool] Returns true if the node is inside an assignment, false otherwise.
 515    #>
 516    function _IsInAssignment([System.Management.Automation.Language.Ast] $node) {
 517        # IMPORTANT: AST parent chains can escape the scanned ScriptBlockAst
 518        # (e.g. when the scriptblock literal is part of an outer assignment).
 519        # We only care about assignments that occur *within* the scanned scriptblock.
 1520        $p = $node.Parent
 1521        while ($p) {
 2522            if ($p -is [System.Management.Automation.Language.AssignmentStatementAst]) { return $true }
 1523            if ($p -is [System.Management.Automation.Language.ScriptBlockAst]) { break }
 1524            $p = $p.Parent
 525        }
 1526        return $false
 527    }
 528
 529    <#
 530    .SYNOPSIS
 531        Checks if a variable expression is decorated with attributes.
 532    .DESCRIPTION
 533        PowerShell variable declarations/assignments may be wrapped in an AttributedExpressionAst
 534        when attributes are present, e.g. [ValidateNotNull()][int]$Foo = 1.
 535        This helper walks parent nodes to detect that wrapper.
 536    .PARAMETER node
 537        The AST node to inspect (typically the LHS or ConvertExpressionAst).
 538    .OUTPUTS
 539        [bool] Returns true if attributes are present; otherwise false.
 540    #>
 541    function _HasAttributes([System.Management.Automation.Language.Ast] $node) {
 1542        $p = $node
 1543        while ($p) {
 1544            if ($p -is [System.Management.Automation.Language.AttributedExpressionAst]) {
 545                # Note: AttributedExpressionAst stores a single attribute in the `Attribute` property.
 546                # Multiple attributes are represented as nested AttributedExpressionAst nodes.
 547                # Type constraints like [int] are represented as TypeConstraintAst and should NOT
 548                # count as "attributes" for -WithoutAttributesOnly filtering.
 1549                if ($p.Attribute -is [System.Management.Automation.Language.AttributeAst]) {
 1550                    return $true
 551                }
 552                # Otherwise, it's a type constraint (e.g. [int]); keep walking up because
 553                # there may be an outer AttributedExpressionAst with a real attribute.
 554            }
 1555            if ($p -is [System.Management.Automation.Language.ScriptBlockAst]) { break }
 1556            $p = $p.Parent
 557        }
 1558        return $false
 559    }
 560
 1561    $assignAsts = $ScriptBlock.Ast.FindAll(
 1562        { param($n) $n -is [System.Management.Automation.Language.AssignmentStatementAst] }, $true)
 563
 1564    foreach ($a in $assignAsts) {
 1565        $declaredType = $null
 1566        $typeInfo = $null
 1567        $hasAttributes = _HasAttributes $a.Left
 568
 569        # If assignment target is typed ([int]$x = 1), capture the declared type from the LHS.
 1570        $lhsConvert = $a.Left.Find(
 571            {
 572                param($n)
 1573                $n -is [System.Management.Automation.Language.ConvertExpressionAst] -and
 574                $n.Child -is [System.Management.Automation.Language.VariableExpressionAst]
 575            },
 576            $true
 1577        ) | Select-Object -First 1
 578
 1579        if ($lhsConvert -and $lhsConvert.Type -and $lhsConvert.Type.TypeName) {
 1580            $declaredType = $lhsConvert.Type.TypeName.FullName
 1581            $typeInfo = _GetTypeInfoFromDeclaredType -declaredType $declaredType
 582        }
 583
 1584        $varAst = $a.Left.Find(
 1585            { param($n) $n -is [System.Management.Automation.Language.VariableExpressionAst] }, $true
 1586        ) | Select-Object -First 1
 1587        if (-not $varAst) { continue }
 588
 1589        $info = _GetScopeAndNameFromVariablePath $varAst.VariablePath
 1590        if (-not $info) { continue }
 1591        if ($excludeSet.Contains($info.Name)) { continue }
 592
 1593        $val = _TryResolveValue -name $info.Name -scopeHint $info.ScopeHint
 1594        if ($null -eq $val) {
 595            # If runtime resolution fails (common when scanning caller scripts from a module),
 596            # fall back to reading simple constant initializers directly from the AST.
 1597            $val = _TryGetInitializerValueFromAssignment -assignmentAst $a
 598        }
 599
 1600        $type = $null
 1601        if ($null -ne $val) {
 1602            $type = $val.GetType()
 1603        } elseif ($typeInfo -and $null -ne $typeInfo.Type) {
 604            # If value is $null but LHS is typed, surface the underlying declared type.
 0605            $type = $typeInfo.Type
 606        }
 607
 1608        [void]$rows.Add([pscustomobject]@{
 1609                Name = $info.Name
 1610                ScopeHint = $info.ScopeHint
 1611                ProviderPath = $info.ProviderPath
 1612                Source = 'Assignment'
 1613                Operator = $a.Operator.ToString()
 1614                Type = $type
 3615                DeclaredType = if ($typeInfo) { $typeInfo.DeclaredType } else { $null }
 3616                IsNullable = if ($typeInfo) { $typeInfo.IsNullable } else { $null }
 1617                HasAttributes = $hasAttributes
 1618                Value = $val
 619            })
 620    }
 621
 622    # Also capture declaration-only typed variables like: [int]$x  (no assignment)
 623    # We scan ConvertExpressionAst directly to reliably catch both plain and attributed declarations.
 1624    $convertAsts = $ScriptBlock.Ast.FindAll(
 625        {
 626            param($n)
 1627            $n -is [System.Management.Automation.Language.ConvertExpressionAst] -and
 628            $n.Child -is [System.Management.Automation.Language.VariableExpressionAst]
 629        },
 630        $true
 631    )
 632
 1633    foreach ($c in $convertAsts) {
 634        # Skip casts that are part of assignments (those are already handled as assignments)
 1635        if (_IsInAssignment $c) {
 636            continue
 637        }
 638
 1639        $hasAttributes = _HasAttributes $c
 640
 1641        $varExpr = [System.Management.Automation.Language.VariableExpressionAst]$c.Child
 1642        $info = _GetScopeAndNameFromVariablePath $varExpr.VariablePath
 1643        if (-not $info) { continue }
 1644        if ($excludeSet.Contains($info.Name)) { continue }
 645
 1646        $declaredType = $c.Type.TypeName.FullName
 1647        $typeInfo = _GetTypeInfoFromDeclaredType -declaredType $declaredType
 1648        $val = _TryResolveValue -name $info.Name -scopeHint $info.ScopeHint
 649
 1650        [void]$rows.Add([pscustomobject]@{
 1651                Name = $info.Name
 1652                ScopeHint = $info.ScopeHint
 1653                ProviderPath = $info.ProviderPath
 1654                Source = 'Declaration'
 1655                Operator = $null
 1656                Type = $typeInfo.Type
 1657                DeclaredType = $typeInfo.DeclaredType
 1658                IsNullable = $typeInfo.IsNullable
 1659                HasAttributes = $hasAttributes
 1660                Value = $val
 661            })
 662    }
 663
 1664    if ($IncludeSetVariable) {
 1665        $cmdAsts = $ScriptBlock.Ast.FindAll(
 2666            { param($n) $n -is [System.Management.Automation.Language.CommandAst] -and -not (_IsInFunction $n) }, $true)
 667
 1668        foreach ($c in $cmdAsts) {
 1669            $cmd = $c.GetCommandName()
 1670            if ($cmd -notin 'Set-Variable', 'New-Variable') { continue }
 1671            $named = @{}
 672
 3673            for ($i = 0; $i -lt $c.CommandElements.Count; $i++) {
 1674                $e = $c.CommandElements[$i]
 1675                if ($e -isnot [System.Management.Automation.Language.CommandParameterAst]) { continue }
 1676                if ($e.ParameterName -notin 'Name', 'Scope') { continue }
 677
 1678                $argAst = $e.Argument
 2679                if (-not $argAst -and ($i + 1) -lt $c.CommandElements.Count) {
 1680                    $next = $c.CommandElements[$i + 1]
 1681                    if ($next -is [System.Management.Automation.Language.StringConstantExpressionAst]) {
 1682                        $argAst = $next
 1683                        $i++
 684                    }
 685                }
 686
 1687                if ($argAst -is [System.Management.Automation.Language.StringConstantExpressionAst]) {
 1688                    $named[$e.ParameterName] = $argAst.Value
 689                }
 690            }
 1691            if ($named.ContainsKey('Name')) {
 1692                $name = $named['Name']
 1693                if ($excludeSet.Contains($name)) { continue }
 2694                $scope = if ($named.ContainsKey('Scope') -and -not [string]::IsNullOrWhiteSpace($named['Scope'])) { $nam
 2695                $provider = "variable:$($scope):$name"
 2696                $val = $null; $type = $null
 1697                if ($ResolveValues) {
 698                    try {
 2699                        $val = (Get-Item -EA SilentlyContinue $provider).Value
 1700                        if ($null -ne $val) { $type = $val.GetType() }
 701                    } catch {
 0702                        Write-Warning "Failed to resolve variable '$name' in scope '$scope': $_"
 703                    }
 704                }
 1705                [pscustomobject]@{
 1706                    Name = $name
 1707                    ScopeHint = $scope
 1708                    ProviderPath = $provider
 1709                    Source = $cmd
 1710                    Operator = $null
 1711                    Type = $type
 1712                    DeclaredType = $null
 1713                    IsNullable = $null
 1714                    HasAttributes = $false
 1715                    Value = $val
 2716                } | ForEach-Object { [void]$rows.Add($_) }
 717            }
 718        }
 719    }
 720
 721    # keep last occurrence per (ScopeHint, Name)
 5722    $final = @($rows | Group-Object ScopeHint, Name | ForEach-Object { $_.Group[-1] })
 723
 1724    if ($WithoutAttributesOnly.IsPresent) {
 4725        $final = @($final | Where-Object { -not $_.HasAttributes })
 726    }
 1727    switch ($OutputStructure) {
 728        'Dictionary' {
 1729            $dict = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreC
 1730            foreach ($v in $final) {
 731                # Preserve declared type/nullable metadata for declaration-only (and typed-null) variables.
 732                # Typed variables (i.e., DeclaredType present) are wrapped so C# can see Type/IsNullable
 733                # even when the runtime value is non-null (e.g. [int]$paginationLimit = 20).
 734                # Untyped variables keep the old behavior (value only).
 1735                $wrap = -not [string]::IsNullOrWhiteSpace($v.DeclaredType)
 1736                if ($wrap) {
 1737                    $meta = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::Ordina
 1738                    $meta['__kestrunVariable'] = $true
 1739                    $meta['Value'] = $v.Value
 1740                    $meta['Type'] = $v.Type
 1741                    $meta['DeclaredType'] = $v.DeclaredType
 1742                    $meta['IsNullable'] = $v.IsNullable
 1743                    $dict[$v.Name] = $meta
 744                } else {
 1745                    $dict[$v.Name] = $v.Value
 746                }
 747            }
 1748            return $dict
 749        }
 750        'StringObjectMap' {
 1751            $dict = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreC
 1752            foreach ($v in $final) {
 1753                $dict[$v.Name] = $v.Value
 754            }
 1755            return $dict
 756        }
 757        default {
 1758            return $final
 759        }
 760    }
 761}