Public/New-PowerShellCallGraph.ps1
|
function New-PowerShellCallGraph { <# .SYNOPSIS Creates a call graph from the results of Find-PowerShellSymbol. .DESCRIPTION Takes the output from Find-PowerShellSymbol and builds a call graph by associating function usages with their callers. Returns a PSObject with Nodes (unique functions) and Edges (caller-callee relationships). .PARAMETER Results The array of PSObjects returned by Find-PowerShellSymbol. .EXAMPLE $results = Find-PowerShellSymbol -Path .\*.ps1 $graph = New-PowerShellCallGraph -Results $results $graph.Nodes # List of functions $graph.Edges # List of [PSCustomObject]@{Caller='funcA'; Callee='funcB'} .EXAMPLE $results = Find-PowerShellSymbol -Path .\*.ps1 $graph = New-PowerShellCallGraph -Results $results $graph | Convert-PowerShellCallGraphToMermaid #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline)] [PSObject[]]$Results ) begin { $nodes = @() $edges = @() $fileCache = @{} # Cache parsed ASTs per file } process { foreach ($result in $Results) { if ($result.Type -eq 'FunctionDefinition') { $nodes += $result.Name } elseif ($result.Type -eq 'FunctionUsage') { $caller = Get-CallerFromUsage -Result $result -FileCache $fileCache if ($caller -and $result.Name) { $edges += [PSCustomObject]@{ Caller = $caller Callee = $result.Name File = $result.File Line = $result.LineNumber } } } } } end { $nodes = $nodes | Sort-Object -Unique [PSCustomObject]@{ Nodes = $nodes Edges = $edges } } } function Get-CallerFromUsage { param( [PSObject]$Result, [hashtable]$FileCache ) $file = $Result.File if (-not $FileCache.ContainsKey($file)) { if (-not (Test-Path $file)) { return $null } try { $content = Get-Content $file -Raw -ErrorAction Stop $ast = [System.Management.Automation.Language.Parser]::ParseInput($content, [ref]$null, [ref]$null) $FileCache[$file] = $ast } catch { return $null } } $ast = $FileCache[$file] $lineNum = $Result.LineNumber # Find the CommandAst at the line number $command = $ast.Find({ param($node) $node -is [System.Management.Automation.Language.CommandAst] -and $node.Extent.StartLineNumber -eq $lineNum }, $true) | Select-Object -First 1 if (-not $command) { return $null } # Traverse up to find the containing function $current = $command while ($current) { if ($current -is [System.Management.Automation.Language.FunctionDefinitionAst]) { return $current.Name } $current = $current.Parent } # If no function found, it's a top-level call return '<Global>' } |