ShowPSAst.psm1
# # .SYNOPSIS # # Provides a graphical interface to explore PowerShell AST. # # .EXAMPLE # # PS> $ast = { if (Test-Path $profile) { echo "Profile exists" } }.Ast # PS> Show-Ast $ast # function Show-Ast { param( ## The object to examine [Parameter(ValueFromPipeline = $true)] $InputObject ) process { Set-StrictMode -Version 3 Add-Type -Assembly System.Windows.Forms $font = New-Object System.Drawing.Font ("Consolas", 12.0) # This a helper function to recursively walk the tree # and add all children to the given node list. function AddChildNode($child, $nodeList) { # A function to add an object to the display tree function PopulateNode($object, $nodeList) { foreach ($child in $object.PSObject.Properties) { # Skip the Parent node, it's not useful here if ($child.Name -eq 'Parent') { continue } $childObject = $child.Value if ($null -eq $childObject) { continue } # Recursively add only Ast nodes. if ($childObject -is [System.Management.Automation.Language.Ast]) { AddChildNode $childObject $nodeList continue } # Several Ast properties are collections of Ast, add them all # as children of the current node. $collection = $childObject -as [System.Management.Automation.Language.Ast[]] if ($collection -ne $null) { for ($i = 0; $i -lt $collection.Length; $i++) { AddChildNode ($collection[$i]) $nodeList } continue } # A little hack for IfStatementAst and SwitchStatementAst - they have a collection # of tuples of Ast. Both items in the tuple are an Ast, so we want to recurse on both. if ($childObject.GetType().FullName -match 'ReadOnlyCollection.*Tuple`2.*Ast.*Ast') { for ($i = 0; $i -lt $childObject.Count; $i++) { AddChildNode ($childObject[$i].Item1) $nodeList AddChildNode ($childObject[$i].Item2) $nodeList } continue } } } # Create the new node to add with the node text of the item type and extent $childNode = [Windows.Forms.TreeNode]@{ Text = $child.GetType().Name + (" [{0},{1})" -f $child.Extent.StartOffset,$child.Extent.EndOffset) Tag = $child } $null = $nodeList.Add($childNode) # Recursively add the current nodes children PopulateNode $child $childNode.Nodes # We want the tree fully expanded after construction $childNode.Expand() } # A function invoked when a node in the tree view is selected. function OnAfterSelect { param($Sender, $TreeViewEventArgs) $dataView.Rows.Clear() $selectedObject = $TreeViewEventArgs.Node.Tag foreach ($property in $selectedObject.PSObject.Properties) { $typeName = [Microsoft.PowerShell.ToStringCodeMethods]::Type([type]$property.TypeNameOfValue) if ($typeName -match '.*ReadOnlyCollection\[(.*)\]') { # Lie about the type to make the display shorter $typeName = $matches[1] + '[]' } # Remove the namespace $typeName = $typeName -replace '.*\.','' $value = $property.Value if ($typeName -eq 'IScriptExtent') { $file = if ($value.File -eq $null) { "" } else { Split-Path -Leaf $value.File } $value = "{0} ({1},{2})-({3},{4})" -f $file, $value.StartLineNumber, $value.StartColumnNumber, $value.EndLineNumber, $value.EndColumnNumber } $dataView.Rows.Add($property.Name, $value, $typeName) } # If the text box has changed, skip doing anything with it until we've updated the tree view. if (!$script:BufferIsDirty) { $startOffset = $selectedObject.Extent.StartOffset - $script:inputObjectStartOffset $endOffset = $selectedObject.Extent.EndOffset - $script:inputObjectStartOffset $scriptView.SelectionStart = $startOffset $scriptView.SelectionLength = $endOffset - $startOffset $scriptView.ScrollToCaret() } } # A function when the text box has focus - so we can refresh the Ast # when asked (by pressing F5). function OnTextBoxKeyUp { param($Sender, $KeyEventArgs) if ($KeyEventArgs.KeyCode -eq 'F5' -and $KeyEventArgs.Alt -eq $false -and $KeyEventArgs.Control -eq $false -and $KeyEventArgs.Shift -eq $false) { $KeyEventArgs.Handled = $true $Ast = [System.Management.Automation.Language.Parser]::ParseInput($scriptView.Text, [ref]$null, [ref]$null) $script:BufferIsDirty = $false $treeView.Nodes.Clear() AddChildNode $Ast $treeView.Nodes $script:inputObjectStartOffset = 0 } } # Create the TreeView for the Ast $treeView = [Windows.Forms.TreeView]@{ Location = [System.Drawing.Point]@{X = 12; Y = 12} Size = [System.Drawing.Size]@{Width = 600; Height = 400} Font = $font TabIndex = 0; PathSeparator = "." } $treeView.Add_AfterSelect( { OnAfterSelect @args } ) # Create the root node for the Ast if ($InputObject -is [scriptblock]) { $InputObject = $InputObject.Ast } elseif ($InputObject -is [System.Management.Automation.FunctionInfo] -or $InputObject -is [System.Management.Automation.ExternalScriptInfo]) { $InputObject = $InputObject.ScriptBlock.Ast } elseif ($InputObject -isnot [System.Management.Automation.Language.Ast]) { $text = [string]$InputObject if (Test-Path -LiteralPath $text) { $path = Resolve-Path $text $InputObject = [System.Management.Automation.Language.Parser]::ParseFile($path.ProviderPath, [ref]$null, [ref]$null) } else { $InputObject = [System.Management.Automation.Language.Parser]::ParseInput($text, [ref]$null, [ref]$null) } } AddChildNode $InputObject $treeView.Nodes # Data view shows properties of the selected Ast in table form $dataView = [Windows.Forms.DataGridView]@{ AllowUserToAddRows = $false AllowUserToDeleteRows = $false AllowUserToResizeRows = $false AutoSizeColumnsMode = [System.Windows.Forms.DataGridViewAutoSizeColumnsMode]::Fill AutoSizeRowsMode = [System.Windows.Forms.DataGridViewAutoSizeRowsMode]::AllCells ColumnHeadersHeightSizeMode = [System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode]::AutoSize ColumnHeadersVisible = $true Font = $font Location = [System.Drawing.Point]@{X = 12; Y = 424} ReadOnly = $true; RowHeadersVisible = $false SelectionMode = [System.Windows.Forms.DataGridViewSelectionMode]::FullRowSelect Size = [System.Drawing.Size]@{Width = 600; Height = 256} TabIndex = 1 } $dataView.Columns.AddRange( [System.Windows.Forms.DataGridViewTextBoxColumn]@{ HeaderText = 'Property' ReadOnly = $true AutoSizeMode = [System.Windows.Forms.DataGridViewAutoSizeColumnMode]::AllCellsExceptHeader}, [System.Windows.Forms.DataGridViewTextBoxColumn]@{ HeaderText = 'Value' ReadOnly = $true Resizable = [System.Windows.Forms.DataGridViewTriState]::True AutoSizeMode = [System.Windows.Forms.DataGridViewAutoSizeColumnMode]::Fill}, [System.Windows.Forms.DataGridViewTextBoxColumn]@{ HeaderText = 'Type' ReadOnly = $true AutoSizeMode = [System.Windows.Forms.DataGridViewAutoSizeColumnMode]::AllCellsExceptHeader }) # The script view is a text box that displays the text of the script. # If the text box has not been edited, selecting an ast in the tree view # will select the matching text in the script view. $scriptView = [System.Windows.Forms.TextBox]@{ Font = $font HideSelection = $false Location = [System.Drawing.Point]@{X = 624; Y = 12} Multiline = $true ScrollBars = 'Both' Size = [System.Drawing.Size]@{Width = 561; Height = 668} TabIndex = 2 Text = $InputObject.Extent.Text WordWrap = $false } $script:BufferIsDirty = $false $scriptView.Add_TextChanged({ $script:BufferIsDirty = $true }) $scriptView.Add_KeyUp({ OnTextBoxKeyUp @args }) $script:inputObjectStartOffset = $InputObject.Extent.StartOffset try { # Create the main form and show it. $form = [Windows.Forms.Form]@{ Text = "Ast Explorer" ClientSize = [System.Drawing.Size]@{Width = 1200; Height = 700} } $form.Controls.Add($dataView) $form.Controls.Add($treeView) $form.Controls.Add($scriptView) $null = $form.ShowDialog() } finally { $form.Dispose() } } } |