diff --git a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs index 0d160a6fb7..0e0355761a 100644 --- a/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs +++ b/src/System.Management.Automation/engine/parser/TypeInferenceVisitor.cs @@ -718,14 +718,176 @@ namespace System.Management.Automation object ICustomAstVisitor.VisitBinaryExpression(BinaryExpressionAst binaryExpressionAst) { - // TODO: Handle other kinds of expressions on the right side. - if (binaryExpressionAst.Operator == TokenKind.As && binaryExpressionAst.Right is TypeExpressionAst typeExpression) + switch (binaryExpressionAst.Operator) { - var type = typeExpression.TypeName.GetReflectionType(); - var psTypeName = type != null ? new PSTypeName(type) : new PSTypeName(typeExpression.TypeName.FullName); - return new[] { psTypeName }; + case TokenKind.And: + case TokenKind.Ccontains: + case TokenKind.Cin: + case TokenKind.Cnotcontains: + case TokenKind.Cnotin: + case TokenKind.Icontains: + case TokenKind.Iin: + case TokenKind.Inotcontains: + case TokenKind.Inotin: + case TokenKind.Is: + case TokenKind.IsNot: + case TokenKind.Or: + case TokenKind.Xor: + // Always returns a bool + return BinaryExpressionAst.BoolTypeNameArray; + + case TokenKind.As: + // TODO: Handle other kinds of expressions on the right side. + if (binaryExpressionAst.Right is TypeExpressionAst typeExpression) + { + var type = typeExpression.TypeName.GetReflectionType(); + var psTypeName = type != null ? new PSTypeName(type) : new PSTypeName(typeExpression.TypeName.FullName); + return new[] { psTypeName }; + } + break; + + case TokenKind.Ceq: + case TokenKind.Cge: + case TokenKind.Cgt: + case TokenKind.Cle: + case TokenKind.Clike: + case TokenKind.Clt: + case TokenKind.Cmatch: + case TokenKind.Cne: + case TokenKind.Cnotlike: + case TokenKind.Cnotmatch: + case TokenKind.Ieq: + case TokenKind.Ige: + case TokenKind.Igt: + case TokenKind.Ile: + case TokenKind.Ilike: + case TokenKind.Ilt: + case TokenKind.Imatch: + case TokenKind.Ine: + case TokenKind.Inotlike: + case TokenKind.Inotmatch: + // Returns a bool or filtered output from the left hand side if it's enumerable + var comparisonOutput = new List() { new(typeof(bool)) }; + comparisonOutput.AddRange(InferTypes(binaryExpressionAst.Left)); + return comparisonOutput; + + case TokenKind.Creplace: + case TokenKind.Format: + case TokenKind.Ireplace: + case TokenKind.Join: + // Always returns a string + return BinaryExpressionAst.StringTypeNameArray; + + case TokenKind.Csplit: + case TokenKind.Isplit: + // Always returns a string array + return BinaryExpressionAst.StringArrayTypeNameArray; + + case TokenKind.QuestionQuestion: + // Can return left or right hand side + var nullCoalescingOutput = InferTypes(binaryExpressionAst.Left).ToList(); + nullCoalescingOutput.AddRange(InferTypes(binaryExpressionAst.Right)); + return nullCoalescingOutput.Distinct(); + + default: + break; } - return InferTypes(binaryExpressionAst.Left); + + List lhsTypes = InferTypes(binaryExpressionAst.Left).ToList(); + if (lhsTypes.Count == 0) + { + return lhsTypes; + } + + string methodName; + switch (binaryExpressionAst.Operator) + { + case TokenKind.Divide: + methodName = "op_Division"; + break; + + case TokenKind.Minus: + methodName = "op_Subtraction"; + break; + + case TokenKind.Multiply: + methodName = "op_Multiply"; + break; + + case TokenKind.Plus: + methodName = "op_Addition"; + break; + + case TokenKind.Rem: + methodName = "op_Modulus"; + break; + + case TokenKind.Shl: + methodName = "op_LeftShift"; + break; + + case TokenKind.Shr: + methodName = "op_RightShift"; + break; + + default: + return lhsTypes; + } + + List rhsTypes = InferTypes(binaryExpressionAst.Right).ToList(); + HashSet addedReturnTypes = new HashSet(); + List result = new List(); + foreach (PSTypeName lType in lhsTypes) + { + if (lType.Type is null) + { + continue; + } + + foreach (MethodInfo method in lType.Type.GetMethods(BindingFlags.Public | BindingFlags.Static)) + { + if (!method.Name.Equals(methodName, StringComparison.Ordinal)) + { + continue; + } + + if (rhsTypes.Count == 0) + { + if (addedReturnTypes.Add(method.ReturnType.FullName)) + { + result.Add(new PSTypeName(method.ReturnType)); + } + + continue; + } + + ParameterInfo[] methodParams = method.GetParameters(); + if (methodParams.Length != 2) + { + continue; + } + + foreach (PSTypeName rType in rhsTypes) + { + if (rType.Type is not null && rType.Type.IsAssignableTo(methodParams[1].ParameterType)) + { + if (addedReturnTypes.Add(method.ReturnType.FullName)) + { + result.Add(new PSTypeName(method.ReturnType)); + } + + break; + } + } + } + } + + if (result.Count == 0) + { + result.AddRange(lhsTypes); + } + + return result; } object ICustomAstVisitor.VisitUnaryExpression(UnaryExpressionAst unaryExpressionAst) diff --git a/src/System.Management.Automation/engine/parser/ast.cs b/src/System.Management.Automation/engine/parser/ast.cs index c83f2189f0..c9c54bcd4b 100644 --- a/src/System.Management.Automation/engine/parser/ast.cs +++ b/src/System.Management.Automation/engine/parser/ast.cs @@ -7591,6 +7591,8 @@ namespace System.Management.Automation.Language } internal static readonly PSTypeName[] BoolTypeNameArray = new PSTypeName[] { new PSTypeName(typeof(bool)) }; + internal static readonly PSTypeName[] StringTypeNameArray = new PSTypeName[] { new PSTypeName(typeof(string)) }; + internal static readonly PSTypeName[] StringArrayTypeNameArray = new PSTypeName[] { new PSTypeName(typeof(string[])) }; #region Visitors diff --git a/test/powershell/engine/Api/TypeInference.Tests.ps1 b/test/powershell/engine/Api/TypeInference.Tests.ps1 index 8a1d9670bb..83bab06d19 100644 --- a/test/powershell/engine/Api/TypeInference.Tests.ps1 +++ b/test/powershell/engine/Api/TypeInference.Tests.ps1 @@ -268,6 +268,56 @@ Describe "Type inference Tests" -tags "CI" { $res.Name | Should -Be 'System.Management.ManagementObject#root\cimv2\Win32_Process' } + It "Infers type from binary expression with a bool operator as bool" { + $res = [AstTypeInference]::InferTypeOf( { + (1..10) -contains 5 + }.Ast) + $res.Count | Should -Be 1 + $res.Name | Should -Be 'System.Boolean' + } + + It "Infers type from binary expression with a string operator as string" { + $res = [AstTypeInference]::InferTypeOf( { + (1..10) -join ',' + }.Ast) + $res.Count | Should -Be 1 + $res.Name | Should -Be 'System.String' + } + + It "Infers type from binary expression with a split operator as string array" { + $res = [AstTypeInference]::InferTypeOf( { + "Test:Value" -split ':' + }.Ast) + $res.Count | Should -Be 1 + $res.Name | Should -Be 'System.String[]' + } + + It "Infers type from binary expression with a comparison operator as bool + the left hand side type" { + $res = [AstTypeInference]::InferTypeOf( { + ("Hello", "World") -eq "Hello" + }.Ast) + $res.Count | Should -Be 2 + $res.Name[0] | Should -Be 'System.Boolean' + $res.Name[1] | Should -Be 'System.String[]' + } + + It "Infers type from binary expression with a null coalescing operator as left and right hand side types" { + $res = [AstTypeInference]::InferTypeOf( { + "NotNull" ?? 10 + }.Ast) + $res.Count | Should -Be 2 + $res.Name[0] | Should -Be 'System.String' + $res.Name[1] | Should -Be 'System.Int32' + } + + It "Infers type from binary expression with an overridden operator" { + $res = [AstTypeInference]::InferTypeOf( { + (Get-Date) - (Get-Date) + }.Ast) + $res.Count | Should -Be 1 + $res.Name | Should -Be 'System.TimeSpan' + } + It "Infers type from DATA statement" { $res = [AstTypeInference]::InferTypeOf( { DATA {