PowerShell/test/powershell/engine/Api/TypeInference.Tests.ps1

1248 lines
48 KiB
PowerShell

# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
using namespace System.Management.Automation
using namespace System.Collections.Generic
Describe "Type inference Tests" -tags "CI" {
BeforeAll {
$ati = [Cmdlet].Assembly.GetType("System.Management.Automation.AstTypeInference")
$inferType = $ati.GetMethods().Where{$_.Name -ceq "InferTypeOf"}
$m1 = 'System.Collections.Generic.IList`1[System.Management.Automation.PSTypeName] InferTypeOf(System.Management.Automation.Language.Ast)'
$m2 = 'System.Collections.Generic.IList`1[System.Management.Automation.PSTypeName] InferTypeOf(System.Management.Automation.Language.Ast, System.Management.Automation.TypeInferenceRuntimePermissions)'
$m3 = 'System.Collections.Generic.IList`1[System.Management.Automation.PSTypeName] InferTypeOf(System.Management.Automation.Language.Ast, System.Management.Automation.PowerShell)'
$m4 = 'System.Collections.Generic.IList`1[System.Management.Automation.PSTypeName] InferTypeOf(System.Management.Automation.Language.Ast, System.Management.Automation.PowerShell, System.Management.Automation.TypeInferenceRuntimePermissions)'
$inferTypeOf1 = $inferType.Where{$m1 -eq $_}[0]
$inferTypeOf2 = $inferType.Where{$m2 -eq $_}[0]
$inferTypeOf3 = $inferType.Where{$m3 -eq $_}[0]
$inferTypeOf4 = $inferType.Where{$m4 -eq $_}[0]
class AstTypeInference {
static [IList[PSTypeName]] InferTypeOf([Language.Ast] $ast) {
return $script:inferTypeOf1.Invoke($null, $ast)
}
static [IList[PSTypeName]] InferTypeOf([Language.Ast] $ast, [System.Management.Automation.TypeInferenceRuntimePermissions] $runtimePermissions) {
return $script:inferTypeOf2.Invoke($null, @($ast, $runtimePermissions))
}
static [IList[PSTypeName]] InferTypeOf([Language.Ast] $ast, [System.Management.Automation.PowerShell] $powershell) {
return $script:inferTypeOf3.Invoke($null, @($ast, $powershell))
}
static [IList[PSTypeName]] InferTypeOf([Language.Ast] $ast, [PowerShell] $powerShell, [System.Management.Automation.TypeInferenceRuntimePermissions] $runtimePermissions) {
return $script:inferTypeOf4.Invoke($null, @($ast, $powerShell, $runtimePermissions))
}
}
}
It "Infers type from integer" {
$res = [AstTypeInference]::InferTypeOf( { 1 }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.Int32'
}
It "Infers type from string literal" {
$res = [AstTypeInference]::InferTypeOf( { "Text" }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.String'
}
It "Infers type from type expression" {
$res = [AstTypeInference]::InferTypeOf( { [int] }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.Type'
}
It "Infers type from hashtable" {
$res = [AstTypeInference]::InferTypeOf( { @{} }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.Collections.Hashtable'
}
It "Infers type from array expression" {
$res = [AstTypeInference]::InferTypeOf( { @() }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.object[]'
}
It "Infers type from array expression with a single statement" {
$res = [AstTypeInference]::InferTypeOf( { @('test') }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.String[]'
}
It "Infers type from array expression with multiple statements" {
$res = [AstTypeInference]::InferTypeOf( { @('test'; 'second test') }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.String[]'
}
It "Infers type from array expression with mixed types" {
$res = [AstTypeInference]::InferTypeOf( { @('test'; 1) }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.Object[]'
}
It "Infers type from array expression with nested arrays" {
$res = [AstTypeInference]::InferTypeOf( { @(@('test'); @('test2')) }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.String[]'
}
It "Infers type from array expression with a non-generic dictionary enumerator" {
$res = [AstTypeInference]::InferTypeOf( { @(@{}.GetEnumerator()) }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.Collections.DictionaryEntry[]'
}
It "Infers type from array expression with a generic dictionary enumerator" {
$res = [AstTypeInference]::InferTypeOf( { @([Dictionary[int, string]]::new().GetEnumerator()) }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be ([KeyValuePair[int, string][]].FullName)
}
It "Infers type from array expression with nested non-array collections" {
$res = [AstTypeInference]::InferTypeOf( {
$list = [List[string]]::new()
$list2 = [List[string]]::new()
@($list; $list2)
}.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.String[]'
}
It "Infers type from Array literal" {
$res = [AstTypeInference]::InferTypeOf( { , 1 }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.Int32[]'
}
It "Infers type from Array literal with multiple elements" {
$res = [AstTypeInference]::InferTypeOf( { 0, 1 }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.Int32[]'
}
It "Infers type from Array literal with mixed types" {
$res = [AstTypeInference]::InferTypeOf( { 'test', 1 }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.Object[]'
}
It "Infers type from Array literal with nested arrays" {
$res = [AstTypeInference]::InferTypeOf( { @('test'), @('test2') }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.String[]'
}
It "Infers type from array expression with a non-generic dictionary enumerator" {
$res = [AstTypeInference]::InferTypeOf( { , @{}.GetEnumerator() }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.Collections.DictionaryEntry[]'
}
It "Infers type from array expression with a generic dictionary enumerator" {
$res = [AstTypeInference]::InferTypeOf( { , [Dictionary[int, string]]::new().GetEnumerator() }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be ([KeyValuePair[int, string][]].FullName)
}
It "Infers type from Array literal with nested non-array collections" {
$res = [AstTypeInference]::InferTypeOf( {
$list = [List[string]]::new()
$list2 = [List[string]]::new()
$list, $list2
}.Ast.EndBlock.Statements[2].PipelineElements[0].Expression)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.String[]'
}
It "Infers type from array IndexExpresssion" {
$res = [AstTypeInference]::InferTypeOf( { (1, 2, 3)[0] }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.Int32'
}
It "Infers type from generic container IndexExpression" {
$res = [AstTypeInference]::InferTypeOf( {
[System.Collections.Generic.List[int]]::new()[0]
}.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.Int32'
}
It 'Infers type of Index expression on Dictionary' {
$ast = {
[System.Collections.Generic.Dictionary[int, DateTime]]::new()[1]
}.ast.EndBlock.Statements[0].PipelineElements[0].Expression
$res = [AstTypeInference]::InferTypeOf( $ast )
$res.Count | Should -Be 1
$res.Name | Should -BeExactly 'System.DateTime'
}
It "Infers type from ScriptblockExpresssion" {
$res = [AstTypeInference]::InferTypeOf( { {} }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.Management.Automation.Scriptblock'
}
It "Infers type from paren expression" {
$res = [AstTypeInference]::InferTypeOf( { (1) }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.Int32'
}
It "Infers type from expandable string expression" {
$res = [AstTypeInference]::InferTypeOf( { "$(1)" }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.String'
}
It "Infers type from cast expression" {
$res = [AstTypeInference]::InferTypeOf( { [int] '1'}.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.Int32'
}
It "Infers type from using namespace" {
$errors = $null
$tokens = $null
$ast = [Language.Parser]::ParseInput("using namespace System", [ref] $tokens, [ref] $errors)
$res = [AstTypeInference]::InferTypeOf( $ast.Find( {param($a) $a -is [System.Management.Automation.Language.UsingStatementAst] }, $true))
$res.Count | Should -Be 0
}
It "Infers type from unary expression" {
$res = [AstTypeInference]::InferTypeOf( { !$true }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.Boolean'
}
It "Infers type from param block" {
$res = [AstTypeInference]::InferTypeOf( { param() }.Ast)
$res.Count | Should -Be 0
}
It "Infers type from using statement" {
$res = [AstTypeInference]::InferTypeOf( { $int = 1; $using:int }.Ast.EndBlock.Statements[1].PipelineElements[0].Expression)
$res.Count | Should -Be 1
$res.Name | Should -Be System.Int32
}
It "Infers type from param block" {
$res = [AstTypeInference]::InferTypeOf( { param([int] $i)}.Ast.ParamBlock)
$res.Count | Should -Be 0
}
It "Infers type no type from Attribute" {
$res = [AstTypeInference]::InferTypeOf( {
[OutputType([int])]
param(
)}.Ast.ParamBlock.Attributes[0])
$res.Count | Should -Be 0
}
It "Infers type no type from named Attribute argument" {
$res = [AstTypeInference]::InferTypeOf( {
[OutputType(Type = [int])]
param(
)}.Ast.ParamBlock.Attributes[0].NamedArguments[0])
$res.Count | Should -Be 0
}
It "Infers type parameter types" {
$res = [AstTypeInference]::InferTypeOf( {
param([int] $i, [string] $s)
}.Ast.ParamBlock.Parameters[0])
$res.Count | Should -Be 1
$res.Name | Should -Be System.Int32
}
It "Infers type parameter from PSTypeNameAttribute type" -Skip:(!$IsWindows) {
$res = [AstTypeInference]::InferTypeOf( {
param([int] $i, [PSTypeName('System.Management.ManagementObject#root\cimv2\Win32_Process')] $s)
}.Ast.ParamBlock.Parameters[1])
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.Management.ManagementObject#root\cimv2\Win32_Process'
}
It "Infers type from DATA statement" {
$res = [AstTypeInference]::InferTypeOf( {
DATA {
"text"
}
}.Ast.EndBlock)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.String'
}
It "Infers type from named block" {
$res = [AstTypeInference]::InferTypeOf( { begin {1}}.Ast.BeginBlock)
$res.Count | Should -Be 1
$res.Name | Should -Be System.Int32
}
It "Infers type from function definition" {
$res = [AstTypeInference]::InferTypeOf( {
function foo {
return 1
}
}.Ast.EndBlock)
$res.Count | Should -Be 0
}
It "Infers type from convert expression" {
$errors = $null
$tokens = $null
$ast = [Language.Parser]::ParseInput('[int] "4"', [ref] $tokens, [ref] $errors)
$res = [AstTypeInference]::InferTypeOf( $ast.EndBlock.Statements[0])
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.Int32'
}
It "Infers type from type constraint" {
$errors = $null
$tokens = $null
$ast = [Language.Parser]::ParseInput('[int] $i', [ref] $tokens, [ref] $errors)
$res = [AstTypeInference]::InferTypeOf( $ast.EndBlock.Statements[0].PipelineElements[0].Expression.Attribute)
$res.Count | Should -Be 0
}
It "Infers type from instance member property" {
$res = [AstTypeInference]::InferTypeOf( { 'Text'.Length }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.Int32'
}
It "Infers type from static member property" {
$res = [AstTypeInference]::InferTypeOf( { [DateTime]::Now }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.DateTime'
}
It "Infers type from instance member method" {
$res = [AstTypeInference]::InferTypeOf( { [int[]].GetElementType() }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.Type'
}
It "Infers type from static member method" {
$res = [AstTypeInference]::InferTypeOf( { [powershell]::Create() }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.Management.Automation.PowerShell'
}
It "Infers type from integer * stringliteral" {
$res = [AstTypeInference]::InferTypeOf( { 5 * "5" }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.Int32'
}
It "Infers type from string literal" {
$res = [AstTypeInference]::InferTypeOf( { "Text" }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.String'
}
It "Infers type from stringliteral * integer" {
$res = [AstTypeInference]::InferTypeOf( { "5" * 2 }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.String'
}
It "Infers type from where-object of integer" {
$res = [AstTypeInference]::InferTypeOf( { [int[]] $i = 1..20; $i | Where-Object {$_ -gt 10} }.Ast)
foreach ($r in $res) {
$r.Name -In 'System.Int32', 'System.Int32[]' | Should -BeTrue
}
}
It "Infers type from foreach-object of integer" {
$res = [AstTypeInference]::InferTypeOf( { [int[]] $i = 1..20; $i | ForEach-Object {$_ * 10} }.Ast)
$res.Count | Should -Be 2
foreach ($r in $res) {
$r.Name -In 'System.Int32', 'System.Int32[]' | Should -BeTrue
}
}
It "Infers type from generic new" {
$res = [AstTypeInference]::InferTypeOf( { [System.Collections.Generic.List[int]]::new() }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Match 'System.Collections.Generic.List`1\[\[System.Int32.*'
}
It "Infers type from cim command" -Skip:(!$IsWindows) {
$res = [AstTypeInference]::InferTypeOf( { Get-CimInstance -Namespace root/CIMV2 -ClassName Win32_Bios }.Ast)
$res.Count | Should -Be 2
foreach ($r in $res) {
$r.Name -In 'Microsoft.Management.Infrastructure.CimInstance#root/CIMV2/Win32_Bios',
'Microsoft.Management.Infrastructure.CimInstance' | Should -BeTrue
}
}
It "Infers type from foreach-object with begin/end" {
$res = [AstTypeInference]::InferTypeOf( { [int[]] $i = 1..20; $i | ForEach-Object -Begin {"Hi"} {$_ * 10} -End {[int]} }.Ast)
$res.Count | Should -Be 4
foreach ($r in $res) {
$r.Name -In 'System.Int32', 'System.Int32[]', 'System.String', 'System.Type' | Should -BeTrue
}
}
It "Infers type from foreach-object with membername" {
$res = [AstTypeInference]::InferTypeOf( { Get-ChildItem | ForEach-Object -MemberName Directory }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be "System.IO.DirectoryInfo"
}
It 'Infers typeof Foreach-Object -Member when Member is Property' {
$ast = {Get-Process | ForEach-Object -Member FileVersion}.Ast
$typeNames = [AstTypeInference]::InferTypeof($ast, [TypeInferenceRuntimePermissions]::AllowSafeEval)
$typeNames.Count | Should -Be 1
$typeNames[0] | Should -Be 'System.String'
}
It 'Infers typeof Foreach-Object -Member when member is ScriptProperty' {
$ast = {Get-Process | ForEach-Object -Member Description}.Ast
$typeNames = [AstTypeInference]::InferTypeof($ast, [TypeInferenceRuntimePermissions]::AllowSafeEval)
$typeNames.Count | Should -Be 1
$typeNames[0] | Should -Be 'System.String'
}
It 'Infers typeof Foreach-Object -Member when Member is Alias' {
$ast = {Get-Process | ForEach-Object -Member Handles}.Ast
$typeNames = [AstTypeInference]::InferTypeof($ast, [TypeInferenceRuntimePermissions]::AllowSafeEval)
$typeNames.Count | Should -Be 1
$typeNames[0] | Should -Be 'System.Int32'
}
It 'Infers typeof Foreach-Object -Member when using dependent scriptproperties' {
class InferScriptPropLevel1 {
[string] $Value
InferScriptPropLevel1() {
$this.Value = "TheValue"
}
}
class InferScriptPropLevel2 {
[InferScriptPropLevel1] $X
InferScriptPropLevel2() {$this.X = [InferScriptPropLevel1]::new()}
}
Update-TypeData -TypeName InferScriptPropLevel1 -MemberName TheValue -MemberType ScriptProperty -Value { return $this.Value } -Force
Update-TypeData -TypeName InferScriptPropLevel2 -MemberName XVal -MemberType ScriptProperty -Value {return $this.X } -Force
try {
$ast = {[InferScriptPropLevel2]::new() | ForEach-Object -MemberName XVal | ForEach-Object -MemberName TheValue}.Ast
$typeNames = [AstTypeInference]::InferTypeof($ast, [TypeInferenceRuntimePermissions]::AllowSafeEval)
$typeNames.Count | Should -Be 1
$typeNames[0] | Should -Be 'System.String'
}
finally {
Remove-TypeData -TypeName InferScriptPropLevel1
Remove-TypeData -TypeName InferScriptPropLevel2
}
}
It "Infers typeof pscustomobject" {
$res = [AstTypeInference]::InferTypeOf( { [pscustomobject] @{
B = "X"
A = 1
}}.Ast)
$res.Count | Should -Be 1
$res[0].GetType().Name | Should -Be "PSSyntheticTypeName"
$res[0].Name | Should -Be "System.Management.Automation.PSObject#A:B"
$res[0].Members[0].Name | Should -Be "A"
$res[0].Members[0].PSTypeName | Should -Be "System.Int32"
$res[0].Members[1].Name | Should -Be "B"
$res[0].Members[1].PSTypeName | Should -Be "System.String"
}
It "Infers typeof pscustomobject with PSTypeName" {
$res = [AstTypeInference]::InferTypeOf( { [pscustomobject] @{
A = 1
B = "X"
PSTypeName = "MyType"
}}.Ast)
$res.Count | Should -Be 1
$res[0].GetType().Name | Should -Be "PSSyntheticTypeName"
$res.Members.Count | Should -Be 2
$res[0].Name | Should -Be "MyType#A:B"
$res[0].Members[0].Name | Should -Be "A"
$res[0].Members[0].PSTypeName | Should -Be "System.Int32"
}
It "Infers typeof Select-Object when Parameter is Property" {
$res = [AstTypeInference]::InferTypeOf( { [io.fileinfo]::new("file") | Select-Object -Property Directory }.Ast)
$res.Count | Should -Be 1
$res[0].GetType().Name | Should -Be "PSSyntheticTypeName"
$res[0].Name | Should -Be "System.Management.Automation.PSObject#Directory"
$res[0].Members[0].Name | Should -Be "Directory"
$res[0].Members[0].PSTypeName | Should -Be "System.IO.DirectoryInfo"
}
It "Infers typeof Select-Object when PSObject and Parameter is Property" {
$res = [AstTypeInference]::InferTypeOf( { [PSCustomObject] @{A = 1; B = "2"} | Select-Object -Property A}.Ast)
$res.Count | Should -Be 1
$res[0].Name | Should -Be "System.Management.Automation.PSObject#A"
$res[0].Members[0].Name | Should -Be "A"
$res[0].Members[0].PSTypeName | Should -Be "System.Int32"
}
It "Infers typeof Select-Object when Parameter is Properties" {
$res = [AstTypeInference]::InferTypeOf( { [io.fileinfo]::new("file") | Select-Object -Property Director*, Name }.Ast)
$res.Count | Should -Be 1
$res[0].Name | Should -Be "System.Management.Automation.PSObject#Directory:DirectoryName:Name"
$res[0].Members[0].Name | Should -Be "Directory"
$res[0].Members[0].PSTypeName | Should -Be "System.IO.DirectoryInfo"
$res[0].Members[1].Name | Should -Be "DirectoryName"
$res[0].Members[1].PSTypeName | Should -Be "System.String"
}
It "Infers typeof Select-Object when Parameter is ExcludeProperty" {
$res = [AstTypeInference]::InferTypeOf( { [io.fileinfo]::new("file") | Select-Object -ExcludeProperty *Time*, E* }.Ast)
$res.Count | Should -Be 1
$res[0].Name | Should -BeExactly "System.Management.Automation.PSObject#Attributes:BaseName:Directory:DirectoryName:FullName:IsReadOnly:Length:LengthString:LinkType:Mode:ModeWithoutHardLink:Name:NameString:Target:VersionInfo"
$names = $res[0].Members.Name
$names -contains "BaseName" | Should -BeTrue
$names -contains "Name" | Should -BeTrue
$names -contains "Mode" | Should -BeTrue
$names -contains "Exits" | Should -BeFalse
}
It "Infers typeof Select-Object when Parameter is ExpandProperty" {
$res = [AstTypeInference]::InferTypeOf( { [io.fileinfo]::new("file") | Select-Object -ExpandProperty Directory }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be "System.IO.DirectoryInfo"
}
It "Infers typeof Select-Object when No projection is done" {
$res = [AstTypeInference]::InferTypeOf( { "Hello" | Select-Object -First 1}.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be "System.String"
}
It "Infers typeof Group-Object Group" {
$res = [AstTypeInference]::InferTypeOf( { Get-ChildItem | Group-Object | ForEach-Object Group }.Ast)
$res.Count | Should -Be 3
($res.Name | Sort-Object)[1,2] -join ', ' | Should -Be "System.IO.DirectoryInfo, System.IO.FileInfo"
}
It "Infers typeof Group-Object Values" {
$res = [AstTypeInference]::InferTypeOf( { Get-ChildItem | Group-Object | ForEach-Object Values }.Ast)
$res.Count | Should -Be 3
($res.Name | Sort-Object)[1,2] -join ', ' | Should -Be "System.IO.DirectoryInfo, System.IO.FileInfo"
}
It "Infers typeof Group-Object Group with Property" {
$res = [AstTypeInference]::InferTypeOf( { Get-ChildItem | Group-Object -Property Name | ForEach-Object Group }.Ast)
$res.Count | Should -Be 3
($res.Name | Sort-Object)[1,2] -join ', ' | Should -Be "System.IO.DirectoryInfo, System.IO.FileInfo"
}
It "Infers typeof Group-Object Values with Property" {
$res = [AstTypeInference]::InferTypeOf( { Get-ChildItem | Group-Object -Property Name | ForEach-Object Values }.Ast)
$res.Count | Should -Be 2
$res.Name -join ', ' | Should -Be "System.String, System.Collections.ArrayList"
}
It "Infers typeof Group-Object Group with NoElement" {
$res = [AstTypeInference]::InferTypeOf( { Get-ChildItem | Group-Object -Property Name -NoElement | ForEach-Object Group }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -BeLike "*Collection*PSObject*"
}
It "Infers typeof Group-Object Values with Properties" {
$res = [AstTypeInference]::InferTypeOf( { Get-ChildItem | Group-Object -Property Name,CreationTime | ForEach-Object Values }.Ast)
$res.Count | Should -Be 3
($res.Name | Sort-Object) -join ', ' | Should -Be "System.Collections.ArrayList, System.DateTime, System.String"
}
It "ignores Group-Object Group with Scriptblock" {
$res = [AstTypeInference]::InferTypeOf( { Get-ChildItem | Group-Object -Property {$_.Name} | ForEach-Object Values }.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be "System.Collections.ArrayList"
}
It "Infers type from OutputTypeAttribute" {
$res = [AstTypeInference]::InferTypeOf( { Get-Process -Id 2345 }.Ast)
$gpsOutput = [Microsoft.PowerShell.Commands.GetProcessCommand].GetCustomAttributes([System.Management.Automation.OutputTypeAttribute], $false).Type
$names = $gpsOutput.Name
foreach ($r in $res) {
$r.Name -In $names | Should -BeTrue
}
}
It "Infers type from variable with AllowSafeEval" {
function Hide-GetProcess { Get-Process }
$p = Hide-GetProcess
$res = [AstTypeInference]::InferTypeOf( { $p }.Ast, [TypeInferenceRuntimePermissions]::AllowSafeEval)
$res.Name | Should -Be 'System.Diagnostics.Process'
}
It "Infers type from variable with type in scope" {
$res = [AstTypeInference]::InferTypeOf( {
$p = 1
$p
}.Ast)
$res.Name | Should -Be 'System.Int32'
}
It "Infers type from block statement" {
$errors = $null
$tokens = $null
$ast = [Language.Parser]::ParseInput("parallel {1}", [ref] $tokens, [ref] $errors)
$res = [AstTypeInference]::InferTypeOf( $ast.EndBlock.Statements[0])
$res.Name | Should -Be 'System.Int32'
}
It 'Infers type from attributed expession' {
$res = [AstTypeInference]::InferTypeOf( {
[ValidateRange(1, 2)]
[int]$i = 1
}.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be System.Int32
}
It 'Infers type from if statement' {
$res = [AstTypeInference]::InferTypeOf( {
if ($true) { return 1}
else { return 'Text'}
}.Ast)
$res.Count | Should -Be 2
foreach ($r in $res) {
$r.Name -In 'System.Int32', 'System.String' | Should -BeTrue
}
}
It 'Infers type from switch statement' {
$res = [AstTypeInference]::InferTypeOf( {
switch (1, 2, 3) {
(1) { return 'Hello'}
(2) {return [int]}
default {return 1}
}
}.Ast)
$res.Count | Should -Be 3
foreach ($r in $res) {
$r.Name -In 'System.Type', 'System.Int32', 'System.String' | Should -BeTrue
}
}
It 'Infers type from Foreach statement' {
$res = [AstTypeInference]::InferTypeOf( {
foreach ($i in 1, 2, 3) {
if ($i -eq 1) { return 'Hello'}
if ($i -eq 2) {return [int]}
return 1
}
}.Ast)
$res.Count | Should -Be 3
foreach ($r in $res) {
$r.Name -In 'System.Type', 'System.Int32', 'System.String' | Should -BeTrue
}
}
It 'Infers type of a Foreach statement current value variable' {
$res = [AstTypeInference]::InferTypeOf( {
foreach ($intValue in 1, 2, 3) {
$intValue
}
}.Ast.EndBlock.Statements[0].Body.Statements[0].PipelineElements[0].Expression)
$res.Count | Should -Be 1
$res.Name | Should -BeExactly 'System.Int32'
}
It 'Infers type of a Foreach statement current value variable with hashtable enumerator' {
$res = [AstTypeInference]::InferTypeOf( {
foreach ($dictionaryEntry in @{}.GetEnumerator()) {
$dictionaryEntry
}
}.Ast.EndBlock.Statements[0].Body.Statements[0].PipelineElements[0].Expression)
$res.Count | Should -Be 2
$res.Name | Should -Be 'System.Collections.DictionaryEntry', 'System.Object'
}
It 'Infers type of a Foreach statement current value variable with dictionary enumerator' {
$res = [AstTypeInference]::InferTypeOf( {
foreach ($keyValuePair in [Dictionary[int, string]]::new().GetEnumerator()) {
$keyValuePair
}
}.Ast.EndBlock.Statements[0].Body.Statements[0].PipelineElements[0].Expression)
$res.Count | Should -Be 3
$res.Name | Should -Be ([KeyValuePair[int, string]].FullName), 'System.Object', 'System.Collections.DictionaryEntry'
}
It 'Infers type of a Foreach statement current value variable with generic IEnumerable' {
$res = [AstTypeInference]::InferTypeOf( {
$debugger = [Debugger]$Host.Runspace.Debugger
foreach ($subscriber in $debugger.GetCallStack()) {
$subscriber
}
}.Ast.EndBlock.Statements[1].Body.Statements[0].PipelineElements[0].Expression)
$res.Count | Should -Be 1
$res.Name | Should -BeExactly 'System.Management.Automation.CallStackFrame'
}
It 'Infers type from While statement' {
$res = [AstTypeInference]::InferTypeOf( {
while ($true) {
if ($i -eq 1) { return 'Hello'}
if ($i -eq 2) {return [int]}
return 1
}
}.Ast)
$res.Count | Should -Be 3
foreach ($r in $res) {
$r.Name -In 'System.Type', 'System.Int32', 'System.String' | Should -BeTrue
}
}
It 'Infers type from For statement' {
$res = [AstTypeInference]::InferTypeOf( {
for ($i = 0; $i -lt 10; $i++) {
if ($i -eq 1) { return 'Hello'}
if ($i -eq 2) {return [int]}
return 1
}
}.Ast)
$res.Count | Should -Be 3
foreach ($r in $res) {
$r.Name -In 'System.Type', 'System.Int32', 'System.String' | Should -BeTrue
}
}
It 'Infers type from Do-While statement' {
$res = [AstTypeInference]::InferTypeOf( {
do {
if ($i -eq 1) { return 'Hello'}
if ($i -eq 2) {return [int]}
return 1
}while ($true)
}.Ast)
$res.Count | Should -Be 3
foreach ($r in $res) {
$r.Name -In 'System.Type', 'System.Int32', 'System.String' | Should -BeTrue
}
}
It 'Infers type from Do-Until statement' {
$res = [AstTypeInference]::InferTypeOf( {
do {
if ($i -eq 1) { return 'Hello'}
if ($i -eq 2) {return [int]}
return 1
} until ($true)
}.Ast)
$res.Count | Should -Be 3
foreach ($r in $res) {
$r.Name -In 'System.Type', 'System.Int32', 'System.String' | Should -BeTrue
}
}
It 'Infers type from full scriptblock' {
$res = [AstTypeInference]::InferTypeOf( {
begin {1}
process {"text"}
end {[int]}
}.Ast)
$res.Count | Should -Be 3
foreach ($r in $res) {
$r.Name -In 'System.Type', 'System.Int32', 'System.String' | Should -BeTrue
}
}
It 'Infers type from sub expression' {
$res = [AstTypeInference]::InferTypeOf( {
$(1)
}.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be System.Int32
}
It 'Infers type from Throw statement' {
$res = [AstTypeInference]::InferTypeOf( {
throw 'Foo'
}.Ast)
$res.Count | Should -Be 0
}
It 'Infers type from Return statement' {
$res = [AstTypeInference]::InferTypeOf( {
return 1
}.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.Int32'
}
It 'Infers type from New-Object statement' {
$res = [AstTypeInference]::InferTypeOf( {
New-Object -TypeName 'System.Diagnostics.Stopwatch'
}.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.Diagnostics.Stopwatch'
}
It 'Infers type from Continue statement' {
$res = [AstTypeInference]::InferTypeOf( {
continue
}.Ast)
$res.Count | Should -Be 0
}
It 'Infers type from Break statement' {
$res = [AstTypeInference]::InferTypeOf( {
break
}.Ast)
$res.Count | Should -Be 0
}
It 'Infers type from Merging redirection' {
$errors = $null
$tokens = $null
$ast = [Language.Parser]::ParseInput("p4 resolve ... 2>&1", [ref] $tokens, [ref] $errors)
$res = [AstTypeInference]::InferTypeOf( $ast.EndBlock.Statements[0].PipelineElements[0].Redirections[0] )
$res.Count | Should -Be 0
}
It 'Infers type from File redirection' {
$errors = $null
$tokens = $null
$ast = [Language.Parser]::ParseInput("p4 resolve ... > foo.txt", [ref] $tokens, [ref] $errors)
$res = [AstTypeInference]::InferTypeOf( $ast.EndBlock.Statements[0].PipelineElements[0].Redirections[0] )
$res.Count | Should -Be 0
}
It 'Infers type of alias property' {
class X {
[int] $Length
}
Update-TypeData -TypeName X -MemberType AliasProperty -MemberName AliasLength -Value Length -Force
$res = [AstTypeInference]::InferTypeOf( {
[x]::new().AliasLength
}.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be System.Int32
}
It 'Infers type of code property' {
class X {
static [int] CodeProp([psobject] $o) { return 1 }
}
class Y {}
Update-TypeData -TypeName Y -MemberName CodeProp -MemberType CodeProperty -Value ([X].GetMethod("CodeProp")) -Force
$res = [AstTypeInference]::InferTypeOf( {
[Y]::new().CodeProp
}.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be System.Int32
}
It 'Infers type of script property' {
class Y {}
Update-TypeData -TypeName Y -MemberName ScriptProp -MemberType ScriptProperty -Value {1} -Force
$res = [AstTypeInference]::InferTypeOf( {
[Y]::new().ScriptProp
}.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be System.Int32
}
It 'Infers type of script property with outputtype' {
class Y {}
Update-TypeData -TypeName Y -MemberName ScriptProp -MemberType ScriptProperty -Value {[OutputType([int])]param()1} -Force
$res = [AstTypeInference]::InferTypeOf( {
[Y]::new().ScriptProp
}.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be System.Int32
}
It 'Infers type of script method with outputtype' {
class Y {}
Update-TypeData -TypeName Y -MemberName MyScriptMethod -MemberType ScriptMethod -Value {[OutputType([int])]param()1} -Force
$res = [AstTypeInference]::InferTypeOf( {
[Y]::new().MyScriptMethod
}.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be System.Int32
}
It 'Infers type of note property' {
$res = [AstTypeInference]::InferTypeOf( {
[pscustomobject] @{
A = ''
}.A
}.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.Management.Automation.PSObject'
}
It 'Infers type of try catch finally' {
$res = [AstTypeInference]::InferTypeOf( {
try {
1
}
catch [exception] {
"Text"
}
finally {
[int]
}
}.Ast)
$res.Count | Should -Be 3
foreach ($r in $res) {
$r.Name -In 'System.Int32', 'System.String', 'System.Type' | Should -BeTrue
}
}
It "Infers type from trap statement" {
$res = [AstTypeInference]::InferTypeOf( {
trap {
"text"
}
}.Ast.EndBlock.Traps[0])
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.String'
}
It "Infers type from exit statement" {
$res = [AstTypeInference]::InferTypeOf( {
exit
}.Ast.EndBlock)
$res.Count | Should -Be 0
}
It 'Infers type of Where/Sort/Foreach pipeline' {
$res = [AstTypeInference]::InferTypeOf( {
[int[]](1..10) | Sort-Object -Descending | Where-Object {$_ -gt 3} | ForEach-Object {$_.ToString()}
}.Ast)
$res.Name | Should -Be System.String
}
It 'Infers type of Method accessed as Property' {
$res = [AstTypeInference]::InferTypeOf( {
''.ToString
}.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be System.Management.Automation.PSMethod
}
It 'Infers int from List[int] with foreach' {
$res = [AstTypeInference]::InferTypeOf( {
$l = [System.Collections.Generic.List[string]]::new()
$l | ForEach-Object {$_}
}.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be System.String
}
It 'Infers class type' {
$res = [AstTypeInference]::InferTypeOf( {
class X {
[int] $I
[bool] Method() {
return $true
}
}
}.Ast)
$res.Count | Should -Be 0
}
Context "TestDrivePath" {
BeforeAll {
$errors = $null
$tokens = $null
$p = Resolve-Path TestDrive:/
}
It 'Infers type of command parameter' {
$ast = [Language.Parser]::ParseInput("Get-ChildItem -Path $p/foo.txt", [ref] $tokens, [ref] $errors)
$cmdParam = $ast.EndBlock.Statements[0].Pipelineelements[0].CommandElements[1]
$res = [AstTypeInference]::InferTypeOf( $cmdParam )
$res.Count | Should -Be 0
}
It 'Infers type of command parameter - second form' {
$ast = [Language.Parser]::ParseInput("Get-ChildItem -LiteralPath $p/foo.txt", [ref] $tokens, [ref] $errors)
$cmdParam = $ast.EndBlock.Statements[0].Pipelineelements[0].CommandElements[1]
$res = [AstTypeInference]::InferTypeOf( $cmdParam )
$res.Count | Should -Be 0
}
It 'Infers type of common commands with Path parameter' {
$ast = [Language.Parser]::ParseInput("Get-ChildItem -Path:$p/foo.txt", [ref] $tokens, [ref] $errors)
$cmdAst = $ast.EndBlock.Statements[0].Pipelineelements[0]
$res = [AstTypeInference]::InferTypeOf( $cmdAst )
$res.Count | Should -Be 2
foreach ($r in $res) {
$r.Name -In 'System.IO.FileInfo', 'System.IO.DirectoryInfo' | Should -BeTrue
}
}
It 'Infers type of common commands with LiteralPath parameter' {
$ast = [Language.Parser]::ParseInput("Get-ChildItem -LiteralPath:$p/foo.txt", [ref] $tokens, [ref] $errors)
$cmdAst = $ast.EndBlock.Statements[0].Pipelineelements[0]
$res = [AstTypeInference]::InferTypeOf( $cmdAst )
$res.Count | Should -Be 2
foreach ($r in $res) {
$r.Name -In 'System.IO.FileInfo', 'System.IO.DirectoryInfo' | Should -BeTrue
}
}
}
It 'Infers type of variable $_ in hashtable in command parameter' {
$variableAst = {1..10 | Format-Table @{n = 'x'; ex = {$_}}}.ast.Find( {param($a) $a -is [System.Management.Automation.Language.VariableExpressionAst]}, $true)
$res = [AstTypeInference]::InferTypeOf( $variableAst)
$res.Count | Should -Be 1
$res.Name | Should -Be System.Int32
}
It 'Infers type of variable $_ in hashtable from Array' {
$variableAst = { [int[]]::new(10) | Format-Table @{n = 'x'; ex = {$_}}}.ast.Find( {param($a) $a -is [System.Management.Automation.Language.VariableExpressionAst]}, $true)
$res = [AstTypeInference]::InferTypeOf( $variableAst)
$res.Count | Should -Be 1
$res.Name | Should -Be System.Int32
}
It 'Infers type of variable $_ in hashtable from generic IEnumerable ' {
$variableAst = { [System.Collections.Generic.List[int]]::new() | Format-Table @{n = 'x'; ex = {$_}}}.ast.Find( {param($a) $a -is [System.Management.Automation.Language.VariableExpressionAst]}, $true)
$res = [AstTypeInference]::InferTypeOf( $variableAst)
$res.Count | Should -Be 1
$res.Name | Should -Be System.Int32
}
It 'Infers type of variable $_ command parameter' {
$variableAst = { 1..10 | Group-Object {$_.Length}}.ast.Find( {param($a) $a -is [System.Management.Automation.Language.VariableExpressionAst]}, $true)
$res = [AstTypeInference]::InferTypeOf( $variableAst)
$res.Count | Should -Be 1
$res.Name | Should -Be System.Int32
}
It 'Infers type of variable $_ in catch block' {
$variableAst = { try {} catch { $_ } }.Ast.Find({ param($a) $a -is [System.Management.Automation.Language.VariableExpressionAst] }, $true)
$res = [AstTypeInference]::InferTypeOf($variableAst)
$res | Should -HaveCount 1
$res.Name | Should -Be System.Management.Automation.ErrorRecord
}
It 'Infers type of untyped $_.Exception in catch block' {
$memberAst = { try {} catch { $_.Exception } }.Ast.Find({ param($a) $a -is [System.Management.Automation.Language.MemberExpressionAst] }, $true)
$res = [AstTypeInference]::InferTypeOf($memberAst)
$res | Should -HaveCount 1
$res.Name | Should -Be System.Exception
}
$catchClauseTypes = @(
@{ Type = 'System.ArgumentException' }
@{ Type = 'System.ArgumentNullException' }
@{ Type = 'System.ArgumentOutOfRangeException' }
@{ Type = 'System.Collections.Generic.KeyNotFoundException' }
@{ Type = 'System.DivideByZeroException' }
@{ Type = 'System.FormatException' }
@{ Type = 'System.IndexOutOfRangeException' }
@{ Type = 'System.InvalidOperationException' }
@{ Type = 'System.IO.DirectoryNotFoundException' }
@{ Type = 'System.IO.DriveNotFoundException' }
@{ Type = 'System.IO.FileNotFoundException' }
@{ Type = 'System.IO.PathTooLongException' }
@{ Type = 'System.Management.Automation.CommandNotFoundException' }
@{ Type = 'System.Management.Automation.JobFailedException' }
@{ Type = 'System.Management.Automation.RuntimeException' }
@{ Type = 'System.Management.Automation.ValidationMetadataException' }
@{ Type = 'System.NotImplementedException' }
@{ Type = 'System.NotSupportedException' }
@{ Type = 'System.ObjectDisposedException' }
@{ Type = 'System.OverflowException' }
@{ Type = 'System.PlatformNotSupportedException' }
@{ Type = 'System.RankException' }
@{ Type = 'System.TimeoutException' }
@{ Type = 'System.UriFormatException' }
)
It 'Infers type of $_.Exception in [<Type>] typed catch block' -TestCases $catchClauseTypes {
param($Type)
$memberAst = [scriptblock]::Create("try {} catch [$Type] { `$_.Exception }").Ast.Find(
{ param($a) $a -is [System.Management.Automation.Language.MemberExpressionAst] },
$true
)
$res = [AstTypeInference]::InferTypeOf($memberAst)
$res | Should -HaveCount 1
$res.Name | Should -Be $Type
}
It 'Infers possible types of $_.Exception in multi-typed catch block' {
$memberAst = { try {} catch [System.ArgumentException], [System.NotImplementedException] { $_.Exception } }.Ast.Find(
{ param($a) $a -is [System.Management.Automation.Language.MemberExpressionAst] },
$true
)
$res = [AstTypeInference]::InferTypeOf($memberAst)
$res | Should -HaveCount 2
$res[0].Name | Should -Be System.ArgumentException
$res[1].Name | Should -Be System.NotImplementedException
}
It 'Infers type of $_.Exception in each successive catch block' {
$memberAst = {
try {}
catch [System.ArgumentException] { $_.Exception }
catch { $_.Exception }
}.Ast.FindAll(
{ param($a) $a -is [System.Management.Automation.Language.MemberExpressionAst] },
$true
)
$res = foreach ($item in $memberAst) { [AstTypeInference]::InferTypeOf($item) }
$res | Should -HaveCount 2
$res[0].Name | Should -Be System.ArgumentException
$res[1].Name | Should -Be System.Exception
}
It 'Infers type of function member' {
$res = [AstTypeInference]::InferTypeOf( {
class X {
[DateTime] GetDate() { return [datetime]::Now }
}
}.Ast.Find( {param($ast) $ast -is [System.Management.Automation.Language.FunctionMemberAst]}, $true))
$res.Count | Should -Be 0
}
It 'Infers type of MemberExpression on class property' {
class X {
[DateTime] $Date
}
$x = [X]::new()
$res = [AstTypeInference]::InferTypeOf( {
$x.Date
}.Ast.Find( {param($ast) $ast -is [System.Management.Automation.Language.MemberExpressionAst] -and $ast.Member.Value -eq 'Date'}, $true))
$res.Count | Should -Be 1
$res.Name | Should -Be System.DateTime
}
It 'Infers type of MemberExpression on class Method' {
class X {
[DateTime] GetDate() { return [DateTime]::Now }
}
$x = [X]::new()
$res = [AstTypeInference]::InferTypeOf( {
$x.GetDate()
}.Ast.Find( {param($ast) $ast -is [System.Management.Automation.Language.MemberExpressionAst] -and $ast.Member.Value -eq 'GetDate'}, $true))
$res.Count | Should -Be 1
$res.Name | Should -Be System.DateTime
}
It 'Infers type of note property with safe eval' -Skip {
$res = [AstTypeInference]::InferTypeOf( {
[pscustomobject] @{
A = ''
}.A
}.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.String'
}
It 'Infers type of invoke operator scriptblock' -Skip {
$res = [AstTypeInference]::InferTypeOf( {
& {1}
}.Ast)
$res.Count | Should -Be 1
$res.Name | Should -Be System.Int32
}
It 'Infers type of script property with safe eval' -Skip {
class Y {}
Update-TypeData -TypeName Y -MemberName SafeEvalScriptProp -MemberType ScriptProperty -Value {1} -Force
$res = [AstTypeInference]::InferTypeOf( {
[Y]::new().SafeEvalScriptProp
}.Ast, [TypeInferenceRuntimePermissions]::AllowSafeEval)
$res.Count | Should -Be 1
$res.Name | Should -Be System.Int32
}
It 'Infers type of base ctor' -Skip {
$res = [AstTypeInference]::InferTypeOf( {
class BaseType {
[string] $s
BaseType([string]$s) {$this.s = $s}
}
class X : BaseType {
X() : base("foo") {}
}
}.Ast.Find( {param($ast) $ast -is [System.Management.Automation.Language.BaseCtorInvokeMemberExpressionAst]}, $true))
$res.Count | Should -Be BaseType
}
}
Describe "AstTypeInference tests" -Tags CI {
It "Infers type from integer with passed in powershell instance" {
$powerShell = [PowerShell]::Create([RunspaceMode]::CurrentRunspace)
$res = [AstTypeInference]::InferTypeOf( { 1 }.Ast, $powerShell)
$res.Count | Should -Be 1
$res.Name | Should -Be 'System.Int32'
}
It "Infers type from integer with passed in powershell instance and typeinferencespermissions" {
$powerShell = [PowerShell]::Create([RunspaceMode]::CurrentRunspace)
$v = 1
$res = [AstTypeInference]::InferTypeOf( { $v }.Ast, $powerShell, [TypeInferenceRuntimePermissions]::AllowSafeEval)
$res.Name | Should -Be 'System.Int32'
}
}