EditorAstProvider.psm1
using namespace System.Collections.Generic using namespace System.IO using namespace System.Management.Automation using namespace System.Management.Automation.Language using namespace Microsoft.PowerShell.EditorServices.Extensions using namespace Microsoft.PowerShell.SHiPS Import-LocalizedData -BindingVariable Strings -FileName Strings -ErrorAction Ignore # Make-shift property accessor class AstItemCodeMethods { static [string] AstTypeCodeProperty([psobject] $instance) { if (-not $instance.Ast) { return 'None' } return $instance.Ast.GetType().Name } } class AstTypeParameter { [Parameter()] [ValidateSet( 'ErrorStatementAst', 'ErrorExpressionAst', 'ScriptBlockAst', 'ParamBlockAst', 'NamedBlockAst', 'NamedAttributeArgumentAst', 'AttributeBaseAst', 'AttributeAst', 'TypeConstraintAst', 'ParameterAst', 'StatementBlockAst', 'StatementAst', 'TypeDefinitionAst', 'UsingStatementAst', 'MemberAst', 'PropertyMemberAst', 'FunctionMemberAst', 'FunctionDefinitionAst', 'IfStatementAst', 'DataStatementAst', 'LabeledStatementAst', 'LoopStatementAst', 'ForEachStatementAst', 'ForStatementAst', 'DoWhileStatementAst', 'DoUntilStatementAst', 'WhileStatementAst', 'SwitchStatementAst', 'CatchClauseAst', 'TryStatementAst', 'TrapStatementAst', 'BreakStatementAst', 'ContinueStatementAst', 'ReturnStatementAst', 'ExitStatementAst', 'ThrowStatementAst', 'PipelineBaseAst', 'PipelineAst', 'CommandElementAst', 'CommandParameterAst', 'CommandBaseAst', 'CommandAst', 'CommandExpressionAst', 'RedirectionAst', 'MergingRedirectionAst', 'FileRedirectionAst', 'AssignmentStatementAst', 'ConfigurationDefinitionAst', 'DynamicKeywordStatementAst', 'ExpressionAst', 'BinaryExpressionAst', 'UnaryExpressionAst', 'BlockStatementAst', 'AttributedExpressionAst', 'ConvertExpressionAst', 'MemberExpressionAst', 'InvokeMemberExpressionAst', 'BaseCtorInvokeMemberExpressionAst', 'TypeExpressionAst', 'VariableExpressionAst', 'ConstantExpressionAst', 'StringConstantExpressionAst', 'ExpandableStringExpressionAst', 'ScriptBlockExpressionAst', 'ArrayLiteralAst', 'HashtableAst', 'ArrayExpressionAst', 'ParenExpressionAst', 'SubExpressionAst', 'UsingExpressionAst', 'IndexExpressionAst')] [string] $AstType } class AstContainerBase : SHiPSDirectory { static [hashtable] $TypeMap = @{ [ScriptBlockAst] = [ScriptBlockAstItem] [TypeDefinitionAst] = [TypeDefinitionAstItem] [FunctionMemberAst] = [FunctionMemberAstItem] [PropertyMemberAst] = [PropertyMemberAstItem] [AssignmentStatementAst] = [AssignmentStatementAstItem] [FunctionDefinitionAst] = [FunctionDefinitionAstItem] [CommandAst] = [CommandAstItem] [ParameterAst] = [ParameterAstItem] [NamedBlockAst] = [NamedBlockAstItem] [BinaryExpressionAst] = [BinaryExpressionAstItem] [UnaryExpressionAst] = [UnaryExpressionAstItem] [AttributedExpressionAst] = [AttributedExpressionAstItem] [ConvertExpressionAst] = [ConvertExpressionAstItem] [MemberExpressionAst] = [MemberExpressionAstItem] [InvokeMemberExpressionAst] = [InvokeMemberExpressionAstItem] [BaseCtorInvokeMemberExpressionAst] = [InvokeMemberExpressionAstItem] [IndexExpressionAst] = [IndexExpressionAstItem] [NamedAttributeArgumentAst] = [NamedAttributeArgumentAstItem] [AttributeAst] = [AttributeAstItem] [DataStatementAst] = [DataStatementAstItem] [ForEachStatementAst] = [ForEachStatementAstItem] [ForStatementAst] = [ForStatementAstItem] [DoWhileStatementAst] = [LoopStatementAstItem] [DoUntilStatementAst] = [LoopStatementAstItem] [WhileStatementAst] = [LoopStatementAstItem] [TryStatementAst] = [TryStatementAstItem] [ConfigurationDefinitionAst] = [ConfigurationDefinitionAstItem] [UsingStatementAst] = [UsingStatementAstItem] [SwitchStatementAst] = [SwitchStatementAstItem] [IfStatementAst] = [IfStatementAstItem] [HashtableAst] = [HashtableAstItem] [TypeConstraintAst] = [TypeConstraintAstItem] [ConstantExpressionAst] = [ConstantExpressionAstItem] [StringConstantExpressionAst] = [StringConstantExpressionAstItem] [CommandParameterAst] = [CommandParameterAstItem] [VariableExpressionAst] = [VariableExpressionAstItem] }; [Ast] $Ast; [IScriptExtent] $Extent; AstContainerBase() : base($this.GetType().Name) { } AstContainerBase([string] $name) : base($name) { } static [SHiPSBase] CreateAstItem([Ast] $item) { $astType = $item.GetType() if ($itemType = [AstContainerBase]::TypeMap[$astType]) { return $itemType::new($item) } if ($itemType = [SinglePropertyItem]::PropertyMap[$astType]) { return [SinglePropertyItem]::new($item, $itemType) } return [AstContainer]::new($item) } hidden static [void] AssertCommandsModuleLoaded() { if ([CurrentFileAst]::IsCommandsLoaded) { return } $commandsModule = Get-Module PowerShellEditorServices.Commands -ErrorAction Ignore if ($commandsModule) { [CurrentFileAst]::IsCommandsLoaded = $true return } if ($path = [CurrentFileAst]::CommandsModulePath) { Import-Module $path -Global [CurrentFileAst]::IsCommandsLoaded = $true return } $exception = [PSInvalidOperationException]::new($script:Strings.CannotLoadPSESCommands) throw [ErrorRecord]::new( <# exception: #> $exception, <# errorId: #> 'CannotLoadPSESCommands', <# errorCategory: #> 'ObjectNotFound', <# targetObject: #> $null) } [object[]] GetChildItem() { [AstContainerBase]::AssertCommandsModuleLoaded() $nameCounts = [Dictionary[string, int]]::new() return $this.GetRawChildItem().ForEach{ if (-not $PSItem) { return } if ([string]::IsNullOrEmpty($PSItem.Name)) { if ($PSItem.Ast) { $PSItem.Name = $PSItem.Ast.GetType().Name } else { $PSItem.Name = $PSItem.GetType().Name } } $name = ($PSItem.Name. Split([Path]::InvalidPathChars) -join '') -replace '[\[\]]' if (-not $nameCounts.TryGetValue($name, [ref]$null)) { $nameCounts.Add($name, 1) $PSItem.Name = $name return $PSItem } $nameCounts[$name]++ $PSItem.Name = $name + '-' + $nameCounts[$name] return $PSItem } } hidden [object[]] GetRawChildItem() { $astType = ('System.Management.Automation.Language.' + $this.ProviderContext.DynamicParameters.AstType) -as [type] return $this. GetChildItemImpl(). ForEach{ if ($PSItem -is [Ast]) { return [AstContainerBase]::CreateAstItem($PSItem) } return $PSItem }. Where{ -not $astType -or $PSItem.Ast -is $astType } } <# abstract #> [object[]] GetChildItemImpl() { throw [NotImplementedException]::new() } [object] GetChildItemDynamicParameters() { return [AstTypeParameter]::new() } [string] ToString() { return $this.Ast.ToString() } } class AstContainer : AstContainerBase { AstContainer ([Ast] $ast) : base($ast.ForEach('GetType').Name) { $this.Ast = $ast $this.Extent = $ast.Extent } AstContainer ([Ast] $ast, [string] $name) : base($name) { $this.Ast = $ast $this.Extent = $ast.Extent } [object[]] GetChildItemImpl() { return $this. Ast. FindAll({ $true }, $false). Where({ $PSItem -ne $this.Ast }, 'SkipUntil') } [object] GetChildItemDynamicParameters() { return [AstTypeParameter]::new() } } class SinglePropertyItem : AstContainer { hidden static [hashtable] $PropertyMap = @{ [PipelineAst] = 'PipelineElements' [ParamBlockAst] = 'Parameters' [StatementBlockAst] = 'Statements' [ScriptBlockExpressionAst] = 'ScriptBlock' [CommandExpressionAst] = 'Expression' [ExpandableStringExpressionAst] = 'NestedExpressions' [ArrayLiteralAst] = 'Elements' [ArrayExpressionAst] = 'SubExpression' [ParenExpressionAst] = 'Pipeline' [SubExpressionAst] = 'SubExpression' [UsingExpressionAst] = 'SubExpression' [ThrowStatementAst] = 'Pipeline' [ReturnStatementAst] = 'Pipeline' [ExitStatementAst] = 'Pipeline' [DynamicKeywordStatementAst] = 'CommandElements' [CatchClauseAst] = 'Body' } hidden [string] $ReturnPropertyName; SinglePropertyItem([Ast] $ast, [string] $returnProperty) : base($ast) { if ([string]::IsNullOrEmpty($returnProperty)) { $returnProperty = [SinglePropertyItem]::PropertyMap[$ast.GetType()] } $this.ReturnPropertyName = $returnProperty } [object[]] GetChildItemImpl() { return $this.Ast.($this.ReturnPropertyName) } } class CurrentFileAst : AstContainerBase { hidden static [string] $GetEditorObjectEventName = 'EditorAstProvider.GetEditorObject' hidden static [EditorObject] $EditorObject; hidden static [string] $CommandsModulePath; hidden static [bool] $IsCommandsLoaded; CurrentFileAst() : base($this.GetType()) { $this.Init() } CurrentFileAst([string] $name) : base($name) { $this.Init() } hidden [void] Init() { if ([CurrentFileAst]::EditorObject -and [CurrentFileAst]::CommandsModulePath) { return } # The provider runs in a different runspace, so we need to get the # editor object (psEditor) and Commands module path from the main # runspace. $rs = Get-Runspace 2 $rs.Events.SubscribeEvent( <# source: #> $null, <# eventName: #> [CurrentFileAst]::GetEditorObjectEventName, <# sourceIdentifier: #> [CurrentFileAst]::GetEditorObjectEventName, <# data: #> $null, <# scriptblock: #> { $sender::EditorObject = Get-Variable psEditor -Scope Global -ValueOnly $commandsModule = Get-Module PowerShellEditorServices.Commands $joinPathSplat = @{ Path = $commandsModule.ModuleBase ChildPath = 'PowerShellEditorServices.Commands.psd1' } $sender::CommandsModulePath = Join-Path @joinPathSplat }, <# supportEvent: #> $true, <# forwardEvent: #> $false, <# maxTriggerCount: #> 1) $rs.Events.GenerateEvent( <# sourceIdentifier: #> [CurrentFileAst]::GetEditorObjectEventName, <# sender: #> $this.GetType(), <# args: #> @(), <# extraData: #> $null, <# processInCurrentThread: #> $false, <# waitForCompletionInCurrentThread: #> $true) } [object[]] GetChildItemImpl() { if (-not [CurrentFileAst]::EditorObject) { return @() } Set-Variable psEditor -Scope Global -Value ([CurrentFileAst]::EditorObject) return [ScriptBlockAstItem]::new( [CurrentFileAst]::EditorObject.GetEditorContext().CurrentFile.Ast, 'Root') } } class ScriptBlockAstItem : AstContainer { hidden static [string[]] $NamedBlockKinds = ( 'DynamicParamBlock', 'BeginBlock', 'ProcessBlock', 'EndBlock') ScriptBlockAstItem([Ast] $ast) : base($ast) { } ScriptBlockAstItem([Ast] $ast, [string] $name) : base($ast, $name) { } [object[]] GetChildItemImpl() { return . { [ScriptBlockAst] $sbAst = $this.Ast # yield $sbAst.ParamBlock foreach ($blockKind in [ScriptBlockAstItem]::NamedBlockKinds) { if (-not $sbAst.$blockKind) { continue } # yield [NamedBlockAstItem]::new( $sbAst.$blockKind, $blockKind) } } } } class TypeDefinitionAstItem : AstContainer { TypeDefinitionAstItem([TypeDefinitionAst] $ast) : base($ast, $ast.Name) { } [object[]] GetChildItemImpl() { return $this.Ast.Members } } class FunctionMemberAstItem : AstContainer { FunctionMemberAstItem([FunctionMemberAst] $ast) : base($ast, $ast.Name + '(' + $ast.Parameters.Count + ')') { } [object[]] GetChildItemImpl() { return . { $this.Ast.ReturnType $this.Ast.Parameters [ScriptBlockAstItem]::new($this.Ast.Body, 'Body') } } } class PropertyMemberAstItem : AstContainer { PropertyMemberAstItem([PropertyMemberAst] $ast) : base($ast, $ast.Name) { } [object[]] GetChildItemImpl() { return . { $this.Ast.PropertyType $this.Ast.Attributes $this.Ast.InitialValue } } } class AssignmentStatementAstItem : AstContainer { AssignmentStatementAstItem([AssignmentStatementAst] $ast) : base($ast) { } [object[]] GetChildItemImpl() { return $this.Ast.Left, $this.Ast.Right } } class FunctionDefinitionAstItem : AstContainer { FunctionDefinitionAstItem([FunctionDefinitionAst] $ast) : base($ast, $ast.Name) { } [object[]] GetChildItemImpl() { return [ScriptBlockAstItem]::new($this.Ast.Body, 'Body') } } class CommandAstItem : AstContainer { CommandAstItem([CommandAst] $ast) : base($ast, $ast.GetCommandName()) { } [object[]] GetChildItemImpl() { return $this.Ast.CommandElements } } class ParameterAstItem : AstContainer { ParameterAstItem([ParameterAst] $ast) : base($ast, $ast.Name.VariablePath.UserPath) { } [object[]] GetChildItemImpl() { return . { $this.Ast.Attributes $this.Ast.Name $this.Ast.DefaultValue } } } class NamedBlockAstItem : AstContainer { [TokenKind] $BlockKind; [bool] $IsUnnamed; NamedBlockAstItem([Ast] $ast, [string] $name) : base($ast, $name) { $this.BlockKind = $ast.BlockKind $this.IsUnnamed = $ast.Unnamed } [object[]] GetChildItemImpl() { return $this.Ast.Statements } } class BinaryExpressionAstItem : AstContainer { [TokenKind] $Operator; BinaryExpressionAstItem([BinaryExpressionAst] $ast) : base($ast) { $this.Operator = $ast.Operator } [object[]] GetChildItemImpl() { return $this.Ast.Left, $this.Ast.Right } } class UnaryExpressionAstItem : AstContainer { [TokenKind] $TokenKind; UnaryExpressionAstItem([UnaryExpressionAst] $ast) : base($ast) { $this.TokenKind = $ast.TokenKind } [object[]] GetChildItemImpl() { return $this.Ast.Child } } class AttributedExpressionAstItem : AstContainer { AttributedExpressionAstItem([AttributedExpressionAst] $ast) : base($ast) { } [object[]] GetChildItemImpl() { return $this.Ast.Attribute, $this.Ast.Child } } class ConvertExpressionAstItem : AstContainer { ConvertExpressionAstItem([ConvertExpressionAst] $ast) : base($ast) { } [object[]] GetChildItemImpl() { return $this.Ast.Type, $this.Ast.Attribute, $this.Ast.Child } } class MemberExpressionAstItem : AstContainer { [bool] $IsStatic; MemberExpressionAstItem([MemberExpressionAst] $ast) : base($ast) { $this.IsStatic = $true } [object[]] GetChildItemImpl() { return $this.Ast.Expression, $this.Ast.Member } } class InvokeMemberExpressionAstItem : MemberExpressionAstItem { InvokeMemberExpressionAstItem([InvokeMemberExpressionAst] $ast) : base($ast) { } [object[]] GetChildItemImpl() { return . { $this.Ast.Expression $this.Ast.Member $this.Ast.Arguments } } } class IndexExpressionAstItem : AstContainer { IndexExpressionAstItem([IndexExpressionAst] $ast) : base($ast) { } [object[]] GetChildItemImpl() { return $this.Ast.Target, $this.Ast.Index } } class NamedAttributeArgumentAstItem : AstContainer { [bool] $ExpressionOmitted; NamedAttributeArgumentAstItem([NamedAttributeArgumentAst] $ast) : base($ast, $ast.ArgumentName) { $this.ExpressionOmitted = $ast.ExpressionOmitted } [object[]] GetChildItemImpl() { return $this.Ast.Argument } } class AttributeAstItem : AstContainer { AttributeAstItem([AttributeAst] $ast) : base($ast, $ast.TypeName.Name) { } [object[]] GetChildItemImpl() { return . { $this.Ast.PositionalArguments $this.Ast.NamedArguments } } } class DataStatementAstItem : AstContainer { DataStatementAstItem([DataStatementAst] $ast) : base($ast, $ast.Variable) { } [object[]] GetChildItemImpl() { return . { $this.Ast.CommandsAllowed $this.Ast.Body } } } class ForEachStatementAstItem : AstContainer { ForEachStatementAstItem([ForEachStatementAst] $ast) : base($ast) { } [object[]] GetChildItemImpl() { return $this.Ast.Variable, $this.Ast.Condition, $this.Ast.ThrottleLimit, $this.Ast.Body } } class ForStatementAstItem : AstContainer { ForStatementAstItem([ForStatementAst] $ast) : base($ast) { } [object[]] GetChildItemImpl() { return $this.Ast.Initializer, $this.Ast.Iterator, $this.Ast.Body, $this.Ast.Condition } } class LoopStatementAstItem : AstContainer { LoopStatementAstItem([LoopStatementAst] $ast) : base($ast) { } [object[]] GetChildItemImpl() { return $this.Ast.Body, $this.Ast.Condition } } class TryStatementAstItem : AstContainer { TryStatementAstItem([TryStatementAst] $ast) : base($ast) { } [object[]] GetChildItemImpl() { return . { $this.Ast.Body $this.Ast.CatchClauses $this.Ast.Finally } } } class ConfigurationDefinitionAstItem : AstContainer { ConfigurationDefinitionAstItem([ConfigurationDefinitionAst] $ast) : base($ast, $ast.InstanceName) { } [object[]] GetChildItemImpl() { return $this.Ast.InstanceName, $this.Ast.Body } } class UsingStatementAstItem : AstContainer { [UsingStatementKind] $UsingStatementKind; UsingStatementAstItem([UsingStatementAst] $ast) : base($ast) { $this.UsingStatementKind = $ast.UsingStatementKind } [object[]] GetChildItemImpl() { return $this.Ast.Name, $this.Ast.Alias, $this.Ast.ModuleSpecification } } class SwitchStatementAstItem : AstContainer { SwitchStatementAstItem([SwitchStatementAst] $ast) : base($ast) { } [object[]] GetChildItemImpl() { return . { $this.Ast.Condition $this.Ast.Clauses.ForEach{ # yield [IfStatementClauseItem]::new($PSItem, [TokenKind]::Switch) } if ($this.Ast.Default) { # yield [IfStatementClauseItem]::new( [Tuple[ExpressionAst, StatementBlockAst]]::new( $null, $this.Ast.Default), [TokenKind]::Else) } } } } class IfStatementAstItem : AstContainer { IfStatementAstItem([IfStatementAst] $ast) : base ($ast) { } [object[]] GetChildItemImpl() { return . { $first, $rest = $this.Ast.Clauses.Where({ $true }, 'Split', 1) # yield [IfStatementClauseItem]::new($first[0], [TokenKind]::If) $rest.ForEach{ # yield [IfStatementClauseItem]::new($PSItem, [TokenKind]::ElseIf) } if ($this.Ast.ElseClause) { # yield [IfStatementClauseItem]::new( [Tuple[PipelineBaseAst, StatementBlockAst]]::new( $null, $this.Ast.ElseClause), [TokenKind]::Else) } } } } class HashtableAstItem : AstContainer { HashtableAstItem([HashtableAst] $ast) : base($ast) { } [object[]] GetChildItemImpl() { return $this.Ast.KeyValuePairs.ForEach{ [HashtableKeyValuePairItem]::new($PSItem) } } } # The next two classes aren't based on an actual AST, they are AST pairs # stored in a tuple class IfStatementClauseItem : AstContainer { hidden [Tuple[PipelineBaseAst, StatementBlockAst]] $Clause; [TokenKind] $ClauseKind; IfStatementClauseItem( [Tuple[PipelineBaseAst, StatementBlockAst]] $clause, [TokenKind] $clauseKind) : base($null, $clauseKind) { $this.Clause = $clause $this.ClauseKind = $clauseKind $this.Extent = $clause.Item1.Extent, $clause.Item2.Extent | Join-ScriptExtent } [object[]] GetChildItemImpl() { return $this.Clause.Item1, $this.Clause.Item2 } } class HashtableKeyValuePairItem : AstContainer { hidden [Tuple[ExpressionAst, StatementAst]] $KeyValuePair; HashtableKeyValuePairItem([Tuple[ExpressionAst, StatementAst]] $keyValuePair) : base($null, $keyValuePair.Item1.ToString()) { $this.KeyValuePair = $keyValuePair $this.Extent = $keyValuePair.Item1.Extent, $keyValuePair.Item2.Extent | Join-ScriptExtent } [object[]] GetChildItemImpl() { return $this.KeyValuePair.Item1, $this.KeyValuePair.Item2 } } class AstLeaf : SHiPSLeaf { [Ast] $Ast; [IScriptExtent] $Extent; AstLeaf([Ast] $ast) : base($ast.ForEach('GetType').Name) { $this.Ast = $ast $this.Extent = $ast.Extent } AstLeaf ([Ast] $ast, [string] $name) : base($name) { $this.Ast = $ast $this.Extent = $ast.Extent } [string] ToString() { return $this.Ast.ToString() } } class TypeConstraintAstItem : AstLeaf { [ITypeName] $TypeName; TypeConstraintAstItem([TypeConstraintAst] $ast) : base($ast, $ast.TypeName.Name) { $this.TypeName = $ast.TypeName } } class ConstantExpressionAstItem : AstLeaf { [type] $StaticType; [object] $Value; ConstantExpressionAstItem([ConstantExpressionAst] $ast) : base($ast) { $this.StaticType = $ast.StaticType $this.Value = $ast.Value } } class StringConstantExpressionAstItem : ConstantExpressionAstItem { [string] $Value; [StringConstantType] $StringConstantType; StringConstantExpressionAstItem([StringConstantExpressionAst] $ast) : base($ast) { $this.StringConstantType = $ast.StringConstantType } } class CommandParameterAstItem : AstLeaf { CommandParameterAstItem([CommandParameterAst] $ast) : base($ast, $ast.ParameterName) { } } class VariableExpressionAstItem : AstLeaf { [bool] $Splatted; [type] $StaticType; [VariablePath] $VariablePath; VariableExpressionAstItem([VariableExpressionAst] $ast) : base($ast, $ast.VariablePath.UserPath) { $this.Splatted = $ast.Splatted $this.StaticType = $ast.StaticType $this.VariablePath = $ast.VariablePath } } function New-EditorAstPSDrive { <# .EXTERNALHELP EditorAstProvider-help.xml #> [CmdletBinding()] param() end { $updateTypeDataSplat = @{ MemberType = 'CodeProperty' MemberName = 'AstType' Value = ([AstItemCodeMethods].GetMethod('AstTypeCodeProperty')) Force = $true } Update-TypeData @updateTypeDataSplat -TypeName AstContainerBase Update-TypeData @updateTypeDataSplat -TypeName AstLeaf $newPSDriveSplat = @{ Root = 'EditorAstProvider#CurrentFileAst' PSProvider = 'SHiPS' Name = 'CurrentFile' Scope = 'Global' } New-PSDrive @newPSDriveSplat } } Export-ModuleMember -Function New-EditorAstPSDrive |