diff --git a/src/System.Management.Automation/engine/CommandInfo.cs b/src/System.Management.Automation/engine/CommandInfo.cs index 2c4cd08eb75..9f1168de64e 100644 --- a/src/System.Management.Automation/engine/CommandInfo.cs +++ b/src/System.Management.Automation/engine/CommandInfo.cs @@ -3,11 +3,15 @@ using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Management.Automation.Language; using System.Management.Automation.Runspaces; using System.Reflection; using System.Runtime.ExceptionServices; +using System.Text; + using Microsoft.PowerShell.Commands; namespace System.Management.Automation @@ -793,6 +797,17 @@ public PSTypeName(string name) _type = null; } + /// + /// This constructor is used when the creating a PSObject with a custom typename. + /// + /// The name of the type. + /// The real type. + public PSTypeName(string name, Type type) + { + Name = name; + _type = type; + } + /// /// This constructor is used when the type is defined in PowerShell. /// @@ -901,6 +916,95 @@ public override string ToString() } } + [DebuggerDisplay("{PSTypeName} {Name}")] + internal struct PSMemberNameAndType + { + public readonly string Name; + + public readonly PSTypeName PSTypeName; + + public readonly object Value; + + public PSMemberNameAndType(string name, PSTypeName typeName, object value = null) + { + Name = name; + PSTypeName = typeName; + Value = value; + } + } + + /// + /// Represents dynamic types such as , + /// but can be used where a real type might not be available, in which case the name of the type can be used. + /// The type encodes the members of dynamic objects in the type name. + /// + internal class PSSyntheticTypeName : PSTypeName + { + internal static PSSyntheticTypeName Create(string typename, IList membersTypes) => Create(new PSTypeName(typename), membersTypes); + + internal static PSSyntheticTypeName Create(Type type, IList membersTypes) => Create(new PSTypeName(type), membersTypes); + + internal static PSSyntheticTypeName Create(PSTypeName typename, IList membersTypes) + { + var typeName = GetMemberTypeProjection(typename.Name, membersTypes); + var members = new List(); + members.AddRange(membersTypes); + members.Sort((c1,c2) =>string.Compare(c1.Name, c2.Name, StringComparison.CurrentCultureIgnoreCase)); + return new PSSyntheticTypeName(typeName, typename.Type, members); + } + + private PSSyntheticTypeName(string typeName, Type type, IList membersTypes) + : base(typeName, type) + { + Members = membersTypes; + if (type != typeof(PSObject)) + { + return; + } + + for (int i = 0; i < Members.Count; i++) + { + var psMemberNameAndType = Members[i]; + if (IsPSTypeName(psMemberNameAndType)) + { + Members.RemoveAt(i); + break; + } + } + } + + private static bool IsPSTypeName(PSMemberNameAndType member) => member.Name.Equals(nameof(PSTypeName), StringComparison.CurrentCultureIgnoreCase); + + private static string GetMemberTypeProjection(string typename, IList members) + { + if (typename == typeof(PSObject).FullName) + { + foreach (var mem in members) + { + if (IsPSTypeName(mem)) + { + typename = mem.Value.ToString(); + } + } + } + + var builder = new StringBuilder(typename, members.Count * 7); + builder.Append('#'); + foreach (var m in members.OrderBy(m => m.Name)) + { + if (!IsPSTypeName(m)) + { + builder.Append(m.Name).Append(":"); + } + } + + builder.Length--; + return builder.ToString(); + } + + public IList Members { get; } + } + internal interface IScriptCommandInfo { ScriptBlock ScriptBlock { get; } diff --git a/src/System.Management.Automation/engine/MshMemberInfo.cs b/src/System.Management.Automation/engine/MshMemberInfo.cs index a49351846fe..6f50677a8f1 100644 --- a/src/System.Management.Automation/engine/MshMemberInfo.cs +++ b/src/System.Management.Automation/engine/MshMemberInfo.cs @@ -83,9 +83,13 @@ public enum PSMemberTypes /// Dynamic = 4096, /// + /// Members that are inferred by type inference for PSObject and hashtable. + /// + InferredProperty = 8192, + /// /// All property member types /// - Properties = AliasProperty | CodeProperty | Property | NoteProperty | ScriptProperty, + Properties = AliasProperty | CodeProperty | Property | NoteProperty | ScriptProperty | InferredProperty, /// /// All method member types /// @@ -954,6 +958,34 @@ public override string TypeNameOfValue } + /// + /// Type used to capture the properties inferred from Hashtable and PSObject. + /// + internal class PSInferredProperty : PSPropertyInfo + { + public PSInferredProperty(string name, PSTypeName typeName) + { + this.name = name; + TypeName = typeName; + } + + internal PSTypeName TypeName { get; } + + public override PSMemberTypes MemberType => PSMemberTypes.InferredProperty; + + public override object Value { get; set; } + + public override string TypeNameOfValue => TypeName.Name; + + public override PSMemberInfo Copy() => new PSInferredProperty(Name, TypeName); + + public override bool IsSettable => false; + + public override bool IsGettable => false; + + public override string ToString() => $"{ToStringCodeMethods.Type(TypeName.Type)} {Name}"; + } + /// /// Used to access the adapted or base properties from the BaseObject /// diff --git a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs index 8a2ea1a68c3..482d212e1b4 100644 --- a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs +++ b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs @@ -8,6 +8,7 @@ using System.Management.Automation.Language; using System.Management.Automation.Runspaces; using System.Reflection; +using System.Text; using System.Text.RegularExpressions; using Microsoft.PowerShell.Commands; @@ -174,6 +175,14 @@ internal IList GetMembersByInferredType(PSTypeName typename, bool isStat List results = new List(); Func filterToCall = filter; + if (typename is PSSyntheticTypeName synthetic) + { + foreach (var mem in synthetic.Members) + { + results.Add(new PSInferredProperty(mem.Name, mem.PSTypeName)); + } + } + if (typename.Type != null) { AddMembersByInferredTypesClrType(typename, isStatic, filter, filterToCall, results); @@ -515,6 +524,62 @@ object ICustomAstVisitor.VisitArrayLiteral(ArrayLiteralAst arrayLiteralAst) object ICustomAstVisitor.VisitHashtable(HashtableAst hashtableAst) { + if (hashtableAst.KeyValuePairs.Count > 0) + { + var properties = new List(); + + foreach (var kv in hashtableAst.KeyValuePairs) + { + string name = null; + string typeName = null; + if (kv.Item1 is StringConstantExpressionAst stringConstantExpressionAst) + { + name = stringConstantExpressionAst.Value; + } + else if (kv.Item1 is ConstantExpressionAst constantExpressionAst) + { + name = constantExpressionAst.Value.ToString(); + } + else if (SafeExprEvaluator.TrySafeEval(kv.Item1, _context.ExecutionContext, out object nameValue)) + { + name = nameValue.ToString(); + } + if (name != null) + { + object value = null; + if (kv.Item2 is PipelineAst pipelineAst && pipelineAst.GetPureExpression() is ExpressionAst expression) + { + switch (expression) + { + case ConstantExpressionAst constantExpression: + { + value = constantExpression.Value; + break; + } + default: + { + typeName = InferTypes(kv.Item2).FirstOrDefault()?.Name; + if (typeName == null) + { + if (SafeExprEvaluator.TrySafeEval(expression, _context.ExecutionContext, out object safeValue)) + { + value = safeValue; + } + } + + break; + } + } + } + + var pstypeName = value != null ? new PSTypeName(value.GetType()) : new PSTypeName(typeName ?? "System.Object"); + properties.Add(new PSMemberNameAndType(name, pstypeName, value)); + } + } + + return new[] { PSSyntheticTypeName.Create(typeof(Hashtable), properties) }; + } + return new[] { new PSTypeName(typeof(Hashtable)) }; } @@ -580,7 +645,18 @@ object ICustomAstVisitor.VisitUnaryExpression(UnaryExpressionAst unaryExpression object ICustomAstVisitor.VisitConvertExpression(ConvertExpressionAst convertExpressionAst) { + // The reflection type of PSCustomObject is PSObject, so this covers both the + // [PSObject] @{ Key = "Value" } and the [PSCustomObject] @{ Key = "Value" } case. var type = convertExpressionAst.Type.TypeName.GetReflectionType(); + + if (type == typeof(PSObject) && convertExpressionAst.Child is HashtableAst hashtableAst) + { + if (InferTypes(hashtableAst).FirstOrDefault() is PSSyntheticTypeName syntheticTypeName) + { + return new[] { PSSyntheticTypeName.Create(type, syntheticTypeName.Members) }; + } + } + var psTypeName = type != null ? new PSTypeName(type) : new PSTypeName(convertExpressionAst.Type.TypeName.FullName); return new[] { psTypeName }; } @@ -960,7 +1036,7 @@ private void InferTypesFrom(CommandAst commandAst, List inferredType /// The ast to infer types from. /// The cmdletInfo. /// Pseudo bindings of parameters. - /// List of inferred typenames. + /// List of inferred type names. private List InferTypesFromObjectCmdlets(CommandAst commandAst, CmdletInfo cmdletInfo, PseudoBindingInfo pseudoBinding) { var inferredTypes = new List(16); @@ -995,10 +1071,15 @@ private List InferTypesFromObjectCmdlets(CommandAst commandAst, Cmdl } else if (cmdletInfo.ImplementingType.FullName.EqualsOrdinalIgnoreCase("Microsoft.PowerShell.Commands.SelectObjectCommand")) { - // Select-object - yields whatever we saw before where-object in the pipeline. + // Select-object - adds whatever we saw before where-object in the pipeline. // unless -property or -excludeproperty InferTypesFromSelectCommand(pseudoBinding, commandAst, inferredTypes); } + else if (cmdletInfo.ImplementingType.FullName.EqualsOrdinalIgnoreCase("Microsoft.PowerShell.Commands.GroupObjectCommand")) + { + // Group-object - annotates the types of Group and Value based on whatever we saw before Group-Object in the pipeline. + InferTypesFromGroupCommand(pseudoBinding, commandAst, inferredTypes); + } return inferredTypes; } @@ -1072,6 +1153,118 @@ private void InferTypesFromForeachCommand(PseudoBindingInfo pseudoBinding, Comma } } + private void InferTypesFromGroupCommand(PseudoBindingInfo pseudoBinding, CommandAst commandAst, List inferredTypes) + { + if (pseudoBinding.BoundArguments.TryGetValue("AsHashTable", out AstParameterArgumentPair _)) + { + inferredTypes.Add(new PSTypeName(typeof(Hashtable))); + return; + } + + var noElement = pseudoBinding.BoundArguments.TryGetValue("NoElement", out AstParameterArgumentPair _); + + string[] properties = null; + bool scriptBlockProperty = false; + if (pseudoBinding.BoundArguments.TryGetValue("Property", out AstParameterArgumentPair propertyArgumentPair)) + { + if (propertyArgumentPair is AstPair astPair) + { + switch (astPair.Argument) + { + case StringConstantExpressionAst stringConstant: + { + properties = new[] { stringConstant.Value }; + break; + } + case ArrayLiteralAst arrayLiteral: + { + properties = arrayLiteral.Elements.OfType().Select(c => c.Value).ToArray(); + scriptBlockProperty = arrayLiteral.Elements.OfType().Any(); + break; + } + case CommandElementAst _: + { + scriptBlockProperty = true; + break; + } + } + } + } + + bool IsInPropertyArgument(object o) + { + if (properties == null) + { + return true; + } + + string name; + switch (o) + { + case string s: + name = s; + break; + default: + name = GetMemberName(o); + break; + } + + foreach (var propertyName in properties) + { + if (name.Equals(propertyName, StringComparison.CurrentCultureIgnoreCase)) + { + return true; + } + } + + return false; + } + + var previousPipelineElement = GetPreviousPipelineCommand(commandAst); + var typeName = "Microsoft.PowerShell.Commands.GroupInfo"; + var members = new List(); + foreach (var prevType in InferTypes(previousPipelineElement)) + { + members.Clear(); + if (noElement) + { + members.Add(new PSMemberNameAndType("Values", new PSTypeName(prevType.Name))); + inferredTypes.Add(PSSyntheticTypeName.Create(typeName, members)); + continue; + } + + var memberNameAndTypes = GetMemberNameAndTypeFromProperties(prevType, IsInPropertyArgument); + if (!memberNameAndTypes.Any()) + { + continue; + } + + if (properties != null) + { + foreach (var memType in memberNameAndTypes) + { + members.Clear(); + members.Add(new PSMemberNameAndType("Group", new PSTypeName(prevType.Name))); + members.Add(new PSMemberNameAndType("Values", new PSTypeName(memType.PSTypeName.Name))); + inferredTypes.Add(PSSyntheticTypeName.Create(typeName, members)); + } + } + else + { + // No Property parameter given + // group infers to IList + members.Add(new PSMemberNameAndType("Group", new PSTypeName(prevType.Name))); + // Value infers to IList + if (!scriptBlockProperty) + { + members.Add(new PSMemberNameAndType("Values", new PSTypeName(prevType.Name))); + } + } + + inferredTypes.Add(PSSyntheticTypeName.Create(typeName, members)); + } + } + private void InferTypesFromWhereAndSortCommand(CommandAst commandAst, List inferredTypes) { InferTypesFromPreviousCommand(commandAst, inferredTypes); @@ -1099,10 +1292,78 @@ private void InferTypesFromPreviousCommand(CommandAst commandAst, List inferredTypes) { - if (pseudoBinding.BoundArguments.TryGetValue("Property", out _) - || pseudoBinding.BoundArguments.TryGetValue("ExcludeProperty", out _)) + void InferFromSelectProperties(AstParameterArgumentPair astParameterArgumentPair, CommandBaseAst previousPipelineElementAst, bool includeMatchedProperties = true) { - return; + if (astParameterArgumentPair is AstPair astPair) + { + object ToWildCardOrString(string value) => WildcardPattern.ContainsWildcardCharacters(value) ? (object)new WildcardPattern(value) : value; + object[] properties = null; + switch (astPair.Argument) + { + case StringConstantExpressionAst stringConstant: + { + properties = new[] { ToWildCardOrString(stringConstant.Value) }; + break; + } + case ArrayLiteralAst arrayLiteral: + { + properties = arrayLiteral.Elements.OfType().Select(c => ToWildCardOrString(c.Value)).ToArray(); + break; + } + } + + if (properties == null) + { + return; + } + + bool IsInPropertyArgument(object o) + { + string name; + switch (o) + { + case string s: + name = s; + break; + default: + name = GetMemberName(o); + break; + } + + foreach (var propertyNameOrPattern in properties) + { + switch (propertyNameOrPattern) + { + case string propertyName: + { + if (string.Compare(name, propertyName, StringComparison.CurrentCultureIgnoreCase) == 0) + { + return includeMatchedProperties; + } + + break; + } + case WildcardPattern pattern: + { + if (pattern.IsMatch(name)) + { + return includeMatchedProperties; + } + + break; + } + } + } + + return !includeMatchedProperties; + } + + foreach (var t in InferTypes(previousPipelineElementAst)) + { + var list = GetMemberNameAndTypeFromProperties(t, IsInPropertyArgument); + inferredTypes.Add(PSSyntheticTypeName.Create(typeof(PSObject), list)); + } + } } var previousPipelineElement = GetPreviousPipelineCommand(commandAst); @@ -1111,6 +1372,20 @@ private void InferTypesFromSelectCommand(PseudoBindingInfo pseudoBinding, Comman return; } + if (pseudoBinding.BoundArguments.TryGetValue("Property", out var property)) + { + InferFromSelectProperties(property, previousPipelineElement); + + return; + } + + if (pseudoBinding.BoundArguments.TryGetValue("ExcludeProperty", out var excludeProperty)) + { + InferFromSelectProperties(excludeProperty, previousPipelineElement, includeMatchedProperties: false); + + return; + } + if (pseudoBinding.BoundArguments.TryGetValue("ExpandProperty", out var expandedPropertyArgument)) { foreach (var t in InferTypes(previousPipelineElement)) @@ -1131,6 +1406,71 @@ private void InferTypesFromSelectCommand(PseudoBindingInfo pseudoBinding, Comman InferTypesFromPreviousCommand(commandAst, inferredTypes); } + private List GetMemberNameAndTypeFromProperties(PSTypeName t, Func isInPropertyList) + { + var list = new List(8); + var members = _context.GetMembersByInferredType(t, false, isInPropertyList); + var memberTypes = new List(); + foreach (var mem in members) + { + if (!IsProperty(mem)) + { + continue; + } + + var memberName = GetMemberName(mem); + if (!isInPropertyList(memberName)) + { + continue; + } + + bool maybeWantDefaultCtor = false; + memberTypes.Clear(); + GetTypesOfMembers(t, memberName, members, ref maybeWantDefaultCtor, isInvokeMemberExpressionAst: false, memberTypes); + if (memberTypes.Count > 0) + { + list.Add(new PSMemberNameAndType(memberName, memberTypes[0])); + } + } + + return list; + } + + private static bool IsProperty(object member) + { + switch (member) + { + case PropertyInfo _: return true; + case PSMemberInfo memberInfo: return (memberInfo.MemberType & PSMemberTypes.Properties) == memberInfo.MemberType; + default: return false; + } + } + + private static string GetMemberName(object member) + { + var name = string.Empty; + switch (member) + { + case PSMemberInfo psMemberInfo: + name = psMemberInfo.Name; + break; + case MemberInfo memberInfo: + name = memberInfo.Name; + break; + case PropertyMemberAst propertyMember: + name = propertyMember.Name; + break; + case FunctionMemberAst functionMember: + name = functionMember.Name; + break; + case DotNetAdapter.MethodCacheEntry methodCacheEntry: + name = methodCacheEntry[0].method.Name; + break; + } + + return name; + } + private static PSTypeName InferTypesFromNewObjectCommand(PseudoBindingInfo pseudoBinding) { if (pseudoBinding.BoundArguments.TryGetValue("TypeName", out var typeArgument)) @@ -1361,6 +1701,11 @@ private bool TryGetTypeFromMember( scriptBlock = scriptMethod.Script; break; } + case PSInferredProperty inferredProperty: + { + result.Add(inferredProperty.TypeName); + break; + } } if (scriptBlock != null) diff --git a/test/powershell/engine/Api/TypeInference.Tests.ps1 b/test/powershell/engine/Api/TypeInference.Tests.ps1 index a6fbb254655..bdc1c25a31b 100644 --- a/test/powershell/engine/Api/TypeInference.Tests.ps1 +++ b/test/powershell/engine/Api/TypeInference.Tests.ps1 @@ -352,8 +352,76 @@ Describe "Type inference Tests" -tags "CI" { } } - It "Infers typeof Select-Object when Member is ExpandProperty" { - $res = [AstTypeInference]::InferTypeOf( { Get-ChildItem | Select-Object -ExpandProperty Directory }.Ast) + 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 -Be "System.Management.Automation.PSObject#Attributes:BaseName:Directory:DirectoryName:FullName:IsReadOnly:Length:LinkType:Mode:Name:Target" + $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" } @@ -364,11 +432,47 @@ Describe "Type inference Tests" -tags "CI" { $res.Name | Should -Be "System.String" } - It "Don't Infer typeof Select-Object when projection is done" { - $res = [AstTypeInference]::InferTypeOf( { Get-ChildItem | Select-Object -Property Name}.Ast) - $res.Count | Should -Be 0 + 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)