Public/Find-FunctionCall.ps1

function Find-FunctionCall
{
    <#
        .SYNOPSIS
        For a given function, find what functions it calls.

        .DESCRIPTION
        For the purposes of working out dependencies, it may be good to know what a function depends
        on at the function scale.

        This command takes a function and builds a tree of functions called by that function.

        .PARAMETER Name
        Provide the name of a function to analyse.

        .PARAMETER Function
        Provide a function object as input. This will be the output of Get-Command.

        .PARAMETER Depth
        Maximum level of nesting to analyse. If this depth is exceeded, a warning will be emitted.

        .PARAMETER ResolveAlias
        Specifies to resolve aliases to the aliased command.

        .PARAMETER All
        Specifies to return all commands. By default, built-in modules are excluded.

        .INPUTS

        [System.Management.Automation.FunctionInfo]

        .OUTPUTS

        [FunctionCallInfo]

        This command outputs an object similar to System.Management.Automation.FunctionInfo. Note
        that this is not a child class of FunctionInfo.

        .EXAMPLE
        Find-FunctionCall Install-Module

        CommandType Name Version Source
        ----------- ---- ------- ------
        Function Install-Module 2.2.5 PowerShellGet
        Function Get-ProviderName 2.2.5 PowerShellGet
        Function Get-PSRepository 2.2.5 PowerShellGet
        Function New-ModuleSourceFromPackageSource 2.2.5 PowerShellGet
        Cmdlet Get-PackageSource 1.4.7 PackageManagement
        Function Install-NuGetClientBinaries 2.2.5 PowerShellGet
        Function Get-ParametersHashtable 2.2.5 PowerShellGet
        Cmdlet Get-PackageProvider 1.4.7 PackageManagement
        Cmdlet Import-PackageProvider 1.4.7 PackageManagement
        Cmdlet Install-PackageProvider 1.4.7 PackageManagement
        Function Test-RunningAsElevated 2.2.5 PowerShellGet
        Function ThrowError 2.2.5 PowerShellGet
        Function New-PSGetItemInfo 2.2.5 PowerShellGet
        Function Get-EntityName 2.2.5 PowerShellGet
        Function Get-First 2.2.5 PowerShellGet
        Function Get-SourceLocation 2.2.5 PowerShellGet

        For the 'Install-Module' command from the PowerShellGet module, determine the call tree.

        .EXAMPLE
        Find-FunctionCall Import-Plugz -Depth 2 -ResolveAlias -All

        WARNING: Resulting output is truncated as call tree has exceeded the set depth of 2.
        CommandType Name Version Source
        ----------- ---- ------- ------
        Function Import-Plugz 0.2.0 Plugz
        Cmdlet Export-ModuleMember 7.2.5.500 Microsoft.PowerShell.Core
        Function Get-PlugzConfig 0.2.0 Plugz
        Cmdlet Add-Member 7.0.0.0 Microsoft.PowerShell.Utility
        Function Import-Configuration 1.5.1 Configuration
        Cmdlet Join-Path 7.0.0.0 Microsoft.PowerShell.Management
        Cmdlet New-Module 7.2.5.500 Microsoft.PowerShell.Core
        Cmdlet Select-Object 7.0.0.0 Microsoft.PowerShell.Utility
        Cmdlet Set-Alias 7.0.0.0 Microsoft.PowerShell.Utility
        Cmdlet Set-Item 7.0.0.0 Microsoft.PowerShell.Management
        Cmdlet Set-Variable 7.0.0.0 Microsoft.PowerShell.Utility
        Function Test-CalledFromProfile 0.2.0 Plugz
        Cmdlet Get-PSCallStack 7.0.0.0 Microsoft.PowerShell.Utility
        Cmdlet Select-Object 7.0.0.0 Microsoft.PowerShell.Utility
        Cmdlet Where-Object 7.2.5.500 Microsoft.PowerShell.Core
        Cmdlet Test-Path 7.0.0.0 Microsoft.PowerShell.Management
        Cmdlet Where-Object 7.2.5.500 Microsoft.PowerShell.Core
        Cmdlet Write-Error 7.0.0.0 Microsoft.PowerShell.Utility
        Cmdlet Write-Verbose 7.0.0.0 Microsoft.PowerShell.Utility

        Find calls made by the 'Import-Plugz' command. Depth is limited to 2. Built-in commands are
        included. Aliases are resolved to the resolved commands.
    #>


    [OutputType([FunctionCallInfo[]])]
    [CmdletBinding(DefaultParameterSetName = 'FromFunction', PositionalBinding = $false)]
    param
    (
        [Parameter(ParameterSetName = 'ByName', Mandatory, ValueFromPipeline, Position = 0)]
        [string]$Name,

        [Parameter(ParameterSetName = 'FromFunction', Mandatory, ValueFromPipeline, Position = 0)]
        [Management.Automation.FunctionInfo]$Function,

        [int]$Depth = 4,

        [switch]$ResolveAlias,

        [switch]$All,

        [Parameter(DontShow, ParameterSetName = 'Recursing', Mandatory, ValueFromPipeline)]
        [IFunctionCallInfo]$CallingFunction,

        [Parameter(DontShow, ParameterSetName = 'Recursing')]
        [int]$_CallDepth = 0,

        [Parameter(DontShow, ParameterSetName = 'Recursing')]
        [Collections.Generic.ISet[Management.Automation.FunctionInfo]]$_SeenFunctions = [Collections.Generic.HashSet[Management.Automation.FunctionInfo]]::new()
    )

    process
    {
        if ($_CallDepth -ge $Depth)
        {
            Write-Warning "Resulting output is truncated as call tree has exceeded the set depth of $Depth."
            return
        }


        if ($PSCmdlet.ParameterSetName -eq 'ByName')
        {
            $Function = Get-Command $Name -CommandType Function -ErrorAction Stop
        }

        if ($PSCmdlet.ParameterSetName -eq 'Recursing')
        {
            $Function = $CallingFunction.Command
        }
        else
        {
            $CallingFunction = [FunctionCallInfo]$Function
        }

        # Returns false if already in set
        if (-not $_SeenFunctions.Add($Function))
        {
            return
        }

        if (-not $_CallDepth)
        {
            $CallingFunction
        }


        #region Parse
        $Def = "function $($Function.Name) {$($Function.Definition)}"
        $Tokens = @()
        [void][Management.Automation.Language.Parser]::ParseInput($Def, [ref]$Tokens, [ref]$null)


        $CommandTokens = $Tokens | Where-Object {$_.TokenFlags -band 'CommandName'}
        $CalledCommandNames = $CommandTokens.Text | Sort-Object -Unique
        if (-not $CalledCommandNames)
        {
            return
        }
        #endregion Parse

        #region Resolve commands
        $Resolver = {
            param ([string[]]$CommandNames, [string]$ModuleName, [switch]$ResolveAlias)

            foreach ($CommandName in $CommandNames)
            {
                try
                {
                    $ResolvedCommand = Get-Command $CommandName -ErrorAction Stop

                    if ($ResolveAlias -and $ResolvedCommand.CommandType -eq 'Alias')
                    {
                        [FunctionCallInfo]$ResolvedCommand.ResolvedCommand
                    }
                    else
                    {
                        [FunctionCallInfo]$ResolvedCommand
                    }
                }
                catch [Management.Automation.CommandNotFoundException]
                {
                    [UnresolvedFunctionCallInfo]$CommandName

                    $_.ErrorDetails = "Command resolution failed for command '$CommandName'$(if ($ModuleName) {" in module '$ModuleName'"})."
                    Write-Error -ErrorRecord $_
                }
                catch
                {
                    Write-Error -ErrorRecord $_
                }
            }
        }

        [IFunctionCallInfo[]]$CalledCommands = if ($Function.Module)
        {
            $Function.Module.Invoke($Resolver, @($CalledCommandNames, $Function.Module.Name, $ResolveAlias))
        }
        else
        {
            & $Resolver $CalledCommandNames '' $ResolveAlias
        }


        if (-not $All)
        {
            $CalledCommands = $CalledCommands | Where-Object Source -notmatch '^Microsoft.PowerShell'
        }

        if (-not $CalledCommands)
        {
            return
        }
        #endregion Resolve commands

        #region Recurse
        $RecurseParams = [hashtable]$PSBoundParameters
        $RecurseParams.Remove('Name')
        $RecurseParams.Remove('Function')
        $RecurseParams.Remove('CallingFunction')
        $RecurseParams.Depth = $Depth
        $RecurseParams._CallDepth = ++$_CallDepth
        $RecurseParams._SeenFunctions = $_SeenFunctions

        $CalledCommands | ForEach-Object {
            $_.Depth = $_CallDepth
            $_.CalledBy = $CallingFunction

            # Recurse
            [IFunctionCallInfo[]]$CallsOfCalls = $_ |
                Where-Object CommandType -eq 'Function' |
                Find-FunctionCall @RecurseParams |
                Where-Object Name

            $_ | Write-Output

            if ($CallsOfCalls)
            {
                $_.Calls.AddRange($CallsOfCalls)

                $CallsOfCalls | Write-Output
            }
        }
        #endregion Recurse
    }
}