Skip to content

Commit e3e2486

Browse files
committed
Out-CallGraph
- Short-hand to full length alias translation working - Cronological order of commands in the code reflected in the output - Clean-ups. Comments and the like. - Make it possible to specify that debug commands should be excluded from the graph.
1 parent 0062b41 commit e3e2486

1 file changed

Lines changed: 77 additions & 36 deletions

File tree

Public/PowerShellInfo/Out-PSModuleCallGraph.ps1

Lines changed: 77 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
function Out-PSModuleCallGraph() {
33
<#
44
.DESCRIPTION
5-
Out-PSModuleCallGraph generates a call graph on a PowerShell module. The call graph is generated by parsing the public functions of the module being analyzed. Identifying
5+
Out-PSModuleCallGraph generates a call graph on a PowerShell module. The call graph is generated by parsing the public commands/functions of the module being analyzed. Identifying
66
the commands/functions each one utilizes. Noting their scope and the chronological order by which they where called. Finally the graph is generated with the PSGraph module and
77
saved in the format and on the media specified.
88
.INPUTS
@@ -11,18 +11,24 @@ function Out-PSModuleCallGraph() {
1111
.OUTPUTS
1212
A graphviz graph via the PSGraph module.
1313
.NOTES
14-
Pre-requisites:
15-
- The PSGraph module should already be installed.
14+
On the analysis:
15+
- The analysis is based on parsing the AST of each command/public function in the analyzed module. You therefore do not get a sequence like graph over certain calls made
16+
if 'x' path in the program was followed. Rather the generated call graph represents the code written, static as it is, when parsed line by line, as it is read-in from files
17+
on disk, to identify the sub-routines called and to be able to present an overview of how parts of the program is thought to interact/can interact with eachother.
1618
Other:
1719
- Call graphs can be very useful when working with your own PowerShell module or a PS module developed by external parties. A call graph gives you an overview over the calling
1820
relationships of commands/functions in a program. Thereby making it possible to get a good overview of a PowerShell module and they its sub-routines interact with eachother.
21+
Pre-requisites:
22+
- The PSGraph module should already be installed.
1923
.EXAMPLE
2024
Out-PSModuleCallGraph -ModuleName Pester
2125
This will generate a call graph on the Pester module.
2226
.EXAMPLE
2327
Out-PSModuleCallGraph -ModuleRoot ./PowerShellTooling/
2428
This will generate a call graph on a properly defined PowerShell module in the folder 'PowerShellTooling'. A sub-folder to current folder. Useful if the module is not installed
2529
in one of the default PowerShell module installation locations.
30+
.PARAMETER ExcludeDebugCommands
31+
Used to specify that you wish to exclude common debug commands such as > Write-Verbose & Write-Error.
2632
.PARAMETER ModuleName
2733
The name of the module to analyze. Assumes that the module is installed in one of the default PowerShell module installation locations.
2834
.PARAMETER ModuleRoot
@@ -35,14 +41,21 @@ function Out-PSModuleCallGraph() {
3541
[CmdletBinding(DefaultParameterSetName = "Default")]
3642
[OutputType([Void])]
3743
param(
44+
[Parameter()]
45+
[Parameter(ParameterSetName="ByModuleName")]
46+
[Parameter(ParameterSetName="ByModuleRoot")]
47+
[Switch]$ExcludeDebugCommands,
3848
[Parameter(Mandatory, ParameterSetName="ByModuleName")]
3949
[ValidateNotNullOrEmpty()]
4050
[String]$ModuleName,
4151
[Parameter(Mandatory, ParameterSetName="ByModuleRoot")]
4252
[ValidateNotNullOrEmpty()]
43-
[string]$ModuleRoot,
53+
[String]$ModuleRoot,
4454
[Parameter()]
45-
[string]$OutputPath
55+
[Parameter(ParameterSetName="ByModuleName")]
56+
[Parameter(ParameterSetName="ByModuleRoot")]
57+
[ValidateNotNullOrEmpty()]
58+
[String]$OutputPath
4659
)
4760

4861
#############
@@ -62,6 +75,19 @@ function Out-PSModuleCallGraph() {
6275

6376
# Collection to hold the call hierarchy of the analyzed module
6477
[System.Collections.ArrayList]$CallGraphObjects = New-Object System.Collections.ArrayList
78+
79+
# Prepare to exclude common debug commands
80+
if ($ExcludeDebugCommands) {
81+
$DebugCommandsToExclude = @('Write-Debug','Write-Error','Write-Verbose')
82+
} else {
83+
$DebugCommandsToExclude = @()
84+
}
85+
86+
# Short-hand values for commands to be translated to their fullname counterpart
87+
$FullNameCommands = @{
88+
"%" = "ForEach-Object"
89+
"?" = "Where-Object"
90+
}
6591
}
6692
Process {
6793
<#
@@ -115,35 +141,46 @@ function Out-PSModuleCallGraph() {
115141
$CommandsUsed = $ast.where( { $_.Type -eq "Command" } )
116142

117143
if ($null -ne $CommandsUsed) {
144+
# Ordered collection to hold the commands found in the command/function being analyzed
118145
[System.Collections.ArrayList]$CommandsUsedInfo = New-Object System.Collections.Specialized.OrderedDictionary
119-
foreach ($Command in $CommandsUsed) {
120-
# Control if it is a private function in the module
121-
$IsPrivateCommand = $PrivateFunctions.Contains($Command)
122-
if ($IsPrivateCommand) {
123-
[String]$CommandScope = "Private"
124-
}
125-
126-
# Control if it is a public function in the module
127-
$IsPublicCommand = $PublicFunctions.Contains($Command)
128-
if ($IsPublicCommand) {
129-
[String]$CommandScope = "Public"
130-
}
131146

132-
# COntrol if the command is defined in an external module
133-
if (-not $IsPrivateCommand -and -not $IsPublicCommand) {
134-
[String]$CommandScope = "External"
147+
foreach ($Command in $CommandsUsed) {
148+
# "Translate" command short-hands to their full-length counterpart.
149+
if ($FullNameCommands.Contains($Command.Content)) {
150+
[String]$CommandName = $FullNameCommands."$($Command.Content)"
151+
} else {
152+
[String]$CommandName = $Command.Content
135153
}
136154

137-
# Create a custom object to hold the info on the command analyzed
138-
$CommandInfo = @{
139-
"CommandName" = $Command.Content
140-
"CommandScope" = $CommandScope
141-
}
142-
$CommandInfoCustomObject = New-Object -TypeName PSCustomObject -Property $CommandInfo
143-
144-
# Add the command info to the collection
145-
$CommandsUsedInfo.Add($CommandInfoCustomObject) | Out-Null
146-
}
155+
if ($DebugCommandsToExclude.Count -eq 0 -or $DebugCommandsToExclude -notcontains $Command.Content) {
156+
# Control if it is a private function in the module
157+
$IsPrivateCommand = $PrivateFunctions.Contains($Command)
158+
if ($IsPrivateCommand) {
159+
[String]$CommandScope = "Private"
160+
}
161+
162+
# Control if it is a public function in the module
163+
$IsPublicCommand = $PublicFunctions.Contains($Command)
164+
if ($IsPublicCommand) {
165+
[String]$CommandScope = "Public"
166+
}
167+
168+
# COntrol if the command is defined in an external module
169+
if (-not $IsPrivateCommand -and -not $IsPublicCommand) {
170+
[String]$CommandScope = "External"
171+
}
172+
173+
# Create a custom object to hold the info on the command analyzed
174+
$CommandInfo = @{
175+
"CommandName" = $CommandName
176+
"CommandScope" = $CommandScope
177+
}
178+
$CommandInfoCustomObject = New-Object -TypeName PSCustomObject -Property $CommandInfo
179+
180+
# Add the command info to the collection
181+
$CommandsUsedInfo.Add($CommandInfoCustomObject) | Out-Null
182+
} # End of conditional on Exclude
183+
} # End of foreach on $DebugCommandsToExclude
147184

148185
# Add the analyzed info to the collection that holds all the aggregated info, derived by analyzing the public function/command currently being iterated over
149186
$PublicFunctionCommandHierarchy.Add(@{
@@ -160,19 +197,23 @@ function Out-PSModuleCallGraph() {
160197
- Generate data for the graph
161198
#>
162199
$graphData = Graph ModuleCallGraph {
200+
# Graph root node. To which all other nodes will be rooted.
163201
Node ProjectRoot -Attribute @{label="$($Module.Name)";shape='invhouse'}
164202

165-
#
203+
# Create nodes on the graph on all the analyzed data
166204
foreach ($CallGraphObject in $CallGraphObjects) {
167-
# Graph the root of the module and the direct descendants (the public functions)
205+
# "Attach" the public command/function to the root node
168206
Edge ProjectRoot, $CallGraphObject.PublicFunctionAffiliation
169207

170-
#
208+
# Control that the command/function actually used any other commands/functions
171209
if ($CallGraphObject.Commands.CommandsUsedInfo.Count -gt 0) {
172-
#Write-host "CallGraphObject Commands > $(gm -InputObject $CallGraphObject.Commands.CommandsUsedInfo.GetEnumerator() | Out-String)"
210+
# Counter used to annotate the nodes with the chronological order by which the command was called
211+
$CommandCounter = 1
212+
213+
# Create nodes for all the commands/functions the public command/function uses
173214
$CallGraphObject.Commands.GetEnumerator() | ForEach-Object {
174-
Write-Verbose -Message "Just the object > $_"
175-
Edge $CallGraphObject.PublicFunctionAffiliation, $_.CommandName
215+
Edge $CallGraphObject.PublicFunctionAffiliation, $_.CommandName -Attributes @{label=$CommandCounter}
216+
$CommandCounter++
176217
}
177218
}
178219
}

0 commit comments

Comments
 (0)