Private/Variable/Get-KrAssignedVariables.ps1

<#
    .SYNOPSIS
      Find variables that are *defined/assigned* in a scriptblock and (optionally) fetch their values.
    .DESCRIPTION
      Scans the AST for AssignmentStatementAst where the LHS is a VariableExpressionAst,
      and optionally Set-Variable/New-Variable calls. Can resolve current values from
      the appropriate scope (local/script/global) in the *current* runspace.
    .PARAMETER ScriptBlock
      ScriptBlock to scan. If omitted, use -Current to inspect the currently running block.
    .PARAMETER FromParent
      Inspect the *currently executing* scriptblock (via $MyInvocation.MyCommand.ScriptBlock).
    .PARAMETER IncludeSetVariable
      Also detect variables created via Set-Variable/New-Variable (-Name … [-Scope …]).
    .PARAMETER ResolveValues
      Resolve current values from variable:<scope>:<name> and include Type/Value.
    .PARAMETER DefaultScope
      Scope to assume when the LHS had no explicit scope (default: Local).
  #>

function Get-KrAssignedVariable {
    [CmdletBinding(DefaultParameterSetName = 'Given')]
    param(
        [Parameter(ParameterSetName = 'Given', Position = 0)]
        [scriptblock]$ScriptBlock,

        # NEW: use the caller's scriptblock (parent frame)
        [Parameter(ParameterSetName = 'FromParent', Mandatory)]
        [switch]$FromParent,

        # How many frames up to climb (1 = immediate caller)
        [Parameter(ParameterSetName = 'FromParent')]
        [int]$Up = 1,

        # Optional: skip frames from these modules when searching
        [Parameter(ParameterSetName = 'FromParent')]
        [string[]]$ExcludeModules = @('Kestrun'),

        [switch]$IncludeSetVariable,
        [switch]$ResolveValues,
        [ValidateSet('Local', 'Script', 'Global')]
        [string]$DefaultScope = 'Script'
    )

    # ---------- resolve $ScriptBlock source ----------
    if ($FromParent.IsPresent) {
        $allFrames = Get-PSCallStack
        # 0 = this function, 1 = immediate caller, 2+ = higher parents
        $frames = $allFrames | Select-Object -Skip 1

        if ($ExcludeModules.Count) {
            $frames = $frames | Where-Object {
                $mn = $_.InvocationInfo.MyCommand.ModuleName
                -not ($mn -and ($mn -in $ExcludeModules))
            }
        }

        # pick the desired parent frame
        $frame = $frames | Select-Object -Skip ($Up - 1) -First 1
        if (-not $frame) { throw "No parent frame found (Up=$Up)." }

        # Figure out how far “up” that is compared to the original call stack
        $scopeUp = ($allFrames.IndexOf($frame)) + 1
        if ($scopeUp -lt 1) { throw "Parent frame not found." }

        # prefer its live ScriptBlock; if null, rebuild from file
        $ScriptBlock = $frame.InvocationInfo.MyCommand.ScriptBlock
        if (-not $ScriptBlock -and $frame.ScriptName) {
            $ScriptBlock = [scriptblock]::Create((Get-Content -Raw -LiteralPath $frame.ScriptName))
        }
        if (-not $ScriptBlock) { throw "Parent frame has no scriptblock or script file to parse." }
    }

    if (-not $ScriptBlock) {
        throw "No scriptblock provided. Use -FromParent or pass a ScriptBlock."
    }
    $ast = ($ScriptBlock.Ast).ToString()

    $endstring = $ast.IndexOf("Enable-KrConfiguration", [StringComparison]::OrdinalIgnoreCase)
    if ($endstring -lt 0) {
        throw "The provided scriptblock does not appear to contain 'Enable-KrConfiguration' call."
    }
    $ast = $ast.Substring(0, $endstring).Trim()
    if ($ast.StartsWith('{')) {
        $ast += "`n}"
    }
    $ScriptBlock = [scriptblock]::Create($ast)


    <#
   .SYNOPSIS
       Checks if a given AST node is inside a function.
   .DESCRIPTION
       This function traverses the parent nodes of the given AST node to determine if it is
       located within a function definition.
    .PARAMETER node
       The AST node to check.
    .OUTPUTS
       [bool] Returns true if the node is inside a function, false otherwise.
   #>

    function _IsInFunction([System.Management.Automation.Language.Ast] $node) {
        $p = $node.Parent
        while ($p) {
            if ($p -is [System.Management.Automation.Language.FunctionDefinitionAst]) { return $true }
            $p = $p.Parent
        }
        return $false
    }

    $assignAsts = $ScriptBlock.Ast.FindAll(
        { param($n) $n -is [System.Management.Automation.Language.AssignmentStatementAst] }, $true)

    foreach ($a in $assignAsts) {
        $varAst = $a.Left.Find(
            { param($n) $n -is [System.Management.Automation.Language.VariableExpressionAst] }, $true
        ) | Select-Object -First 1
        if (-not $varAst) { continue }

        $vp = $varAst.VariablePath
        $name = $vp.UnqualifiedPath

        if (-not $name) { $name = $vp.UserPath }            # ← fallback
        if (-not $name) { continue }
        $name = $name -replace '^[^:]*:', ''
        if ($name.Contains('.') -or $name.Contains('[')) { continue }
        if ($name.StartsWith('{') -and $name.EndsWith('}')) {
            $name = $name.Substring(1, $name.Length - 2)        # ← ${foo} → foo
        }
        $val = Get-Variable -Name $name -Scope $scopeUp -ErrorAction SilentlyContinue

        [pscustomobject]@{
            Name = $name
            ScopeHint = $scope
            ProviderPath = $provider
            Source = 'Assignment'
            Operator = $a.Operator.ToString()
            Type = $type
            Value = $val
        }
    }

    if ($IncludeSetVariable) {
        $cmdAsts = $ScriptBlock.Ast.FindAll(
            { param($n) $n -is [System.Management.Automation.Language.CommandAst] -and -not (_IsInFunction $n) }, $true)

        foreach ($c in $cmdAsts) {
            $cmd = $c.GetCommandName()
            if ($cmd -notin 'Set-Variable', 'New-Variable') { continue }
            $named = @{}
            foreach ($e in $c.CommandElements) {
                if ($e -is [System.Management.Automation.Language.CommandParameterAst] -and $e.ParameterName -in 'Name', 'Scope') {
                    $arg = $e.Argument
                    if ($arg -is [System.Management.Automation.Language.StringConstantExpressionAst]) {
                        $named[$e.ParameterName] = $arg.Value
                    }
                }
            }
            if ($named.ContainsKey('Name')) {
                $name = $named['Name']
                $scope = $named['Scope'] ?? $DefaultScope
                $provider = "variable:$($scope):$name"
                $val = $null; $type = $null
                if ($ResolveValues) {
                    try {
                        $val = (Get-Item -EA SilentlyContinue $provider).Value
                        if ($null -ne $val) { $type = $val.GetType().FullName }
                    } catch {
                        Write-Warning "Failed to resolve variable '$name' in scope '$scope': $_"
                    }
                }
                [pscustomobject]@{
                    Name = $name
                    ScopeHint = $scope
                    ProviderPath = $provider
                    Source = $cmd
                    Operator = $null
                    Type = $type
                    Value = $val
                } | ForEach-Object { [void]$rows.Add($_) }
            }
        }
    }

    # keep last occurrence per (ScopeHint, Name)
    $rows | Group-Object ScopeHint, Name | ForEach-Object { $_.Group[-1] }
}