Private/GetInferredType.ps1
using namespace System.Reflection function GetInferredType { <# .SYNOPSIS Attempts to determine the type of a variable within a script file. .DESCRIPTION This function first attempts to infer type from command completion context. Failing that it will enumerate the scopes of any modules contained in the workspace as well as the global scope. If the variable is found in one of the scopes, the type of it's value will be returned. If the type cannot be inferred from command completion and the variable is defined in a function (or other child scope) this method will not work. The variable needs to be in a scope that exists at the time this function is ran. A workaround is to set a breakpoint right after the variable is defined. .INPUTS None .OUTPUTS System.Type Returns the inferred type if it was determined. This function does not have output otherwise. .EXAMPLE PS C:\> GetInferredType -Ast $memberExpressionAst.Expression Determines the type of the variable used in a member expression. #> [CmdletBinding()] [OutputType([type])] param( # Specifies the current context of the editor. [Parameter(Position=0)] [ValidateNotNullOrEmpty()] [Microsoft.PowerShell.EditorServices.Extensions.EditorContext] $Context = $psEditor.GetEditorContext(), # Specifies the ast to analyze. [Parameter(Position=1, Mandatory)] [ValidateNotNullOrEmpty()] [System.Management.Automation.Language.Ast] $Ast ) end { if ($Ast.InferredType -and $Ast.InferredType -ne ([object])) { return $Ast.InferredType } if ($Ast -is [System.Management.Automation.Language.TypeExpressionAst]) { return GetType -TypeName $Ast.TypeName } $PSCmdlet.WriteVerbose($Strings.InferringFromCompletion) $results = [System.Management.Automation.CommandCompletion]::CompleteInput( <# ast: #> $Context.CurrentFile.Ast, <# tokens: #> $Context.CurrentFile.Tokens, <# cursorPosition: #> $Ast.Extent.EndScriptPosition, <# options: #> $null ) $type = $results.CompletionMatches[0].ToolTip | Select-String '\[([\w\.]*)\]\$' | ForEach-Object { $PSItem.Matches.Groups[1].Value } | GetType if ($type -and $type.ToString() -match 'System.Collections' -or $type.IsArray) { $type = $null } if (-not $type -and $Ast -is [ExtendedMemberExpressionAst]) { $type = $Ast.InferredType } if (-not $type -and $Ast -is [System.Management.Automation.Language.MemberExpressionAst]) { $member = GetInferredMember $Ast $type = $member.ReturnType, $member.PropertyType, $member.FieldType | Where-Object { $_ } } # If it's a variable then check for it in scopes relevant to the current workspace. if (-not $type -and $Ast -is [System.Management.Automation.Language.VariableExpressionAst]) { $PSCmdlet.WriteVerbose($Strings.GettingImportedModules) $silent = @{ ErrorAction = 'Ignore' WarningAction = 'Ignore' } $workspaceModuleGuids = GetWorkspaceFile | Where-Object FullName -match '.psd1$' | Test-ModuleManifest @silent | Where-Object Guid -NotMatch '^[0-]*$' | ForEach-Object Guid $workspaceModules = Get-Module | Where-Object Guid -In $workspaceModuleGuids $internals = $workspaceModules | ForEach-Object { $PSItem.SessionState.GetType(). GetProperty('Internal', [BindingFlags]'Instance, NonPublic'). GetValue($PSItem.SessionState) } # If there are no modules in the workspace then grab the global scope. This isn't needed # otherwise because enumerating a module's scopes will hit global as well. if (-not $internals) { $PSCmdlet.WriteVerbose($Strings.CheckingDefaultScope) $internals = $ExecutionContext.SessionState.GetType(). GetProperty('Internal', [BindingFlags]'Instance, NonPublic'). GetValue($ExecutionContext.SessionState) } foreach ($internal in $internals) { $PSCmdlet.WriteVerbose($Strings.EnumeratingScopesForMember) $searcher = [ref].Assembly.GetType('System.Management.Automation.VariableScopeItemSearcher'). InvokeMember( <# name: #> '', <# invokeAttr: #> [BindingFlags]'CreateInstance, Instance, Public', <# binder: #> $null, <# target: #> $null, <# args: #> @( <# sessionState: #> $internal, <# lookupPath: #> $Ast.VariablePath, <# origin: #> [System.Management.Automation.CommandOrigin]::Runspace ) ) # Enumerate manually because enumerating normally sometimes causes an endless loop # with global variables. do { $match = $searcher.Current.Value } until ($match -or -not $searcher.MoveNext()) if ($match.Count -gt 1) { $match = $match[0] } if ($match) { $type = $match.GetType() $PSCmdlet.WriteVerbose($Strings.VariableFound -f $type) break } } } if (-not $type) { ThrowError -Exception ([InvalidOperationException]::new($Strings.CannotInferType -f $Ast)) ` -Id CannotInferType ` -Category InvalidOperation ` -Target $Ast } $type } } |