From b65bc629341a24f18f29602436b4b11774fed258 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Thu, 22 Jun 2017 22:34:33 +0200 Subject: [PATCH] Add type inference for Foreach-Object -MemberName (#3972) This PR adds type inference support for the 'MemberName' parameterset for Foreach-Object. Both of the following should work: ``` Get-Process | % {$_.MainModule} | % Com Get-Process | % MainModule | % Com ``` Before this change, only the first line worked, this PR addresses the second line. Fixes #2596 --- .../engine/parser/TypeInferenceVisitor.cs | 403 +++++++++++------- .../engine/Api/TypeInference.Tests.ps1 | 56 ++- 2 files changed, 302 insertions(+), 157 deletions(-) diff --git a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs index 0297f000c6..7369607174 100644 --- a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs +++ b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs @@ -62,7 +62,7 @@ namespace System.Management.Automation /// public static IList InferTypeOf(Ast ast, PowerShell powerShell) { - return InferTypeOf(ast, powerShell, TypeInferenceRuntimePermissions.None); + return InferTypeOf(ast, powerShell, TypeInferenceRuntimePermissions.None); } /// @@ -91,7 +91,7 @@ namespace System.Management.Automation try { context.RuntimePermissions = evalPersmissions; - return context.InferType(ast, new TypeInferenceVisitor(context)).ToList(); + return context.InferType(ast, new TypeInferenceVisitor(context)).Distinct(new PSTypeNameComparer()).ToList(); } finally { @@ -100,6 +100,19 @@ namespace System.Management.Automation } } + class PSTypeNameComparer : IEqualityComparer + { + public bool Equals(PSTypeName x, PSTypeName y) + { + return x.Name.Equals(y.Name); + } + + public int GetHashCode(PSTypeName obj) + { + return obj.Name.GetHashCode(); + } + } + internal class TypeInferenceContext { public static readonly PSTypeName[] EmptyPSTypeNameArray = Utils.EmptyArray(); @@ -122,6 +135,9 @@ namespace System.Management.Automation Helper = new PowerShellExecutionHelper(powerShell); } + // used to infer types in script properties attached to an object, + // to be able to determine the type of $this in the scripts properties + public PSTypeName CurrentThisType { get; set; } public TypeDefinitionAst CurrentTypeDefinitionAst { get; set; } @@ -868,7 +884,7 @@ namespace System.Management.Automation // foreach-object - yields the type of it's script block parameters if (cmdletInfo.ImplementingType == typeof(ForEachObjectCommand)) { - foreach (var foreachType in InferTypesFromForeachCommand(pseudoBinding)) + foreach (var foreachType in InferTypesFromForeachCommand(pseudoBinding, commandAst)) { yield return foreachType; } @@ -895,9 +911,32 @@ namespace System.Management.Automation yield return new PSTypeName(typeof(CimInstance)); } - private IEnumerable InferTypesFromForeachCommand(PseudoBindingInfo pseudoBinding) + private IEnumerable InferTypesFromForeachCommand(PseudoBindingInfo pseudoBinding, CommandAst commandAst) { AstParameterArgumentPair argument; + if (pseudoBinding.BoundArguments.TryGetValue("MemberName", out argument)) + { + var previousPipelineElement = GetPreviousPipelineCommand(commandAst); + if (previousPipelineElement == null) + { + yield break; + } + foreach (var t in InferTypes(previousPipelineElement)) + { + var memberName = (((AstPair)argument).Argument as StringConstantExpressionAst)?.Value; + + if (memberName != null) + { + var members = _context.GetMembersByInferredType(t, false, null); + bool maybeWantDefaultCtor = false; + foreach (var type in GetTypesOfMembers(t, memberName, members, ref maybeWantDefaultCtor, isInvokeMemberExpressionAst: false)) + { + yield return type; + } + } + } + } + if (pseudoBinding.BoundArguments.TryGetValue("Begin", out argument)) { foreach (var type in GetInferredTypeFromScriptBlockParameter(argument)) @@ -968,173 +1007,212 @@ namespace System.Management.Automation // If the member name isn't simple, don't even try. var memberAsStringConst = memberCommandElement as StringConstantExpressionAst; if (memberAsStringConst == null) - yield break; + return Utils.EmptyArray(); - var exprType = GetExpressionType(expression, isStatic); + var exprType = GetExpressionType(expression, isStatic); if (exprType == null || exprType.Length == 0) { - yield break; + return Utils.EmptyArray(); } - + + var res = new List(10); + bool isInvokeMemberExpressionAst = memberExpressionAst is InvokeMemberExpressionAst; var maybeWantDefaultCtor = isStatic - && memberExpressionAst is InvokeMemberExpressionAst - && memberAsStringConst.Value.EqualsOrdinalIgnoreCase("new"); + && isInvokeMemberExpressionAst + && memberAsStringConst.Value.EqualsOrdinalIgnoreCase("new"); // We use a list of member names because we might discover aliases properties // and if we do, we'll add to the list. var memberNameList = new List { memberAsStringConst.Value }; foreach (var type in exprType) { + if (type.Type == typeof(PSObject)) + { + continue; + } var members = _context.GetMembersByInferredType(type, isStatic, filter: null); - for (int i = 0; i < memberNameList.Count; i++) - { - string memberName = memberNameList[i]; - foreach (var member in members) - { - var isInvokeMemberAst = memberExpressionAst is InvokeMemberExpressionAst; - switch (member) - { - case PropertyInfo propertyInfo: // .net property - { - if (propertyInfo.Name.EqualsOrdinalIgnoreCase(memberName) && !isInvokeMemberAst) - { - yield return new PSTypeName(propertyInfo.PropertyType); - goto NextMember; - } - continue; - } - case FieldInfo fieldInfo: // .net field - { - if (fieldInfo.Name.EqualsOrdinalIgnoreCase(memberName) && !isInvokeMemberAst) - { - yield return new PSTypeName(fieldInfo.FieldType); - } - continue; - } - - case DotNetAdapter.MethodCacheEntry methodCacheEntry: // .net method - { - if (methodCacheEntry[0].method.Name.EqualsOrdinalIgnoreCase(memberName)) - { - maybeWantDefaultCtor = false; - if (isInvokeMemberAst) - { - foreach (var method in methodCacheEntry.methodInformationStructures) - { - var methodInfo = method.method as MethodInfo; - if (methodInfo != null && !methodInfo.ReturnType.GetTypeInfo().ContainsGenericParameters) - { - yield return new PSTypeName(methodInfo.ReturnType); - } - } - } - else - { - // Accessing a method as a property, we'd return a wrapper over the method. - yield return new PSTypeName(typeof(PSMethod)); - } - } - continue; - } - case MemberAst memberAst: // this is for members defined by PowerShell classes - { - if (memberAst.Name.EqualsOrdinalIgnoreCase(memberName)) - { - if (isInvokeMemberAst) - { - var functionMemberAst = memberAst as FunctionMemberAst; - if (functionMemberAst != null && !functionMemberAst.IsReturnTypeVoid()) - { - yield return new PSTypeName(functionMemberAst.ReturnType.TypeName); - } - } - else - { - var propertyMemberAst = memberAst as PropertyMemberAst; - if (propertyMemberAst != null) - { - if (propertyMemberAst.PropertyType != null) - { - yield return new PSTypeName(propertyMemberAst.PropertyType.TypeName); - } - else - { - yield return new PSTypeName(typeof(object)); - } - } - else - { - // Accessing a method as a property, we'd return a wrapper over the method. - yield return new PSTypeName(typeof(PSMethod)); - } - } - } - continue; - } - case PSMemberInfo memberInfo: - { - if (!memberInfo.Name.EqualsOrdinalIgnoreCase(memberName)) - { - continue; - } - switch (member) - { - case PSProperty p: - { - yield return new PSTypeName(p.Value.GetType()); - goto NextMember; - } - case PSNoteProperty noteProperty: - { - yield return new PSTypeName(noteProperty.Value.GetType()); - goto NextMember; - } - case PSAliasProperty aliasProperty: - { - memberNameList.Add(aliasProperty.ReferencedMemberName); - goto NextMember; - } - case PSCodeProperty codeProperty: - { - if (codeProperty.GetterCodeReference != null) - { - yield return new PSTypeName(codeProperty.GetterCodeReference.ReturnType); - } - goto NextMember; - } - case PSScriptProperty scriptProperty: - { - var scriptBlock = scriptProperty.GetterScript; - foreach (var t in scriptBlock.OutputType) - { - yield return t; - } - goto NextMember; - } - case PSScriptMethod scriptMethod: - { - var scriptBlock = scriptMethod.Script; - foreach (var t in scriptBlock.OutputType) - { - yield return t; - } - goto NextMember; - } - } - break; - } - } - } - NextMember: {} - } + AddTypesOfMembers(type, memberNameList, members, ref maybeWantDefaultCtor, isInvokeMemberExpressionAst, res); // We didn't find any constructors but they used [T]::new() syntax if (maybeWantDefaultCtor) { - yield return type; + res.Add(type); } } + return res; + } + + private List GetTypesOfMembers(PSTypeName thisType, string memberName, IList members, ref bool maybeWantDefaultCtor, bool isInvokeMemberExpressionAst) + { + var memberNamesToCheck = new List { memberName }; + var res = new List(10); + + AddTypesOfMembers(thisType, memberNamesToCheck, members, ref maybeWantDefaultCtor, isInvokeMemberExpressionAst, res); + return res; + } + + private void AddTypesOfMembers(PSTypeName currentType, List memberNamesToCheck, IList members, ref bool maybeWantDefaultCtor, bool isInvokeMemberExpressionAst, List result) + { + for (int i = 0; i < memberNamesToCheck.Count; i++) + { + string memberNameToCheck = memberNamesToCheck[i]; + foreach (var member in members) + { + if (TryGetTypeFromMember(currentType, member, memberNameToCheck, ref maybeWantDefaultCtor, isInvokeMemberExpressionAst, result, memberNamesToCheck)) + { + break; + } + } + } + } + + private bool TryGetTypeFromMember(PSTypeName currentType, object member, string memberName, ref bool maybeWantDefaultCtor, bool isInvokeMemberExpressionAst, List result, List memberNamesToCheck) + { + switch (member) + { + case PropertyInfo propertyInfo: // .net property + { + if (propertyInfo.Name.EqualsOrdinalIgnoreCase(memberName) && !isInvokeMemberExpressionAst) + { + result.Add(new PSTypeName(propertyInfo.PropertyType)); + return true; + } + return false; + } + case FieldInfo fieldInfo: // .net field + { + if (fieldInfo.Name.EqualsOrdinalIgnoreCase(memberName) && !isInvokeMemberExpressionAst) + { + result.Add(new PSTypeName(fieldInfo.FieldType)); + return true; + } + return false; + } + case DotNetAdapter.MethodCacheEntry methodCacheEntry: // .net method + { + if (methodCacheEntry[0].method.Name.Equals(memberName, StringComparison.OrdinalIgnoreCase)) + { + maybeWantDefaultCtor = false; + if (isInvokeMemberExpressionAst) + { + var res = (from method in methodCacheEntry.methodInformationStructures + select method.method as MethodInfo into methodInfo + where methodInfo != null && !methodInfo.ReturnType.GetTypeInfo().ContainsGenericParameters + select new PSTypeName(methodInfo.ReturnType)); + result.AddRange(res); + return true; + } + else + { + // Accessing a method as a property, we'd return a wrapper over the method. + result.Add(new PSTypeName(typeof(PSMethod))); + return true; + } + } + return false; + } + case MemberAst memberAst: // this is for members defined by PowerShell classes + { + if (memberAst.Name.Equals(memberName, StringComparison.OrdinalIgnoreCase)) + { + if (isInvokeMemberExpressionAst) + { + if (memberAst is FunctionMemberAst functionMemberAst && !functionMemberAst.IsReturnTypeVoid()) + { + result.Add(new PSTypeName(functionMemberAst.ReturnType.TypeName)); + return true; + } + } + else + { + if (memberAst is PropertyMemberAst propertyMemberAst) + { + result.Add(propertyMemberAst.PropertyType != null + ? new PSTypeName(propertyMemberAst.PropertyType.TypeName) + : new PSTypeName(typeof(object))); + return true; + } + else + { + // Accessing a method as a property, we'd return a wrapper over the method. + result.Add(new PSTypeName(typeof(PSMethod))); + return true; + } + } + } + return false; + } + case PSMemberInfo memberInfo: + { + if (!memberInfo.Name.Equals(memberName, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + ScriptBlock scriptBlock = null; + switch (memberInfo) + { + case PSProperty p: + { + result.Add(new PSTypeName(p.Value.GetType())); + return true; + } + case PSNoteProperty noteProperty: + { + result.Add(new PSTypeName(noteProperty.Value.GetType())); + return true; + } + case PSAliasProperty aliasProperty: + { + memberNamesToCheck.Add(aliasProperty.ReferencedMemberName); + return true; + } + case PSCodeProperty codeProperty: + { + if (codeProperty.GetterCodeReference != null) + { + result.Add(new PSTypeName(codeProperty.GetterCodeReference.ReturnType)); + } + return true; + } + case PSScriptProperty scriptProperty: + { + scriptBlock = scriptProperty.GetterScript; + break; + } + case PSScriptMethod scriptMethod: + { + scriptBlock = scriptMethod.Script; + break; + } + } + if (scriptBlock != null) + { + var thisToRestore = _context.CurrentThisType; + try + { + _context.CurrentThisType = currentType; + var outputType = scriptBlock.OutputType; + if (outputType != null && outputType.Count != 0) + { + result.AddRange(outputType); + return true; + } + else + { + result.AddRange(InferTypes(scriptBlock.Ast).ToArray()); + return true; + } + } + finally + { + _context.CurrentThisType = thisToRestore; + } + } + } + return false; + } + return false; } private PSTypeName[] GetExpressionType(ExpressionAst expression, bool isStatic) @@ -1264,8 +1342,8 @@ namespace System.Management.Automation // For certain variables, we always know their type, well at least we can assume we know. if (astVariablePath.IsUnqualified) { - if (!astVariablePath.UserPath.EqualsOrdinalIgnoreCase(SpecialVariables.This) || - _context.CurrentTypeDefinitionAst == null) + var isThis = astVariablePath.UserPath.EqualsOrdinalIgnoreCase(SpecialVariables.This); + if (!isThis || (_context.CurrentTypeDefinitionAst == null && _context.CurrentThisType == null)) { for (int i = 0; i < SpecialVariables.AutomaticVariables.Length; i++) { @@ -1279,10 +1357,18 @@ namespace System.Management.Automation } else { - yield return new PSTypeName(_context.CurrentTypeDefinitionAst); + yield return _context.CurrentThisType != null + ? _context.CurrentThisType + : new PSTypeName(_context.CurrentTypeDefinitionAst); yield break; } } + else + { + yield return new PSTypeName(_context.CurrentTypeDefinitionAst); + yield break; + } + // Look for our variable as a parameter or on the lhs of an assignment - hopefully we'll find either // a type constraint or at least we can use the rhs to infer the type. @@ -1488,6 +1574,13 @@ namespace System.Management.Automation // TODO: What is the right InferredType for the AST return dynamicKeywordAst.CommandElements[0].Accept(this); } + + private static CommandBaseAst GetPreviousPipelineCommand(CommandAst commandAst) + { + var pipe = (PipelineAst)commandAst.Parent; + var i = pipe.PipelineElements.IndexOf(commandAst); + return i != 0 ? pipe.PipelineElements[i - 1] : null; + } } static class TypeInferenceExtension diff --git a/test/powershell/engine/Api/TypeInference.Tests.ps1 b/test/powershell/engine/Api/TypeInference.Tests.ps1 index 65e9834ae5..a785ae9b2e 100644 --- a/test/powershell/engine/Api/TypeInference.Tests.ps1 +++ b/test/powershell/engine/Api/TypeInference.Tests.ps1 @@ -32,7 +32,6 @@ Describe "Type inference Tests" -tags "CI" { return $script:inferTypeOf4.Invoke($null, @($ast, $powerShell, $runtimePermissions)) } } - } It "Infers type from integer" { @@ -302,6 +301,58 @@ Describe "Type inference Tests" -tags "CI" { } } + 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 type from OutputTypeAttribute" { $res = [AstTypeInference]::InferTypeOf( { Get-Process -Id 2345 }.Ast) $gpsOutput = [Microsoft.PowerShell.Commands.GetProcessCommand].GetCustomAttributes([System.Management.Automation.OutputTypeAttribute], $false).Type @@ -565,7 +616,8 @@ Describe "Type inference Tests" -tags "CI" { [Y]::new().ScriptProp }.Ast) - $res.Count | Should be 0 + $res.Count | Should be 1 + $res.Name | Should be System.Int32 } It 'Infers type of script property with outputtype' {