FindFunctionCalls.psm1
#region Classes class IFunctionCallInfo { <# .SYNOPSIS A pseudo-child of System.Management.Automation.FunctionInfo that's also a tree node. We can't inherit because all the constructors of FunctionInfo are marked internal. #> # Hot path - we'll implement directly [string]$Name [string]$Source [psmoduleinfo]$Module # This class is a tree node [IFunctionCallInfo]$CalledBy [System.Collections.Generic.IList[IFunctionCallInfo]]$Calls [int]$Depth # Inner object; we'll delegate calls to this hidden [Management.Automation.CommandInfo]$Command IFunctionCallInfo() { if (-not (Get-PSCallStack).FunctionName -match '^(Unresolved)?FunctionCallInfo$') { throw [Management.Automation.MethodException]::new("Cannot instantiate interface 'IFunctionCallInfo'.") } $this.Calls = [Collections.Generic.List[IFunctionCallInfo]]::new() $InheritedProperties = ( 'CmdletBinding', 'CommandType', 'DefaultParameterSet', 'Definition', 'Description', 'HelpFile', # 'Module', 'ModuleName', # 'Name', 'Noun', 'Options', 'OutputType', 'Parameters', 'ParameterSets', 'RemotingCapability', 'ScriptBlock', # 'Source', 'Verb', 'Version', 'Visibility', 'HelpUri' ) $InheritedProperties | ForEach-Object { Add-Member ScriptProperty -InputObject $this -Name $_ -Value ([scriptblock]::Create("`$this.Command.$_")) } } [Management.Automation.ParameterMetadata] ResolveParameter([string]$name) { throw [InvalidOperationException]::new("Cannot resolve parameter for unresolved comand '$Name'.") } } class FunctionCallInfo : IFunctionCallInfo { FunctionCallInfo([Management.Automation.CommandInfo]$Command) : base() { $this.Command = $Command $this.Name = $Command.Name $this.Source = $Command.Source $this.Module = $Command.Module } [int] GetHashCode() { return $this.Command.GetHashCode() } [bool] Equals([object]$obj) { return $this.Command.Equals($obj.Command) } [Management.Automation.ParameterMetadata] ResolveParameter([string]$name) { return $this.Command.ResolveParameter($name) } [string] ToString() { return $this.Command.ToString() } } class UnresolvedFunctionCallInfo : IFunctionCallInfo { UnresolvedFunctionCallInfo([string]$Name) : base() { $this.Name = $Name } } #endregion Classes #region Public 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 $CallingFunction.Calls.Add($_) [IFunctionCallInfo[]]$CallsOfCalls = $_ | Where-Object CommandType -eq 'Function' | Find-FunctionCall @RecurseParams | Where-Object Name $_ | Write-Output $CallsOfCalls | Write-Output } #endregion Recurse } } #endregion Public |