Skip to content

Commit 33ed509

Browse files
authored
Add type inference for functions without OutputType attribute and anonymous functions (PowerShell#21127)
1 parent 2c769d6 commit 33ed509

File tree

2 files changed

+39
-2
lines changed

2 files changed

+39
-2
lines changed

src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs

+24
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ public TypeInferenceContext(PowerShell powerShell)
150150

151151
public TypeDefinitionAst CurrentTypeDefinitionAst { get; set; }
152152

153+
public HashSet<IParameterMetadataProvider> AnalyzedCommands { get; } = new HashSet<IParameterMetadataProvider>();
154+
153155
public TypeInferenceRuntimePermissions RuntimePermissions { get; set; }
154156

155157
internal PowerShellExecutionHelper Helper { get; }
@@ -1339,6 +1341,13 @@ private void InferTypesFrom(CommandAst commandAst, List<PSTypeName> inferredType
13391341
}
13401342
}
13411343

1344+
if (commandAst.CommandElements[0] is ScriptBlockExpressionAst scriptBlock)
1345+
{
1346+
// An anonymous function like: & {"Do Something"}
1347+
inferredTypes.AddRange(InferTypes(scriptBlock.ScriptBlock));
1348+
return;
1349+
}
1350+
13421351
PseudoBindingInfo pseudoBinding = new PseudoParameterBinder()
13431352
.DoPseudoParameterBinding(commandAst, null, null, PseudoParameterBinder.BindingType.ParameterCompletion);
13441353

@@ -1421,6 +1430,21 @@ private void InferTypesFrom(CommandAst commandAst, List<PSTypeName> inferredType
14211430
}
14221431
}
14231432

1433+
if ((commandInfo.OutputType.Count == 0
1434+
|| (commandInfo.OutputType.Count == 1
1435+
&& (commandInfo.OutputType[0].Name.EqualsOrdinalIgnoreCase(typeof(PSObject).FullName)
1436+
|| commandInfo.OutputType[0].Name.EqualsOrdinalIgnoreCase(typeof(object).FullName))))
1437+
&& commandInfo is IScriptCommandInfo scriptCommandInfo
1438+
&& scriptCommandInfo.ScriptBlock.Ast is IParameterMetadataProvider scriptBlockWithParams
1439+
&& _context.AnalyzedCommands.Add(scriptBlockWithParams))
1440+
{
1441+
// This is a function without an output type defined (or it's too generic to be useful)
1442+
// We can analyze the code inside the function to find out what it actually outputs
1443+
// The purpose of the hashset is to avoid infinite loops with functions that call themselves.
1444+
inferredTypes.AddRange(InferTypes(scriptBlockWithParams.Body));
1445+
return;
1446+
}
1447+
14241448
// The OutputType property ignores the parameter set specified in the OutputTypeAttribute.
14251449
// With pseudo-binding, we actually know the candidate parameter sets, so we could take
14261450
// advantage of it here, but I opted for the simpler code because so few cmdlets use

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

+15-2
Original file line numberDiff line numberDiff line change
@@ -650,8 +650,10 @@ Describe "Type inference Tests" -tags "CI" {
650650
}
651651

652652
It "Infers type from variable with AllowSafeEval" {
653-
function Hide-GetProcess { Get-Process }
654-
$p = Hide-GetProcess
653+
# Invoke-Expression is used to "hide" Get-Process from the type inference.
654+
# If the typeinference code is updated to handle Invoke-Expression, this test will need to find some other way to set $p
655+
# so that the type inference can't figure it out without evaluating the variable value
656+
$p = Invoke-Expression -Command 'Get-Process'
655657
$res = [AstTypeInference]::InferTypeOf( { $p }.Ast, [TypeInferenceRuntimePermissions]::AllowSafeEval)
656658
$res.Name | Should -Be 'System.Diagnostics.Process'
657659
}
@@ -1492,6 +1494,17 @@ Describe "Type inference Tests" -tags "CI" {
14921494
$null = [AstTypeInference]::InferTypeOf($FoundAst)
14931495
}
14941496

1497+
It 'Should infer output from anonymous function' {
1498+
$res = [AstTypeInference]::InferTypeOf( { & {"Hello"} }.Ast)
1499+
$res.Name | Should -Be 'System.String'
1500+
}
1501+
1502+
It 'Should infer output from function without OutputType attribute' {
1503+
function MyHello{"Hello"}
1504+
$res = [AstTypeInference]::InferTypeOf( { MyHello }.Ast)
1505+
$res.Name | Should -Be 'System.String'
1506+
}
1507+
14951508
It 'Infers type of command with all streams redirected to Success stream' {
14961509
$res = [AstTypeInference]::InferTypeOf( { Get-PSDrive *>&1 }.Ast)
14971510
$ExpectedTypeNames = @(

0 commit comments

Comments
 (0)