Get-ScriptReference.ps1
#requires -version 3.0 function Get-ScriptReference { <# .Synopsis Gets a script's references .Description Gets the external references of a given PowerShell command. These are the commands the script calls, and the types the script uses. .Example Get-Command Get-ScriptReference | Get-ScriptReference #> [CmdletBinding(DefaultParameterSetName='FilePath')] param( # The path to a file [Parameter(Mandatory=$true,Position=0,ParameterSetName='FilePath',ValueFromPipelineByPropertyName=$true)] [Alias('Fullname')] [string[]] $FilePath, # One or more PowerShell ScriptBlocks [Parameter(Mandatory=$true,Position=0,ParameterSetName='ScriptBlock',ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] [Alias('Definition')] [ScriptBlock[]] $ScriptBlock, # If set, will recursively find references. [switch] $Recurse ) begin { # Let's declare some collections we'll need: $allFiles = [Collections.ArrayList]::new() # * A list of all files (if any are piped in) $LookedUpCommands = @{} # * The commands we've already looked up (to save time) } process { #region Process Piped in Files if ($PSCmdlet.ParameterSetName -eq 'FilePath') { # If we're piping in files, $allFiles.AddRange($FilePath) # add them to the list and process them in the end, return # and stop processing for good measure. } #endregion Process Piped in Files #region Get the Script References # To start off with, take all of the scripts passed in and put them in a queue. $scriptBlockQueue = [Collections.Generic.Queue[ScriptBlock]]::new($ScriptBlock) $resolvedCmds = @{} # Then create a hashtable to store the resolved references $alreadyChecked = [Collections.Generic.List[ScriptBlock]]::new() # and a list of all of the ScriptBlock's we've already taken a look at. # Now it's time for some syntax trickery that should probably be explained. # We're going to want to be able to recursively find references too. # By putting this in a queue, we've already done part of the work, # because we can just enqueue the nested commands. # However, we also want to know _which nested command had which references_ # This means we have to collect all of the references as we go, # and output them in a different way if we're running recursively. # Got all that? # First, we need a tracking variable $CurrentCommand = '.' # for the current command. # Now the syntax trickery: We put the do loop inside of a lambda running in our scope (. {}). # This gives us all of our variables, but lets the results stream to the pipeline. # This is actually pretty important, since this way our tracking variable is accurate when we're outputting the results. # Now that we understand how it works, let's get to: #region Process the Queue of Script Blocks . { $alreadyChecked = [Collections.ArrayList]::new() do { $scriptBlock = $scriptBlockQueue.Dequeue() if ($alreadyChecked -contains $scriptBlock) { continue } $null= $alreadyChecked.Add($ScriptBlock) $foundRefs = $Scriptblock.Ast.FindAll({ param($ast) $ast -is [Management.Automation.Language.CommandAst] -or $ast -is [Management.Automation.Language.TypeConstraintAst] -or $ast -is [Management.Automation.Language.TypeExpressionAst] }, $true) $cmdRefs = [Collections.ArrayList]::new() $cmdStatements = [Collections.ArrayList]::new() $typeRefs = [Collections.ArrayList]::new() foreach ($ref in $foundRefs) { if ($ref -is [Management.Automation.Language.CommandAst]) { $null = $cmdStatements.Add($ref) if (-not $ref.CommandElements) { continue } $theCmd = $ref.CommandElements[0] if ($theCmd.Value) { if (-not $LookedUpCommands[$theCmd.Value]) { $LookedUpCommands[$thecmd.Value] = $ExecutionContext.InvokeCommand.GetCommand($theCmd.Value, 'Cmdlet, Function, Alias') } if ($cmdRefs -notcontains $LookedUpCommands[$theCmd.Value]) { $null = $cmdRefs.Add($LookedUpCommands[$thecmd.Value]) } } else { # referencing a lambda, leave it alone for now } } elseif ($ref.TypeName) { $refType = $ref.TypeName.Fullname -as [type] if ($typeRefs -notcontains $refType) { $null = $typeRefs.Add($refType) } } } [PSCustomObject][Ordered]@{ Commands = $cmdRefs.ToArray() Statements = $cmdStatements.ToArray() Types = $typeRefs.ToArray() } if ($Recurse) { $uniqueCmdRefs | & { process { if ($resolvedCmds.ContainsKey($_.Name)) { return } $nextScriptBlock = $_.ScriptBlock if (-not $nextScriptBlock -and $_.ResolvedCommand.ScriptBlock) { $nextScriptBlock = $_.ResolvedCommand.ScriptBlock } if ($nextScriptBlock) { $scriptBlockQueue.Enqueue($nextScriptBlock) $resolvedCmds[$_.Name] = $true } } } } } while ($ScriptBlockQueue.Count) } | #endregion Process the Queue of Script Blocks #region Handle Each Output & { begin { $refTable = @{} } process { if (-not $Recurse) { return $_ } } } #endregion Handle Each Output #endregion Get the Script References } end { $myParams = @{} + $PSBoundParameters if (-not $allFiles.Count) { return } $c, $t, $id = 0, $allFiles.Count, $(Get-Random) foreach ($file in $allFiles) { $c++ $resolvedFile= try { $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($file)} catch { $null } if (-not $resolvedFile) { continue } $resolvedFile = [IO.FileInfo]"$resolvedFile" if (-not $resolvedFile.Name) { continue } if (-not $resolvedFile.Length) { continue } if ('.ps1', '.psm1' -notcontains $resolvedFile.Extension) { continue } $p = $c * 100 / $t $text = [IO.File]::ReadAllText($resolvedFile.FullName) $scriptBlock= [ScriptBlock]::Create($text) Write-Progress "Getting References" " $($resolvedFile.Name) " -PercentComplete $p -Id $id if (-not $scriptBlock) { continue } Get-ScriptReference -ScriptBlock $scriptBlock | & { process { $_.psobject.properties.add([Management.Automation.PSNoteProperty]::new('FileName',$resolvedFile.Name)) $_.psobject.properties.add([Management.Automation.PSNoteProperty]::new('FilePath',$resolvedFile.Fullname)) $_.pstypenames.add('HelpOut.Script.Reference') $_ } } Write-Progress "Getting References" " " -Completed -Id $id } } } |