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.

        .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
        'Install-Module' | Get-Command | Find-FunctionCall

        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
        Function Install-NuGetClientBinaries 2.2.5 PowerShellGet
        Function Get-ParametersHashtable 2.2.5 PowerShellGet
        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
        Function Set-ModuleSourcesVariable 2.2.5 PowerShellGet
        Function DeSerialize-PSObject 2.2.5 PowerShellGet
        Function Get-PublishLocation 2.2.5 PowerShellGet
        Function Get-ScriptSourceLocation 2.2.5 PowerShellGet

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


    [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,

        [Parameter()]
        [int]$Depth = 4,

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

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

        [Parameter(DontShow, ParameterSetName = 'Recursing')]
        [Collections.Generic.HashSet[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.Function
        }
        else
        {
            $CallingFunction = [FunctionCallInfo]$Function
        }

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

        if (-not $_CallDepth)
        {
            $CallingFunction
        }

        $_CallDepth++


        $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
        }


        $CalledCommands = if ($Function.Module)
        {
            & $Function.Module {$args | Get-Command} $CalledCommandNames
        }
        else
        {
            Get-Command $CalledCommandNames
        }
        [FunctionCallInfo[]]$CalledFunctions = $CalledCommands | Where-Object CommandType -eq 'Function'

        if (-not $CalledFunctions)
        {
            return
        }


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

            # Recurse
            [FunctionCallInfo[]]$CallsOfCalls = $_ |
                Find-FunctionCall -Depth $Depth -_CallDepth $_CallDepth -_SeenFunctions $_SeenFunctions

            $_ | Write-Output

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

                $CallsOfCalls | Write-Output
            }
        }
    }
}