Get-ScriptReference.ps1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
#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
        }
    }
}