diff --git a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs index d010a4885..a1763755f 100644 --- a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs +++ b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs @@ -150,6 +150,8 @@ namespace System.Management.Automation public TypeDefinitionAst CurrentTypeDefinitionAst { get; set; } + public HashSet AnalyzedCommands { get; } = new HashSet(); + public TypeInferenceRuntimePermissions RuntimePermissions { get; set; } internal PowerShellExecutionHelper Helper { get; } @@ -1339,6 +1341,13 @@ namespace System.Management.Automation } } + if (commandAst.CommandElements[0] is ScriptBlockExpressionAst scriptBlock) + { + // An anonymous function like: & {"Do Something"} + inferredTypes.AddRange(InferTypes(scriptBlock.ScriptBlock)); + return; + } + PseudoBindingInfo pseudoBinding = new PseudoParameterBinder() .DoPseudoParameterBinding(commandAst, null, null, PseudoParameterBinder.BindingType.ParameterCompletion); @@ -1421,6 +1430,21 @@ namespace System.Management.Automation } } + if ((commandInfo.OutputType.Count == 0 + || (commandInfo.OutputType.Count == 1 + && (commandInfo.OutputType[0].Name.EqualsOrdinalIgnoreCase(typeof(PSObject).FullName) + || commandInfo.OutputType[0].Name.EqualsOrdinalIgnoreCase(typeof(object).FullName)))) + && commandInfo is IScriptCommandInfo scriptCommandInfo + && scriptCommandInfo.ScriptBlock.Ast is IParameterMetadataProvider scriptBlockWithParams + && _context.AnalyzedCommands.Add(scriptBlockWithParams)) + { + // This is a function without an output type defined (or it's too generic to be useful) + // We can analyze the code inside the function to find out what it actually outputs + // The purpose of the hashset is to avoid infinite loops with functions that call themselves. + inferredTypes.AddRange(InferTypes(scriptBlockWithParams.Body)); + return; + } + // The OutputType property ignores the parameter set specified in the OutputTypeAttribute. // With pseudo-binding, we actually know the candidate parameter sets, so we could take // advantage of it here, but I opted for the simpler code because so few cmdlets use diff --git a/test/powershell/engine/Api/TypeInference.Tests.ps1 b/test/powershell/engine/Api/TypeInference.Tests.ps1 index 39bdeedba..0d0188dc2 100644 --- a/test/powershell/engine/Api/TypeInference.Tests.ps1 +++ b/test/powershell/engine/Api/TypeInference.Tests.ps1 @@ -650,8 +650,10 @@ Describe "Type inference Tests" -tags "CI" { } It "Infers type from variable with AllowSafeEval" { - function Hide-GetProcess { Get-Process } - $p = Hide-GetProcess + # Invoke-Expression is used to "hide" Get-Process from the type inference. + # If the typeinference code is updated to handle Invoke-Expression, this test will need to find some other way to set $p + # so that the type inference can't figure it out without evaluating the variable value + $p = Invoke-Expression -Command 'Get-Process' $res = [AstTypeInference]::InferTypeOf( { $p }.Ast, [TypeInferenceRuntimePermissions]::AllowSafeEval) $res.Name | Should -Be 'System.Diagnostics.Process' } @@ -1492,6 +1494,17 @@ Describe "Type inference Tests" -tags "CI" { $null = [AstTypeInference]::InferTypeOf($FoundAst) } + It 'Should infer output from anonymous function' { + $res = [AstTypeInference]::InferTypeOf( { & {"Hello"} }.Ast) + $res.Name | Should -Be 'System.String' + } + + It 'Should infer output from function without OutputType attribute' { + function MyHello{"Hello"} + $res = [AstTypeInference]::InferTypeOf( { MyHello }.Ast) + $res.Name | Should -Be 'System.String' + } + It 'Infers type of command with all streams redirected to Success stream' { $res = [AstTypeInference]::InferTypeOf( { Get-PSDrive *>&1 }.Ast) $ExpectedTypeNames = @(