From c91003cf8c51a0e026f7b6733742b51ab6dde6bc Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Tue, 20 Jun 2023 23:54:26 +0200 Subject: [PATCH 01/18] Improve variable type inference --- .../CommandCompletion/CompletionCompleters.cs | 10 +- .../engine/parser/TypeInferenceVisitor.cs | 552 +++++++++++++----- .../engine/Api/TypeInference.Tests.ps1 | 91 +++ 3 files changed, 487 insertions(+), 166 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 9dec8bcb3a5..0cfd8f06b83 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -5341,7 +5341,7 @@ private static void AddUniqueVariable(HashSet hashedResults, List s_varModificationCommands = new(StringComparer.OrdinalIgnoreCase) + internal static readonly HashSet s_varModificationCommands = new(StringComparer.OrdinalIgnoreCase) { "New-Variable", "nv", @@ -5350,13 +5350,13 @@ private static void AddUniqueVariable(HashSet hashedResults, List hashedResults, List s_localScopeCommandNames = new(StringComparer.OrdinalIgnoreCase) + internal static readonly HashSet s_localScopeCommandNames = new(StringComparer.OrdinalIgnoreCase) { "Microsoft.PowerShell.Core\\ForEach-Object", "ForEach-Object", diff --git a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs index 1e1ff771ac5..6752c6ba83f 100644 --- a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs +++ b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs @@ -1658,6 +1658,36 @@ private IEnumerable InferTypesFrom(MemberExpressionAst memberExpress return res; } + private static IEnumerable InferTypeFromRef(InvokeMemberExpressionAst invokeMember, ExpressionAst refArgument) + { + Type expressionClrType = (invokeMember.Expression as TypeExpressionAst)?.TypeName.GetReflectionType(); + string memberName = (invokeMember.Member as StringConstantExpressionAst)?.Value; + int argumentIndex = invokeMember.Arguments.IndexOf(refArgument); + if (expressionClrType is null || string.IsNullOrEmpty(memberName) || argumentIndex == -1) + { + yield break; + } + + foreach (var memberInfo in expressionClrType.GetMember(memberName)) + { + if (memberInfo.MemberType == MemberTypes.Method) + { + var methodInfo = memberInfo as MethodInfo; + var methodParams = methodInfo.GetParameters(); + if (methodParams.Length < argumentIndex) + { + continue; + } + + var paramCandidate = methodParams[argumentIndex]; + if (paramCandidate.IsOut) + { + yield return new PSTypeName(paramCandidate.ParameterType.GetElementType()); + } + } + } + } + private void GetTypesOfMembers( PSTypeName thisType, string memberName, @@ -2057,148 +2087,113 @@ private void InferTypeFrom(VariableExpressionAst variableExpressionAst, List)AstSearcher.FindAll( - parent, - ast => + for (int i = 0; i < SpecialVariables.AutomaticVariables.Length; i++) { - if (ast is ParameterAst || ast is AssignmentStatementAst || ast is CommandAst) + if (!astVariablePath.UnqualifiedPath.EqualsOrdinalIgnoreCase(SpecialVariables.AutomaticVariables[i])) { - return variableExpressionAst.AstAssignsToSameVariable(ast) - && ast.Extent.EndOffset < startOffset; + continue; } - if (ast is ForEachStatementAst) + var type = SpecialVariables.AutomaticVariableTypes[i]; + if (type != typeof(object)) { - return variableExpressionAst.AstAssignsToSameVariable(ast) - && ast.Extent.StartOffset < startOffset; + inferredTypes.Add(new PSTypeName(type)); } - return false; - }, - searchNestedScriptBlocks: true); + return; + } + } - foreach (var ast in targetAsts) + // This visitor + loop finds the start of the current scope and traverses top to bottom to find the nearest variable assignment. + // Then repeats the process for each parent scope. + var assignmentVisitor = new VariableAssignmentVisitor() + { + ScopeIsLocal = true, + LocalScopeOnly = variableExpressionAst.VariablePath.IsLocal || variableExpressionAst.VariablePath.IsPrivate, + StopSearchOffset = variableExpressionAst.Extent.StartOffset, + VariableTarget = variableExpressionAst + }; + while (parent is not null) { - if (ast is ParameterAst parameterAst) + if (parent is IParameterMetadataProvider) { - var currentCount = inferredTypes.Count; - inferredTypes.AddRange(InferTypes(parameterAst)); + parent.Visit(assignmentVisitor); - if (inferredTypes.Count != currentCount) + if (assignmentVisitor.LastConstraint is not null) { - return; + inferredTypes.Add(new PSTypeName(assignmentVisitor.LastConstraint)); + break; } - } - } - var assignAsts = targetAsts.OfType().ToArray(); + if (assignmentVisitor.LocalScopeOnly) + { + break; + } - // If any of the assignments lhs use a type constraint, then we use that. - // Otherwise, we use the rhs of the "nearest" assignment - for (int i = assignAsts.Length - 1; i >= 0; i--) - { - if (assignAsts[i].Left is ConvertExpressionAst lhsConvert) - { - inferredTypes.Add(new PSTypeName(lhsConvert.Type.TypeName)); - return; + assignmentVisitor.ScopeIsLocal = false; + assignmentVisitor.StopSearchOffset = parent.Extent.StartOffset; } - } - var foreachAst = targetAsts.OfType().FirstOrDefault(); - if (foreachAst != null) - { - inferredTypes.AddRange( - GetInferredEnumeratedTypes(InferTypes(foreachAst.Condition))); - return; - } - - var commandCompletionAst = targetAsts.OfType().FirstOrDefault(); - if (commandCompletionAst != null) - { - inferredTypes.AddRange(InferTypes(commandCompletionAst)); - return; + parent = parent.Parent; } - int smallestDiff = int.MaxValue; - AssignmentStatementAst closestAssignment = null; - foreach (var assignAst in assignAsts) + if (assignmentVisitor.LastConstraint is null) { - var endOffset = assignAst.Extent.EndOffset; - if ((startOffset - endOffset) < smallestDiff) + if (assignmentVisitor.LastAssignment is not null) { - smallestDiff = startOffset - endOffset; - closestAssignment = assignAst; + if (assignmentVisitor.EnumerateAssignment) + { + inferredTypes.AddRange(GetInferredEnumeratedTypes(InferTypes(assignmentVisitor.LastAssignment))); + } + else + { + if (assignmentVisitor.LastAssignment is ConvertExpressionAst convertExpression + && convertExpression.IsRef()) + { + if (convertExpression.Parent is InvokeMemberExpressionAst memberInvoke) + { + inferredTypes.AddRange(InferTypeFromRef(memberInvoke, convertExpression)); + } + } + else + { + inferredTypes.AddRange(InferTypes(assignmentVisitor.LastAssignment)); + } + } + } + else if (assignmentVisitor.LastAssignmentType is not null) + { + inferredTypes.Add(assignmentVisitor.LastAssignmentType); } - } - - if (closestAssignment != null) - { - inferredTypes.AddRange(InferTypes(closestAssignment.Right)); } if (_context.TryGetRepresentativeTypeNameFromExpressionSafeEval(variableExpressionAst, out var evalTypeName)) @@ -2553,98 +2548,333 @@ private static CommandBaseAst GetPreviousPipelineCommand(CommandAst commandAst) var i = pipe.PipelineElements.IndexOf(commandAst); return i != 0 ? pipe.PipelineElements[i - 1] : null; } - } - internal static class TypeInferenceExtension - { - public static bool EqualsOrdinalIgnoreCase(this string s, string t) + private sealed class VariableAssignmentVisitor : AstVisitor { - return string.Equals(s, t, StringComparison.OrdinalIgnoreCase); - } + internal bool LocalScopeOnly; + internal bool ScopeIsLocal; + internal VariableExpressionAst VariableTarget; + internal int StopSearchOffset; + internal ITypeName LastConstraint; + internal Ast LastAssignment; + internal bool EnumerateAssignment; + internal PSTypeName LastAssignmentType; + private int LastAssignmentOffset = -1; - public static IEnumerable GetGetterProperty(this Type type, string propertyName) - { - var res = new List(); - foreach (var m in type.GetMethods(BindingFlags.Public | BindingFlags.Instance)) + private void SetLastAssignment(Ast ast, bool enumerate = false) { - var name = m.Name; - // Equals without string allocation - if (name.Length == propertyName.Length + 4 - && name.StartsWith("get_") - && name.IndexOf(propertyName, 4, StringComparison.Ordinal) == 4) + if (LastAssignmentOffset < ast.Extent.StartOffset) { - res.Add(m); + LastAssignment = ast; + EnumerateAssignment = enumerate; + LastAssignmentOffset = ast.Extent.StartOffset; } } - return res; - } + private void SetLastAssignmentType(PSTypeName typeName, int assignmentOffset) + { + if (LastAssignmentOffset < assignmentOffset) + { + LastAssignment = null; + LastAssignmentType = typeName; + LastAssignmentOffset = assignmentOffset; + } + } - public static bool AstAssignsToSameVariable(this VariableExpressionAst variableAst, Ast ast) - { - var parameterAst = ast as ParameterAst; - var variableAstVariablePath = variableAst.VariablePath; - if (parameterAst != null) + private bool AssignsToTargetVar(VariableExpressionAst foundVar) + { + if (!foundVar.VariablePath.UnqualifiedPath.EqualsOrdinalIgnoreCase(VariableTarget.VariablePath.UnqualifiedPath)) + { + return false; + } + + int scopeIndex = foundVar.VariablePath.UserPath.IndexOf(':'); + string scopeName = scopeIndex == -1 ? string.Empty : foundVar.VariablePath.UserPath.Remove(scopeIndex); + return AssignsToTargetScope(scopeName); + } + + private bool AssignsToTargetVar(string userPath) { - return variableAstVariablePath.IsUnscopedVariable && - parameterAst.Name.VariablePath.UnqualifiedPath.Equals(variableAstVariablePath.UnqualifiedPath, StringComparison.OrdinalIgnoreCase) && - parameterAst.Parent.Parent.Extent.EndOffset > variableAst.Extent.StartOffset; + if (string.IsNullOrEmpty(userPath)) + { + return false; + } + + string scopeName; + string varName; + int scopeIndex = userPath.IndexOf(':'); + if (scopeIndex == -1) + { + scopeName = string.Empty; + varName = userPath; + } + else + { + scopeName = userPath.Remove(scopeIndex); + varName = userPath.Substring(scopeIndex + 1); + } + + if (!varName.EqualsOrdinalIgnoreCase(VariableTarget.VariablePath.UnqualifiedPath)) + { + return false; + } + + return AssignsToTargetScope(scopeName); + } + + private bool AssignsToTargetScope(string scopeName) + { + if (LocalScopeOnly) + { + return string.IsNullOrEmpty(scopeName) || scopeName.EqualsOrdinalIgnoreCase("Local") || scopeName.EqualsOrdinalIgnoreCase("Private"); + } + + return ScopeIsLocal || !(scopeName.EqualsOrdinalIgnoreCase("Local") || scopeName.EqualsOrdinalIgnoreCase("Private")); } - if (ast is ForEachStatementAst foreachAst) + public override AstVisitAction DefaultVisit(Ast ast) { - return variableAstVariablePath.IsUnscopedVariable && - foreachAst.Variable.VariablePath.UnqualifiedPath.Equals(variableAstVariablePath.UnqualifiedPath, StringComparison.OrdinalIgnoreCase); + if (ast.Extent.StartOffset >= StopSearchOffset) + { + return AstVisitAction.StopVisit; + } + + return AstVisitAction.Continue; } - if (ast is CommandAst commandAst) + public override AstVisitAction VisitAssignmentStatement(AssignmentStatementAst assignmentStatementAst) { - string[] variableParameters = { "PV", "PipelineVariable", "OV", "OutVariable" }; - StaticBindingResult bindingResult = StaticParameterBinder.BindCommand(commandAst, false, variableParameters); + if (assignmentStatementAst.Extent.StartOffset >= StopSearchOffset) + { + return AstVisitAction.StopVisit; + } - if (bindingResult != null) + if (assignmentStatementAst.Left is ConvertExpressionAst convertExpression) { - foreach (string commandVariableParameter in variableParameters) + if (convertExpression.Child is VariableExpressionAst variableExpression && AssignsToTargetVar(variableExpression)) { - if (bindingResult.BoundParameters.TryGetValue(commandVariableParameter, out ParameterBindingResult parameterBindingResult)) + LastConstraint = convertExpression.Type.TypeName; + } + } + else if (assignmentStatementAst.Left is VariableExpressionAst variableExpression && AssignsToTargetVar(variableExpression)) + { + SetLastAssignment(assignmentStatementAst.Right); + } + + return AstVisitAction.Continue; + } + + public override AstVisitAction VisitCommand(CommandAst commandAst) + { + if (commandAst.Extent.StartOffset >= StopSearchOffset) + { + return AstVisitAction.StopVisit; + } + + var commandName = commandAst.GetCommandName(); + if (commandName is not null && CompletionCompleters.s_varModificationCommands.Contains(commandName)) + { + StaticBindingResult bindingResult = StaticParameterBinder.BindCommand(commandAst, resolve: false, CompletionCompleters.s_varModificationParameters); + if (bindingResult is not null + && bindingResult.BoundParameters.TryGetValue("Name", out ParameterBindingResult variableName)) + { + var nameValue = variableName.ConstantValue as string; + if (AssignsToTargetVar(nameValue) + && bindingResult.BoundParameters.TryGetValue("Value", out ParameterBindingResult variableValue)) + { + SetLastAssignment(variableValue.Value); + return AstVisitAction.Continue; + } + } + } + + var bindResult = StaticParameterBinder.BindCommand(commandAst, resolve: false); + if (bindResult is not null) + { + foreach (var parameterName in CompletionCompleters.s_outVarParameters) + { + if (bindResult.BoundParameters.TryGetValue(parameterName, out ParameterBindingResult outVarBind)) { - if (string.Equals(variableAstVariablePath.UnqualifiedPath, (string)parameterBindingResult.ConstantValue, StringComparison.OrdinalIgnoreCase)) + var varName = outVarBind.ConstantValue as string; + if (AssignsToTargetVar(varName)) { - return true; + // The *Variable parameters actually always results in an ArrayList + // But to make type inference of individual elements better, we say it's a generic list. + switch (parameterName) + { + case "ErrorVariable": + case "ev": + SetLastAssignmentType(new PSTypeName(typeof(List)), commandAst.Extent.StartOffset); + break; + + case "WarningVariable": + case "wv": + SetLastAssignmentType(new PSTypeName(typeof(List)), commandAst.Extent.StartOffset); + break; + + case "InformationVariable": + case "iv": + SetLastAssignmentType(new PSTypeName(typeof(List)), commandAst.Extent.StartOffset); + break; + + case "OutVariable": + case "ov": + SetLastAssignment(commandAst); + break; + + default: + break; + } + + return AstVisitAction.Continue; + } + } + } + + if (commandAst.Parent is PipelineAst pipeline && pipeline.Extent.EndOffset > VariableTarget.Extent.StartOffset) + { + foreach (var parameterName in CompletionCompleters.s_pipelineVariableParameters) + { + if (bindResult.BoundParameters.TryGetValue(parameterName, out ParameterBindingResult pipeVarBind)) + { + var varName = pipeVarBind.ConstantValue as string; + if (AssignsToTargetVar(varName)) + { + SetLastAssignment(commandAst, enumerate: true); + return AstVisitAction.Continue; + } } } } } - return false; + return AstVisitAction.Continue; } - var assignmentAst = (AssignmentStatementAst)ast; - var lhs = assignmentAst.Left; - if (lhs is ConvertExpressionAst convertExpr) + public override AstVisitAction VisitParameter(ParameterAst parameterAst) { - lhs = convertExpr.Child; + if (parameterAst.Extent.StartOffset >= StopSearchOffset) + { + return AstVisitAction.StopVisit; + } + + if (AssignsToTargetVar(parameterAst.Name)) + { + foreach (AttributeBaseAst attribute in parameterAst.Attributes) + { + if (attribute is TypeConstraintAst typeConstraint) + { + LastConstraint = typeConstraint.TypeName; + return AstVisitAction.Continue; + } + } + } + + return AstVisitAction.Continue; } - if (lhs is not VariableExpressionAst varExpr) + public override AstVisitAction VisitForEachStatement(ForEachStatementAst forEachStatementAst) { - return false; + if (forEachStatementAst.Extent.StartOffset >= StopSearchOffset) + { + return AstVisitAction.StopVisit; + } + + if (AssignsToTargetVar(forEachStatementAst.Variable) && forEachStatementAst.Condition.Extent.EndOffset < VariableTarget.Extent.StartOffset) + { + SetLastAssignment(forEachStatementAst.Condition, enumerate: true); + } + + return AstVisitAction.Continue; } - var candidateVarPath = varExpr.VariablePath; - if (candidateVarPath.UserPath.Equals(variableAstVariablePath.UserPath, StringComparison.OrdinalIgnoreCase)) + public override AstVisitAction VisitConvertExpression(ConvertExpressionAst convertExpressionAst) { - return true; + if (convertExpressionAst.IsRef() + && convertExpressionAst.Child is VariableExpressionAst varAst + && AssignsToTargetVar(varAst)) + { + SetLastAssignment(convertExpressionAst); + } + + return AstVisitAction.Continue; } - // The following condition is making an assumption that at script scope, we didn't use $script:, but in the local scope, we did - // If we are searching anything other than script scope, this is wrong. - if (variableAstVariablePath.IsScript && variableAstVariablePath.UnqualifiedPath.Equals(candidateVarPath.UnqualifiedPath, StringComparison.OrdinalIgnoreCase)) + public override AstVisitAction VisitAttribute(AttributeAst attributeAst) { - return true; + // Attributes can't assign values to variables so they aren't interesting. + return AstVisitAction.SkipChildren; } - return false; + public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionAst scriptBlockExpressionAst) + { + Ast parent = scriptBlockExpressionAst.Parent; + // This loop checks if the scriptblock is used as a command, or an argument for a command, eg: ForEach-Object -Process {$Var1 = "Hello"}, {Var2 = $true} + while (true) + { + if (parent is CommandAst cmdAst) + { + string cmdName = cmdAst.GetCommandName(); + return CompletionCompleters.s_localScopeCommandNames.Contains(cmdName) + || (cmdAst.CommandElements[0] is ScriptBlockExpressionAst && cmdAst.InvocationOperator == TokenKind.Dot) + ? AstVisitAction.Continue + : AstVisitAction.SkipChildren; + } + + if (parent is not CommandExpressionAst and not PipelineAst and not StatementBlockAst and not ArrayExpressionAst and not ArrayLiteralAst) + { + return AstVisitAction.SkipChildren; + } + + parent = parent.Parent; + } + } + + public override AstVisitAction VisitDataStatement(DataStatementAst dataStatementAst) + { + if (dataStatementAst.Extent.StartOffset >= StopSearchOffset) + { + return AstVisitAction.StopVisit; + } + + if (AssignsToTargetVar(dataStatementAst.Variable) && dataStatementAst.Extent.EndOffset < VariableTarget.Extent.StartOffset) + { + SetLastAssignment(dataStatementAst.Body); + } + + return AstVisitAction.SkipChildren; + } + + public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) + { + return AstVisitAction.SkipChildren; + } + } + } + + internal static class TypeInferenceExtension + { + public static bool EqualsOrdinalIgnoreCase(this string s, string t) + { + return string.Equals(s, t, StringComparison.OrdinalIgnoreCase); + } + + public static IEnumerable GetGetterProperty(this Type type, string propertyName) + { + var res = new List(); + foreach (var m in type.GetMethods(BindingFlags.Public | BindingFlags.Instance)) + { + var name = m.Name; + // Equals without string allocation + if (name.Length == propertyName.Length + 4 + && name.StartsWith("get_") + && name.IndexOf(propertyName, 4, StringComparison.Ordinal) == 4) + { + res.Add(m); + } + } + + return res; } } } diff --git a/test/powershell/engine/Api/TypeInference.Tests.ps1 b/test/powershell/engine/Api/TypeInference.Tests.ps1 index 69817f6290d..7f95ede296c 100644 --- a/test/powershell/engine/Api/TypeInference.Tests.ps1 +++ b/test/powershell/engine/Api/TypeInference.Tests.ps1 @@ -1432,6 +1432,97 @@ Describe "Type inference Tests" -tags "CI" { ) $null = [AstTypeInference]::InferTypeOf($FoundAst) } + + It 'Infers type of ref assigned variable' { + $res = [AstTypeInference]::InferTypeOf(({ + $MyRefVar = $null + $null = [System.Management.Automation.Language.Parser]::ParseInput("", [ref] $MyRefVar, [ref] $null) + $MyRefVar + }.Ast.FindAll({param($Ast) $Ast -is [Language.VariableExpressionAst]}, $true) | Select-Object -Last 1 )) + $res.Name | Should -Be 'System.Management.Automation.Language.Token[]' + } + + It 'Infers type of variable assigned with New/Set-Variable' { + $res = [AstTypeInference]::InferTypeOf( { + New-Variable -Name Var1 -Value $true | Out-Null + New-Variable -Name Var2 -Value "Hello" | Out-Null + $Var1 + $Var2 + }.Ast) + $res[0].Name | Should -Be 'System.Boolean' + $res[1].Name | Should -Be 'System.String' + } + + It 'Infers type of variable assigned with common parameter' -TestCases @( + @{TestId = 1; ParameterName = "WarningVariable"; ExpectedType = [List[WarningRecord]]} + @{TestId = 1; ParameterName = "wv"; ExpectedType = [List[WarningRecord]]} + @{TestId = 1; ParameterName = "ErrorVariable"; ExpectedType = [List[ErrorRecord]]} + @{TestId = 1; ParameterName = "ev"; ExpectedType = [List[ErrorRecord]]} + @{TestId = 1; ParameterName = "InformationVariable"; ExpectedType = [List[InformationalRecord]]} + @{TestId = 1; ParameterName = "iv"; ExpectedType = [List[InformationalRecord]]} + @{TestId = 1; ParameterName = "OutVariable"; ExpectedType = [guid]} + @{TestId = 1; ParameterName = "ov"; ExpectedType = [guid]} + @{TestId = 2; ParameterName = "PipelineVariable"; ExpectedType = [guid]} + @{TestId = 2; ParameterName = "pv"; ExpectedType = [guid]} + ) -Test { + param($TestId, $ParameterName, $ExpectedType) + $Ast = if ($TestId -eq 1) + { + [scriptblock]::Create("New-Guid -$ParameterName MyOutVar | Out-Null; `$MyOutVar").Ast + } + else + { + [scriptblock]::Create("New-Guid -$ParameterName MyOutVar | % {`$MyOutVar}").Ast + } + $res = [AstTypeInference]::InferTypeOf($Ast) + $res.Type | Should -Be $ExpectedType + } + + It 'Infers type of variable assigned via Data statement' { + $res = [AstTypeInference]::InferTypeOf(({ + Data MyDataVar {"Hello"} + $MyDataVar + }.Ast.FindAll({param($Ast) $Ast -is [Language.VariableExpressionAst]}, $true) | Select-Object -Last 1 )) + $res.Name | Should -Be 'System.String' + } + + It 'Infers parameter type from closest parameter' { + $res = [AstTypeInference]::InferTypeOf( ({ + param([string]$Param1) + function TestFunction {param([bool]$Param1) $Param1} + }.Ast.FindAll({param($Ast) $Ast -is [Language.VariableExpressionAst]}, $true) | Select-Object -Last 1 )) + $res.Name | Should -Be 'System.Boolean' + } + + It 'Infers variable type from closest foreach statement' { + $res = [AstTypeInference]::InferTypeOf( ({ + foreach ($X in 1..10) + { + $X + } + foreach ($X in New-Guid) + { + $X + } + }.Ast.FindAll({param($Ast) $Ast -is [Language.VariableExpressionAst]}, $true) | Select-Object -Last 1 )) + $res.Name | Should -Be 'System.Guid' + } + + It 'Infers global variable type in child scope' { + $res = [AstTypeInference]::InferTypeOf( ({ + $Global:GlobalTest1 = "Hello" + function TestFunction {$GlobalTest1} + }.Ast.FindAll({param($Ast) $Ast -is [Language.VariableExpressionAst]}, $true) | Select-Object -Last 1 )) + $res.Name | Should -Be 'System.String' + } + + It 'Does not infer private variable type in child scope' { + $res = [AstTypeInference]::InferTypeOf( ({ + $Private:PrivateTest1 = "Hello" + function TestFunction {$PrivateTest1} + }.Ast.FindAll({param($Ast) $Ast -is [Language.VariableExpressionAst]}, $true) | Select-Object -Last 1 )) + $res.Count | Should -Be 0 + } } Describe "AstTypeInference tests" -Tags CI { From e2a4a570b5e97fca97dbca77360df9286bb3bf5e Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Wed, 19 Feb 2025 17:00:08 +0100 Subject: [PATCH 02/18] Handle type inference for attributed assignments --- .../engine/parser/TypeInferenceVisitor.cs | 27 ++++++++++++++++--- .../engine/Api/TypeInference.Tests.ps1 | 16 +++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs index ac7bebe2d84..47207ff3774 100644 --- a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs +++ b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs @@ -2701,11 +2701,32 @@ public override AstVisitAction VisitAssignmentStatement(AssignmentStatementAst a return AstVisitAction.StopVisit; } - if (assignmentStatementAst.Left is ConvertExpressionAst convertExpression) + if (assignmentStatementAst.Left is AttributedExpressionAst attributedExpression) { - if (convertExpression.Child is VariableExpressionAst variableExpression && AssignsToTargetVar(variableExpression)) + var firstConvertExpression = attributedExpression as ConvertExpressionAst; + ExpressionAst child = attributedExpression.Child; + while (child is AttributedExpressionAst attributeChild) { - LastConstraint = convertExpression.Type.TypeName; + if (firstConvertExpression is null && attributeChild is ConvertExpressionAst convertExpression) + { + // Multiple type constraint can be set on a variable like this: [int] [string] $Var1 = 1 + // But it's the left most type constraint that determines the final type. + firstConvertExpression = convertExpression; + } + + child = attributeChild.Child; + } + + if (child is VariableExpressionAst variableExpression && AssignsToTargetVar(variableExpression)) + { + if (firstConvertExpression is not null) + { + LastConstraint = firstConvertExpression.Type.TypeName; + } + else + { + SetLastAssignment(assignmentStatementAst.Right); + } } } else if (assignmentStatementAst.Left is VariableExpressionAst variableExpression && AssignsToTargetVar(variableExpression)) diff --git a/test/powershell/engine/Api/TypeInference.Tests.ps1 b/test/powershell/engine/Api/TypeInference.Tests.ps1 index 5d55d183e93..ffedda9f870 100644 --- a/test/powershell/engine/Api/TypeInference.Tests.ps1 +++ b/test/powershell/engine/Api/TypeInference.Tests.ps1 @@ -1533,6 +1533,22 @@ Describe "Type inference Tests" -tags "CI" { $res.Count | Should -Be 0 } + It 'Infers variable assigned with an attribute' { + $res = [AstTypeInference]::InferTypeOf( ({ + [ValidateNotNull()]$ValidatedVar1 = New-Guid + $ValidatedVar1 + }.Ast.FindAll({param($Ast) $Ast -is [Language.VariableExpressionAst]}, $true) | Select-Object -Last 1 )) + $res.Name | Should -Be 'System.Guid' + } + + It 'Infers variable assigned with multiple type constraints' { + $res = [AstTypeInference]::InferTypeOf( ({ + [int] [string]$MultiConstraintVar1 = "10" + $MultiConstraintVar1 + }.Ast.FindAll({param($Ast) $Ast -is [Language.VariableExpressionAst]}, $true) | Select-Object -Last 1 )) + $res.Name | Should -Be 'System.Int32' + } + It 'Should only consider assignments wrapped in parentheses to be a part of the output in a Named block' { $res = [AstTypeInference]::InferTypeOf( { [string]$Assignment1 = "Hello"; ([int]$Assignment2 = 42) }.Ast) $res.Count | Should -Be 1 From 4630c8c89ad257862a3379455fcb79e1bf3ea158 Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Wed, 19 Feb 2025 18:03:05 +0100 Subject: [PATCH 03/18] Replace "var" with actual type --- .../engine/parser/TypeInferenceVisitor.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs index 47207ff3774..a111cab9408 100644 --- a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs +++ b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs @@ -1709,18 +1709,18 @@ private static IEnumerable InferTypeFromRef(InvokeMemberExpressionAs yield break; } - foreach (var memberInfo in expressionClrType.GetMember(memberName)) + foreach (MemberInfo memberInfo in expressionClrType.GetMember(memberName)) { if (memberInfo.MemberType == MemberTypes.Method) { var methodInfo = memberInfo as MethodInfo; - var methodParams = methodInfo.GetParameters(); + ParameterInfo[] methodParams = methodInfo.GetParameters(); if (methodParams.Length < argumentIndex) { continue; } - var paramCandidate = methodParams[argumentIndex]; + ParameterInfo paramCandidate = methodParams[argumentIndex]; if (paramCandidate.IsOut) { yield return new PSTypeName(paramCandidate.ParameterType.GetElementType()); @@ -2145,7 +2145,7 @@ private void InferTypeFrom(VariableExpressionAst variableExpressionAst, List VariableTarget.Extent.StartOffset) { - foreach (var parameterName in CompletionCompleters.s_pipelineVariableParameters) + foreach (string parameterName in CompletionCompleters.s_pipelineVariableParameters) { if (bindResult.BoundParameters.TryGetValue(parameterName, out ParameterBindingResult pipeVarBind)) { From 4fd7abf1f31f7d36c4fd2c427f9dab2928078161 Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Mon, 24 Feb 2025 20:06:21 +0100 Subject: [PATCH 04/18] Fix type inference for vars assigned in Do loops. --- .../engine/parser/TypeInferenceVisitor.cs | 12 +++++++--- .../engine/Api/TypeInference.Tests.ps1 | 24 +++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs index a111cab9408..bae1341c900 100644 --- a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs +++ b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs @@ -2601,7 +2601,7 @@ private static CommandBaseAst GetPreviousPipelineCommand(CommandAst commandAst) return i != 0 ? pipe.PipelineElements[i - 1] : null; } - private sealed class VariableAssignmentVisitor : AstVisitor + private sealed class VariableAssignmentVisitor : AstVisitor2 { internal bool LocalScopeOnly; internal bool ScopeIsLocal; @@ -2688,7 +2688,11 @@ public override AstVisitAction DefaultVisit(Ast ast) { if (ast.Extent.StartOffset >= StopSearchOffset) { - return AstVisitAction.StopVisit; + // When visiting do while/until statements, the condition will be visited before the statement block + // The condition itself may not be interesting if it's after the cursor, but the statement block could be + return ast is PipelineBaseAst && ast.Parent is DoUntilStatementAst or DoWhileStatementAst + ? AstVisitAction.SkipChildren + : AstVisitAction.StopVisit; } return AstVisitAction.Continue; @@ -2698,7 +2702,9 @@ public override AstVisitAction VisitAssignmentStatement(AssignmentStatementAst a { if (assignmentStatementAst.Extent.StartOffset >= StopSearchOffset) { - return AstVisitAction.StopVisit; + return assignmentStatementAst.Parent is DoUntilStatementAst or DoWhileStatementAst + ? AstVisitAction.SkipChildren + : AstVisitAction.StopVisit; } if (assignmentStatementAst.Left is AttributedExpressionAst attributedExpression) diff --git a/test/powershell/engine/Api/TypeInference.Tests.ps1 b/test/powershell/engine/Api/TypeInference.Tests.ps1 index ffedda9f870..e84c43527ce 100644 --- a/test/powershell/engine/Api/TypeInference.Tests.ps1 +++ b/test/powershell/engine/Api/TypeInference.Tests.ps1 @@ -1429,6 +1429,30 @@ Describe "Type inference Tests" -tags "CI" { $res.Name | Should -Be 'System.Management.Automation.Internal.Host.InternalHost' } + It 'Infers type of variable assigned inside do while loop' { + $res = [AstTypeInference]::InferTypeOf(({ + do + { + $Test = 1 + $Test + } + while (1) + }.Ast.FindAll({param($Ast) $Ast -is [Language.VariableExpressionAst]}, $true) | Select-Object -Last 1 )) + $res.Name | Should -Be 'System.Int32' + } + + It 'Infers type of variable assigned inside do until loop' { + $res = [AstTypeInference]::InferTypeOf(({ + do + { + $Test = 1 + $Test + } + until ($null = gci) + }.Ast.FindAll({param($Ast) $Ast -is [Language.VariableExpressionAst] -and $Ast.VariablePath.UserPath -eq 'Test'}, $true) | Select-Object -Last 1 )) + $res.Name | Should -Be 'System.Int32' + } + It 'Infers type of external applications' { $res = [AstTypeInference]::InferTypeOf( { pwsh }.Ast) $res.Name | Should -Be 'System.String' From 2a9825d04fb82d72ad11d38935a668e10aa66d5e Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Fri, 28 Feb 2025 23:56:47 +0100 Subject: [PATCH 05/18] Handle redirected vars --- .../engine/parser/TypeInferenceVisitor.cs | 54 +++++++++++++++++-- .../engine/Api/TypeInference.Tests.ps1 | 41 ++++++++++++++ 2 files changed, 92 insertions(+), 3 deletions(-) diff --git a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs index 1d4b169fc0f..43117598fff 100644 --- a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs +++ b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs @@ -1272,7 +1272,7 @@ object ICustomAstVisitor.VisitFileRedirection(FileRedirectionAst fileRedirection return TypeInferenceContext.EmptyPSTypeNameArray; } - private void InferTypesFrom(CommandAst commandAst, List inferredTypes) + private void InferTypesFrom(CommandAst commandAst, List inferredTypes, bool forRedirection = false) { if (commandAst.Redirections.Count > 0) { @@ -1282,7 +1282,7 @@ private void InferTypesFrom(CommandAst commandAst, List inferredType { if (streamRedirection is FileRedirectionAst fileRedirection) { - if (fileRedirection.FromStream is RedirectionStream.All or RedirectionStream.Output) + if (!forRedirection && fileRedirection.FromStream is RedirectionStream.All or RedirectionStream.Output) { // command output is redirected so it returns nothing. return; @@ -2489,6 +2489,10 @@ private void InferTypeFrom(VariableExpressionAst variableExpressionAst, List "variable:".Length) + { + string varName = redirectTarget.Value.Substring("variable:".Length); + if (!AssignsToTargetVar(varName)) + { + continue; + } + + switch (fileRedirection.FromStream) + { + case RedirectionStream.Error: + SetLastAssignmentType(new PSTypeName(typeof(ErrorRecord)), commandAst.Extent.StartOffset); + break; + + case RedirectionStream.Warning: + SetLastAssignmentType(new PSTypeName(typeof(WarningRecord)), commandAst.Extent.StartOffset); + break; + + case RedirectionStream.Verbose: + SetLastAssignmentType(new PSTypeName(typeof(VerboseRecord)), commandAst.Extent.StartOffset); + break; + + case RedirectionStream.Debug: + SetLastAssignmentType(new PSTypeName(typeof(DebugRecord)), commandAst.Extent.StartOffset); + break; + + case RedirectionStream.Information: + SetLastAssignmentType(new PSTypeName(typeof(InformationRecord)), commandAst.Extent.StartOffset); + break; + + default: + SetLastAssignment(commandAst, redirectionAssignment: true); + break; + } + } + } + return AstVisitAction.Continue; } diff --git a/test/powershell/engine/Api/TypeInference.Tests.ps1 b/test/powershell/engine/Api/TypeInference.Tests.ps1 index 9057aec3e22..43dbfea7c7a 100644 --- a/test/powershell/engine/Api/TypeInference.Tests.ps1 +++ b/test/powershell/engine/Api/TypeInference.Tests.ps1 @@ -1625,6 +1625,47 @@ Describe "Type inference Tests" -tags "CI" { $res.Name | Should -Be 'System.Int32' } + It 'Infers variable assigned by redirection' { + $res = [AstTypeInference]::InferTypeOf( ({ + New-Guid *>&1 1>variable:RedirVar1; $RedirVar1 + }.Ast.FindAll({param($Ast) $Ast -is [Language.VariableExpressionAst]}, $true) | Select-Object -Last 1 )) + $ExpectedTypeNames = @( + [ErrorRecord].FullName + [WarningRecord].FullName + [VerboseRecord].FullName + [DebugRecord].FullName + [InformationRecord].FullName + [guid].FullName + ) -join ';' + $res.Name -join ';' | Should -Be $ExpectedTypeNames + } + + It 'Infers variables assigned by redirection from specific streams' { + $VarAsts = [List[Language.Ast]]{ + [void](New-Guid 1>variable:RedirSuccess 2>variable:RedirError 3>variable:RedirWarning 4>variable:RedirVerbose 5>variable:RedirDebug 6>variable:RedirInfo) + $RedirSuccess + $RedirError + $RedirWarning + $RedirVerbose + $RedirDebug + $RedirInfo + }.Ast.FindAll({param($Ast) $Ast -is [Language.VariableExpressionAst]}, $true) + $ExpectedTypeNames = @( + [guid].FullName + [ErrorRecord].FullName + [WarningRecord].FullName + [VerboseRecord].FullName + [DebugRecord].FullName + [InformationRecord].FullName + ) + + for ($i = 0; $i -lt $VarAsts.Count; $i++) + { + $res = [AstTypeInference]::InferTypeOf($VarAsts[$i]) + $res.Name | Should -Be $ExpectedTypeNames[$i] + } + } + It 'Should infer output from anonymous function' { $res = [AstTypeInference]::InferTypeOf( { & {"Hello"} }.Ast) $res.Name | Should -Be 'System.String' From 66449edd656e676b70041746669d707d9128125a Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Sat, 1 Mar 2025 00:18:43 +0100 Subject: [PATCH 06/18] Add comments --- .../engine/parser/TypeInferenceVisitor.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs index 43117598fff..b4c4c453bae 100644 --- a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs +++ b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs @@ -2862,13 +2862,32 @@ private sealed class VariableAssignmentVisitor : AstVisitor2 { internal bool LocalScopeOnly; internal bool ScopeIsLocal; + /// + /// The variable that we are trying to determine the type of. + /// internal VariableExpressionAst VariableTarget; internal int StopSearchOffset; + /// + /// The last type constraint applied to the variable. This takes priority when determining the type of the variable. + /// internal ITypeName LastConstraint; + /// + /// The last ast that assigned a value to the variable. This determines the value of the variable unless a type constraint has been applied. + /// internal Ast LastAssignment; + /// + /// The inferred type from the most recent assignment. This is only used for stream redirections to variables, or the special OutVariable common parameters. + /// + internal PSTypeName LastAssignmentType; + /// + /// Whether or not the types from the last assignment should be enumerated. + /// For assignments made by the PipelineVariable parameter or the foreach statement. + /// internal bool EnumerateAssignment; + /// + /// Whether or not the last assignment was via command redirection. + /// internal bool RedirectionAssignment; - internal PSTypeName LastAssignmentType; private int LastAssignmentOffset = -1; private void SetLastAssignment(Ast ast, bool enumerate = false, bool redirectionAssignment = false) From 96daaa8cb5d97ddd350aa7d910ecb2a1eb9fa598 Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Mon, 3 Mar 2025 00:39:53 +0100 Subject: [PATCH 07/18] Address feedback --- .../engine/parser/TypeInferenceVisitor.cs | 48 +++++++++---------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs index b4c4c453bae..ad836f456dc 100644 --- a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs +++ b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs @@ -2402,40 +2402,38 @@ private void InferTypeFrom(VariableExpressionAst variableExpressionAst, List Date: Mon, 3 Mar 2025 09:51:34 +0100 Subject: [PATCH 08/18] Apply suggestions from code review Co-authored-by: Ilya --- .../engine/parser/TypeInferenceVisitor.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs index ad836f456dc..ab9b8094a26 100644 --- a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs +++ b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs @@ -2394,7 +2394,7 @@ private void InferTypeFrom(VariableExpressionAst variableExpressionAst, List internal VariableExpressionAst VariableTarget; internal int StopSearchOffset; + /// /// The last type constraint applied to the variable. This takes priority when determining the type of the variable. /// @@ -3206,6 +3207,7 @@ public override AstVisitAction VisitAttribute(AttributeAst attributeAst) public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionAst scriptBlockExpressionAst) { Ast parent = scriptBlockExpressionAst.Parent; + // This loop checks if the scriptblock is used as a command, or an argument for a command, eg: ForEach-Object -Process {$Var1 = "Hello"}, {Var2 = $true} while (true) { From f2b805845fe9c39f18bd60c8082540f6af0c8148 Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Mon, 3 Mar 2025 10:14:20 +0100 Subject: [PATCH 09/18] Address feedback --- .../engine/parser/TypeInferenceVisitor.cs | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs index ab9b8094a26..4b87066554e 100644 --- a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs +++ b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs @@ -2469,6 +2469,7 @@ private void InferTypeFrom(VariableExpressionAst variableExpressionAst, List + /// If set, we only look for local/private assignments in the scope of the variable we are inferring. + /// internal bool LocalScopeOnly; + + /// + /// The current scope is local to the variable that is being inferred. + /// internal bool ScopeIsLocal; + /// /// The variable that we are trying to determine the type of. /// internal VariableExpressionAst VariableTarget; - internal int StopSearchOffset; /// /// The last type constraint applied to the variable. This takes priority when determining the type of the variable. /// internal ITypeName LastConstraint; + /// /// The last ast that assigned a value to the variable. This determines the value of the variable unless a type constraint has been applied. /// internal Ast LastAssignment; + /// /// The inferred type from the most recent assignment. This is only used for stream redirections to variables, or the special OutVariable common parameters. /// internal PSTypeName LastAssignmentType; + /// /// Whether or not the types from the last assignment should be enumerated. /// For assignments made by the PipelineVariable parameter or the foreach statement. /// internal bool EnumerateAssignment; + /// /// Whether or not the last assignment was via command redirection. /// internal bool RedirectionAssignment; + internal int StopSearchOffset; private int LastAssignmentOffset = -1; private void SetLastAssignment(Ast ast, bool enumerate = false, bool redirectionAssignment = false) @@ -2952,14 +2965,9 @@ private bool AssignsToTargetVar(string userPath) } private bool AssignsToTargetScope(string scopeName) - { - if (LocalScopeOnly) - { - return string.IsNullOrEmpty(scopeName) || scopeName.EqualsOrdinalIgnoreCase("Local") || scopeName.EqualsOrdinalIgnoreCase("Private"); - } - - return ScopeIsLocal || !(scopeName.EqualsOrdinalIgnoreCase("Local") || scopeName.EqualsOrdinalIgnoreCase("Private")); - } + => LocalScopeOnly + ? string.IsNullOrEmpty(scopeName) || scopeName.EqualsOrdinalIgnoreCase("Local") || scopeName.EqualsOrdinalIgnoreCase("Private") + : ScopeIsLocal || !(scopeName.EqualsOrdinalIgnoreCase("Local") || scopeName.EqualsOrdinalIgnoreCase("Private")); public override AstVisitAction DefaultVisit(Ast ast) { From 4203f68c2a79d82a15064ccf3825d02d4dcee119 Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Mon, 3 Mar 2025 18:20:29 +0100 Subject: [PATCH 10/18] Update the scope logic --- .../engine/parser/TypeInferenceVisitor.cs | 112 ++++++++++-------- .../engine/Api/TypeInference.Tests.ps1 | 23 ++++ 2 files changed, 83 insertions(+), 52 deletions(-) diff --git a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs index 4b87066554e..d652c156481 100644 --- a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs +++ b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs @@ -2451,14 +2451,13 @@ private void InferTypeFrom(VariableExpressionAst variableExpressionAst, List GetGetterProperty(this Type type, string p return res; } + + public static bool IsDotsourced(this ScriptBlockExpressionAst scriptBlockExpressionAst) + { + Ast parent = scriptBlockExpressionAst.Parent; + + // This loop checks if the scriptblock is used as a dot sourced command + // or an argument for a command that uses the local scope eg: ForEach-Object -Process {$Var1 = "Hello"}, {Var2 = $true} + while (parent is not null) + { + if (parent is CommandAst cmdAst) + { + string cmdName = cmdAst.GetCommandName(); + return CompletionCompleters.s_localScopeCommandNames.Contains(cmdName) + || (cmdAst.CommandElements[0] is ScriptBlockExpressionAst && cmdAst.InvocationOperator == TokenKind.Dot); + } + + if (parent is not CommandExpressionAst and not PipelineAst and not StatementBlockAst and not ArrayExpressionAst and not ArrayLiteralAst) + { + break; + } + + parent = parent.Parent; + } + + return false; + } } } diff --git a/test/powershell/engine/Api/TypeInference.Tests.ps1 b/test/powershell/engine/Api/TypeInference.Tests.ps1 index 43dbfea7c7a..985f2d7437e 100644 --- a/test/powershell/engine/Api/TypeInference.Tests.ps1 +++ b/test/powershell/engine/Api/TypeInference.Tests.ps1 @@ -1518,6 +1518,29 @@ Describe "Type inference Tests" -tags "CI" { $null = [AstTypeInference]::InferTypeOf($FoundAst) } + It 'Ignores type constraint defined outside of scope' { + $res = [AstTypeInference]::InferTypeOf(({ + function Outer + { + [string] $Test = "Hello" + function Inner + { + $Test = 2 + $Test + } + } + }.Ast.FindAll({param($Ast) $Ast -is [Language.VariableExpressionAst]}, $true) | Select-Object -Last 1 )) + $res.Name | Should -Be 'System.Int32' + } + + It 'Considers the type constraint defined outside of scope when dot sourcing' { + $res = [AstTypeInference]::InferTypeOf(({ + [string] $Test = "Hello" + . {$Test = 2; $Test} + }.Ast.FindAll({param($Ast) $Ast -is [Language.VariableExpressionAst]}, $true) | Select-Object -Last 1 )) + $res.Name | Should -Be 'System.String' + } + It 'Infers type of ref assigned variable' { $res = [AstTypeInference]::InferTypeOf(({ $MyRefVar = $null From 548af1943ad6ef865210ac454ddd266d0846f019 Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Mon, 3 Mar 2025 21:11:55 +0100 Subject: [PATCH 11/18] Rename var and add comments --- .../engine/parser/TypeInferenceVisitor.cs | 52 ++++++++++--------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs index d652c156481..66a492555b3 100644 --- a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs +++ b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs @@ -2278,7 +2278,7 @@ private void InferTypeFrom(VariableExpressionAst variableExpressionAst, List 0) { if (switchErrorStatement.Conditions[0].Extent.EndOffset < variableExpressionAst.Extent.StartOffset) { - parent = switchErrorStatement.Conditions[0]; + currentAst = switchErrorStatement.Conditions[0]; break; } else { // $_ is inside the condition that is being declared, eg: Get-Process | Sort-Object -Property {switch ($_.Proc - parent = switchErrorStatement.Parent; + currentAst = switchErrorStatement.Parent; continue; } } break; } - else if (parent is ScriptBlockExpressionAst) + else if (currentAst is ScriptBlockExpressionAst) { hasSeenScriptBlock = true; } else if (hasSeenScriptBlock) { - if (parent is InvokeMemberExpressionAst invokeMember) + if (currentAst is InvokeMemberExpressionAst invokeMember) { - parent = invokeMember.Expression; + currentAst = invokeMember.Expression; break; } - else if (parent is CommandAst cmdAst && cmdAst.Parent is PipelineAst pipeline && pipeline.PipelineElements.Count > 1) + else if (currentAst is CommandAst cmdAst && cmdAst.Parent is PipelineAst pipeline && pipeline.PipelineElements.Count > 1) { // We've found a pipeline with multiple commands, now we need to determine what command came before the command with the scriptblock: // eg Get-Partition in this example: Get-Disk | Get-Partition | Where {$_} var indexOfPreviousCommand = pipeline.PipelineElements.IndexOf(cmdAst) - 1; if (indexOfPreviousCommand >= 0) { - parent = pipeline.PipelineElements[indexOfPreviousCommand]; + currentAst = pipeline.PipelineElements[indexOfPreviousCommand]; break; } } } - parent = parent.Parent; + currentAst = currentAst.Parent; } - if (parent is CatchClauseAst catchBlock) + if (currentAst is CatchClauseAst catchBlock) { if (catchBlock.CatchTypes.Count > 0) { @@ -2369,7 +2369,7 @@ private void InferTypeFrom(VariableExpressionAst variableExpressionAst, List Date: Tue, 4 Mar 2025 18:51:06 +0100 Subject: [PATCH 12/18] Fix test --- .../engine/Api/TypeInference.Tests.ps1 | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/test/powershell/engine/Api/TypeInference.Tests.ps1 b/test/powershell/engine/Api/TypeInference.Tests.ps1 index 985f2d7437e..3ae5c368e47 100644 --- a/test/powershell/engine/Api/TypeInference.Tests.ps1 +++ b/test/powershell/engine/Api/TypeInference.Tests.ps1 @@ -1562,26 +1562,22 @@ Describe "Type inference Tests" -tags "CI" { } It 'Infers type of variable assigned with common parameter' -TestCases @( - @{TestId = 1; ParameterName = "WarningVariable"; ExpectedType = [List[WarningRecord]]} - @{TestId = 1; ParameterName = "wv"; ExpectedType = [List[WarningRecord]]} - @{TestId = 1; ParameterName = "ErrorVariable"; ExpectedType = [List[ErrorRecord]]} - @{TestId = 1; ParameterName = "ev"; ExpectedType = [List[ErrorRecord]]} - @{TestId = 1; ParameterName = "InformationVariable"; ExpectedType = [List[InformationalRecord]]} - @{TestId = 1; ParameterName = "iv"; ExpectedType = [List[InformationalRecord]]} - @{TestId = 1; ParameterName = "OutVariable"; ExpectedType = [guid]} - @{TestId = 1; ParameterName = "ov"; ExpectedType = [guid]} - @{TestId = 2; ParameterName = "PipelineVariable"; ExpectedType = [guid]} - @{TestId = 2; ParameterName = "pv"; ExpectedType = [guid]} + @{ParameterName = "WarningVariable"; ExpectedType = [List[WarningRecord]]} + @{ParameterName = "wv"; ExpectedType = [List[WarningRecord]]} + @{ParameterName = "ErrorVariable"; ExpectedType = [List[ErrorRecord]]} + @{ParameterName = "ev"; ExpectedType = [List[ErrorRecord]]} + @{ParameterName = "InformationVariable"; ExpectedType = [List[InformationalRecord]]} + @{ParameterName = "iv"; ExpectedType = [List[InformationalRecord]]} + @{ParameterName = "OutVariable"; ExpectedType = [guid]} + @{ParameterName = "ov"; ExpectedType = [guid]} + @{ParameterName = "PipelineVariable"; ExpectedType = [guid]} + @{ParameterName = "pv"; ExpectedType = [guid]} ) -Test { param($TestId, $ParameterName, $ExpectedType) - $Ast = if ($TestId -eq 1) - { - [scriptblock]::Create("New-Guid -$ParameterName MyOutVar | Out-Null; `$MyOutVar").Ast - } - else - { - [scriptblock]::Create("New-Guid -$ParameterName MyOutVar | % {`$MyOutVar}").Ast - } + $Ast = [scriptblock]::Create("New-Guid -$ParameterName MyOutVar | % {`$MyOutVar}").Ast.FindAll({ + param($Ast) + $Ast -is [Language.VariableExpressionAst] + }, $true) | Select-Object -Last 1 $res = [AstTypeInference]::InferTypeOf($Ast) $res.Type | Should -Be $ExpectedType } From abe50cbaa8db0c2bbad133d48740fa216fd9414c Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Tue, 4 Mar 2025 18:56:24 +0100 Subject: [PATCH 13/18] Add missing test --- test/powershell/engine/Api/TypeInference.Tests.ps1 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/powershell/engine/Api/TypeInference.Tests.ps1 b/test/powershell/engine/Api/TypeInference.Tests.ps1 index 3ae5c368e47..45f20509d2b 100644 --- a/test/powershell/engine/Api/TypeInference.Tests.ps1 +++ b/test/powershell/engine/Api/TypeInference.Tests.ps1 @@ -1590,6 +1590,11 @@ Describe "Type inference Tests" -tags "CI" { $res.Name | Should -Be 'System.String' } + It 'Infers type of well known variable with global scope' { + $res = [AstTypeInference]::InferTypeOf({$global:true}.Ast) + $res.Name | Should -Be 'System.Boolean' + } + It 'Infers parameter type from closest parameter' { $res = [AstTypeInference]::InferTypeOf( ({ param([string]$Param1) From df646e095f2390e8070330ca092983912319bd1c Mon Sep 17 00:00:00 2001 From: MartinGC94 <42123497+MartinGC94@users.noreply.github.com> Date: Tue, 4 Mar 2025 18:56:47 +0100 Subject: [PATCH 14/18] Update test/powershell/engine/Api/TypeInference.Tests.ps1 Co-authored-by: Ilya --- test/powershell/engine/Api/TypeInference.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/powershell/engine/Api/TypeInference.Tests.ps1 b/test/powershell/engine/Api/TypeInference.Tests.ps1 index 45f20509d2b..2ab5681dddb 100644 --- a/test/powershell/engine/Api/TypeInference.Tests.ps1 +++ b/test/powershell/engine/Api/TypeInference.Tests.ps1 @@ -1573,7 +1573,7 @@ Describe "Type inference Tests" -tags "CI" { @{ParameterName = "PipelineVariable"; ExpectedType = [guid]} @{ParameterName = "pv"; ExpectedType = [guid]} ) -Test { - param($TestId, $ParameterName, $ExpectedType) + param($ParameterName, $ExpectedType) $Ast = [scriptblock]::Create("New-Guid -$ParameterName MyOutVar | % {`$MyOutVar}").Ast.FindAll({ param($Ast) $Ast -is [Language.VariableExpressionAst] From 24d76b8dc669ecef52081e7ef54663f6428186d3 Mon Sep 17 00:00:00 2001 From: MartinGC94 <42123497+MartinGC94@users.noreply.github.com> Date: Wed, 5 Mar 2025 22:38:31 +0100 Subject: [PATCH 15/18] Apply suggestions from code review Co-authored-by: Ilya --- .../engine/parser/TypeInferenceVisitor.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs index 66a492555b3..32f778f4297 100644 --- a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs +++ b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs @@ -2975,8 +2975,8 @@ public override AstVisitAction DefaultVisit(Ast ast) { if (ast.Extent.StartOffset >= StopSearchOffset) { - // When visiting do while/until statements, the condition will be visited before the statement block - // The condition itself may not be interesting if it's after the cursor, but the statement block could be + // When visiting do while/until statements, the condition will be visited before the statement block. + // The condition itself may not be interesting if it's after the cursor, but the statement block could be. return ast is PipelineBaseAst && ast.Parent is DoUntilStatementAst or DoWhileStatementAst ? AstVisitAction.SkipChildren : AstVisitAction.StopVisit; @@ -3044,8 +3044,7 @@ public override AstVisitAction VisitCommand(CommandAst commandAst) if (bindingResult is not null && bindingResult.BoundParameters.TryGetValue("Name", out ParameterBindingResult variableName)) { - var nameValue = variableName.ConstantValue as string; - if (AssignsToTargetVar(nameValue) + if (variableName.ConstantValue is string nameValue && AssignsToTargetVar(nameValue) && bindingResult.BoundParameters.TryGetValue("Value", out ParameterBindingResult variableValue)) { SetLastAssignment(variableValue.Value); From 2e2eec16f9d221ab69abeeae3db469ad36341eab Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Thu, 6 Mar 2025 16:09:55 +0100 Subject: [PATCH 16/18] Address feedback --- .../engine/parser/TypeInferenceVisitor.cs | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs index 66a492555b3..eb85fd7a3d3 100644 --- a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs +++ b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs @@ -2908,7 +2908,7 @@ private void SetLastAssignment(Ast ast, bool enumerate = false, bool redirection { if (LastAssignmentOffset < ast.Extent.StartOffset) { - LastAssignment = ast; + ClearAssignmentData(); EnumerateAssignment = enumerate; RedirectionAssignment = redirectionAssignment; LastAssignmentOffset = ast.Extent.StartOffset; @@ -2919,12 +2919,20 @@ private void SetLastAssignmentType(PSTypeName typeName, int assignmentOffset) { if (LastAssignmentOffset < assignmentOffset) { - LastAssignment = null; + ClearAssignmentData(); LastAssignmentType = typeName; LastAssignmentOffset = assignmentOffset; } } + private void ClearAssignmentData() + { + LastAssignment = null; + LastAssignmentType = null; + EnumerateAssignment = false; + RedirectionAssignment = false; + } + private bool AssignsToTargetVar(VariableExpressionAst foundVar) { if (!foundVar.VariablePath.UnqualifiedPath.EqualsOrdinalIgnoreCase(VariableTarget.VariablePath.UnqualifiedPath)) @@ -2977,6 +2985,13 @@ public override AstVisitAction DefaultVisit(Ast ast) { // When visiting do while/until statements, the condition will be visited before the statement block // The condition itself may not be interesting if it's after the cursor, but the statement block could be + // Example: + // do + // { + // $Var = gci + // $Var. + // } + // until($false) return ast is PipelineBaseAst && ast.Parent is DoUntilStatementAst or DoWhileStatementAst ? AstVisitAction.SkipChildren : AstVisitAction.StopVisit; @@ -3042,15 +3057,13 @@ public override AstVisitAction VisitCommand(CommandAst commandAst) { StaticBindingResult bindingResult = StaticParameterBinder.BindCommand(commandAst, resolve: false, CompletionCompleters.s_varModificationParameters); if (bindingResult is not null - && bindingResult.BoundParameters.TryGetValue("Name", out ParameterBindingResult variableName)) + && bindingResult.BoundParameters.TryGetValue("Name", out ParameterBindingResult variableName) + && variableName.ConstantValue is string nameValue + && AssignsToTargetVar(nameValue) + && bindingResult.BoundParameters.TryGetValue("Value", out ParameterBindingResult variableValue)) { - var nameValue = variableName.ConstantValue as string; - if (AssignsToTargetVar(nameValue) - && bindingResult.BoundParameters.TryGetValue("Value", out ParameterBindingResult variableValue)) - { - SetLastAssignment(variableValue.Value); - return AstVisitAction.Continue; - } + SetLastAssignment(variableValue.Value); + return AstVisitAction.Continue; } } @@ -3101,14 +3114,12 @@ public override AstVisitAction VisitCommand(CommandAst commandAst) { foreach (string parameterName in CompletionCompleters.s_pipelineVariableParameters) { - if (bindResult.BoundParameters.TryGetValue(parameterName, out ParameterBindingResult pipeVarBind)) + if (bindResult.BoundParameters.TryGetValue(parameterName, out ParameterBindingResult pipeVarBind) + && pipeVarBind.ConstantValue is string varName + && AssignsToTargetVar(varName)) { - var varName = pipeVarBind.ConstantValue as string; - if (AssignsToTargetVar(varName)) - { - SetLastAssignment(commandAst, enumerate: true); - return AstVisitAction.Continue; - } + SetLastAssignment(commandAst, enumerate: true); + return AstVisitAction.Continue; } } } From 4aa929c110a4595ae798d249b9e448d2aa16852e Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Thu, 6 Mar 2025 16:16:32 +0100 Subject: [PATCH 17/18] Add missing assignment --- .../engine/parser/TypeInferenceVisitor.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs index eb85fd7a3d3..f58fa1a3c97 100644 --- a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs +++ b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs @@ -2909,6 +2909,7 @@ private void SetLastAssignment(Ast ast, bool enumerate = false, bool redirection if (LastAssignmentOffset < ast.Extent.StartOffset) { ClearAssignmentData(); + LastAssignment = ast; EnumerateAssignment = enumerate; RedirectionAssignment = redirectionAssignment; LastAssignmentOffset = ast.Extent.StartOffset; From 8216056e766bbcf9411ff4ebdeb7738cd61c2c30 Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Thu, 6 Mar 2025 16:20:44 +0100 Subject: [PATCH 18/18] Fix another "as" statement --- .../engine/parser/TypeInferenceVisitor.cs | 60 +++++++++---------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs index f58fa1a3c97..6996b64eb2a 100644 --- a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs +++ b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs @@ -3073,41 +3073,39 @@ public override AstVisitAction VisitCommand(CommandAst commandAst) { foreach (string parameterName in CompletionCompleters.s_outVarParameters) { - if (bindResult.BoundParameters.TryGetValue(parameterName, out ParameterBindingResult outVarBind)) + if (bindResult.BoundParameters.TryGetValue(parameterName, out ParameterBindingResult outVarBind) + && outVarBind.ConstantValue is string varName + && AssignsToTargetVar(varName)) { - var varName = outVarBind.ConstantValue as string; - if (AssignsToTargetVar(varName)) + // The *Variable parameters actually always results in an ArrayList + // But to make type inference of individual elements better, we say it's a generic list. + switch (parameterName) { - // The *Variable parameters actually always results in an ArrayList - // But to make type inference of individual elements better, we say it's a generic list. - switch (parameterName) - { - case "ErrorVariable": - case "ev": - SetLastAssignmentType(new PSTypeName(typeof(List)), commandAst.Extent.StartOffset); - break; - - case "WarningVariable": - case "wv": - SetLastAssignmentType(new PSTypeName(typeof(List)), commandAst.Extent.StartOffset); - break; - - case "InformationVariable": - case "iv": - SetLastAssignmentType(new PSTypeName(typeof(List)), commandAst.Extent.StartOffset); - break; - - case "OutVariable": - case "ov": - SetLastAssignment(commandAst); - break; - - default: - break; - } + case "ErrorVariable": + case "ev": + SetLastAssignmentType(new PSTypeName(typeof(List)), commandAst.Extent.StartOffset); + break; - return AstVisitAction.Continue; + case "WarningVariable": + case "wv": + SetLastAssignmentType(new PSTypeName(typeof(List)), commandAst.Extent.StartOffset); + break; + + case "InformationVariable": + case "iv": + SetLastAssignmentType(new PSTypeName(typeof(List)), commandAst.Extent.StartOffset); + break; + + case "OutVariable": + case "ov": + SetLastAssignment(commandAst); + break; + + default: + break; } + + return AstVisitAction.Continue; } }