From 4051e097965019fdd681a90eafd63e63aa5219f1 Mon Sep 17 00:00:00 2001 From: MartinGC94 <42123497+MartinGC94@users.noreply.github.com> Date: Tue, 23 Aug 2022 00:18:49 +0200 Subject: [PATCH] Add completion for values in comparisons when comparing Enums (#17654) --- .../CommandCompletion/CompletionAnalysis.cs | 52 ++++++++++++- .../CommandCompletion/CompletionCompleters.cs | 77 +++++++++++++++++++ .../TabCompletion/TabCompletion.Tests.ps1 | 26 +++++++ 3 files changed, 154 insertions(+), 1 deletion(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index d623a10af..24e687396 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -500,6 +500,15 @@ namespace System.Management.Automation return completions; } } + else if (lastAst.Parent is BinaryExpressionAst binaryExpression) + { + completionContext.WordToComplete = (tokenAtCursor as StringToken).Value; + result = CompletionCompleters.CompleteComparisonOperatorValues(completionContext, binaryExpression.Left); + if (result.Count > 0) + { + return result; + } + } else if (lastAst.Parent is IndexExpressionAst indexExpressionAst) { // Handles quoted string inside index expression like: $PSVersionTable[""] @@ -911,8 +920,27 @@ namespace System.Management.Automation { result = GetResultForAttributeArgument(completionContext, ref replacementIndex, ref replacementLength); } - break; + + case TokenKind.Ieq: + case TokenKind.Ceq: + case TokenKind.Ine: + case TokenKind.Cne: + case TokenKind.Ilike: + case TokenKind.Clike: + case TokenKind.Inotlike: + case TokenKind.Cnotlike: + case TokenKind.Imatch: + case TokenKind.Cmatch: + case TokenKind.Inotmatch: + case TokenKind.Cnotmatch: + if (lastAst is BinaryExpressionAst binaryExpression) + { + completionContext.WordToComplete = string.Empty; + result = CompletionCompleters.CompleteComparisonOperatorValues(completionContext, binaryExpression.Left); + } + break; + case TokenKind.LBracket: if (lastAst.Parent is IndexExpressionAst indexExpression) { @@ -921,6 +949,7 @@ namespace System.Management.Automation result = CompletionCompleters.CompleteIndexExpression(completionContext, indexExpression.Target); } break; + default: break; } @@ -991,6 +1020,26 @@ namespace System.Management.Automation return result; } break; + + case TokenKind.Ieq: + case TokenKind.Ceq: + case TokenKind.Ine: + case TokenKind.Cne: + case TokenKind.Ilike: + case TokenKind.Clike: + case TokenKind.Inotlike: + case TokenKind.Cnotlike: + case TokenKind.Imatch: + case TokenKind.Cmatch: + case TokenKind.Inotmatch: + case TokenKind.Cnotmatch: + if (lastAst is BinaryExpressionAst binaryExpression) + { + completionContext.WordToComplete = string.Empty; + result = CompletionCompleters.CompleteComparisonOperatorValues(completionContext, binaryExpression.Left); + } + break; + case TokenKind.LBracket: if (lastAst.Parent is IndexExpressionAst indexExpression) { @@ -999,6 +1048,7 @@ namespace System.Management.Automation result = CompletionCompleters.CompleteIndexExpression(completionContext, indexExpression.Target); } break; + default: break; } diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 59e261e61..3fbbf928c 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -2317,6 +2317,11 @@ namespace System.Management.Automation { NativeCompletionMemberName(context, result, commandAst, boundArguments?[parameterName]); } + else if (parameterName.Equals("Value", StringComparison.OrdinalIgnoreCase) + && boundArguments?["Property"] is AstPair pair && pair.Argument is StringConstantExpressionAst stringAst) + { + NativeCompletionMemberValue(context, result, commandAst, stringAst.Value); + } break; } @@ -3994,6 +3999,38 @@ namespace System.Management.Automation result.Add(CompletionResult.Null); } + private static void NativeCompletionMemberValue(CompletionContext context, List result, CommandAst commandAst, string propertyName) + { + string wordToComplete = context.WordToComplete.Trim('"', '\''); + IEnumerable prevTypes = GetInferenceTypes(context, commandAst); + if (prevTypes is not null) + { + foreach (var type in prevTypes) + { + if (type.Type is null) + { + continue; + } + + PropertyInfo property = type.Type.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); + if (property is not null && property.PropertyType.IsEnum) + { + foreach (var value in property.PropertyType.GetEnumNames()) + { + if (value.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase)) + { + result.Add(new CompletionResult(value, value, CompletionResultType.ParameterValue, value)); + } + } + + break; + } + } + } + + result.Add(CompletionResult.Null); + } + /// /// Returns all string values bound to a parameter except the one the cursor is currently at. /// @@ -5913,6 +5950,46 @@ namespace System.Management.Automation return results; } + internal static List CompleteComparisonOperatorValues(CompletionContext context, ExpressionAst operatorLeftValue) + { + var result = new List(); + var resolvedTypes = new List(); + + if (SafeExprEvaluator.TrySafeEval(operatorLeftValue, context.ExecutionContext, out object value) && value is not null) + { + resolvedTypes.Add(value.GetType()); + } + else + { + var inferredTypes = AstTypeInference.InferTypeOf(operatorLeftValue, context.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval); + foreach (var type in inferredTypes) + { + if (type.Type is not null) + { + resolvedTypes.Add(type.Type); + } + } + } + + foreach (var type in resolvedTypes) + { + if (type.IsEnum) + { + foreach (var name in type.GetEnumNames()) + { + if (name.StartsWith(context.WordToComplete, StringComparison.OrdinalIgnoreCase)) + { + result.Add(new CompletionResult($"'{name}'", name, CompletionResultType.ParameterValue, name)); + } + } + + break; + } + } + + return result; + } + /// /// Complete members against extension methods 'Where' and 'ForEach' /// diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index 9ab7d735e..725b47138 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -558,6 +558,32 @@ ConstructorTestClass(int i, bool b) $res.CompletionMatches[0].CompletionText | Should -BeExactly Cat } + it 'Should complete "Value" parameter value in "Where-Object" for Enum property with no input' { + $res = TabExpansion2 -inputScript 'Get-Command | where-Object CommandType -eq ' + $res.CompletionMatches[0].CompletionText | Should -BeExactly Alias + } + + it 'Should complete "Value" parameter value in "Where-Object" for Enum property with partial input' { + $res = TabExpansion2 -inputScript 'Get-Command | where-Object CommandType -ne Ali' + $res.CompletionMatches[0].CompletionText | Should -BeExactly Alias + } + + it 'Should complete the right hand side of a comparison operator when left is an Enum with no input' { + $res = TabExpansion2 -inputScript 'Get-Command | Where-Object -FilterScript {$_.CommandType -like ' + $res.CompletionMatches[0].CompletionText | Should -BeExactly "'Alias'" + } + + it 'Should complete the right hand side of a comparison operator when left is an Enum with partial input' { + $TempVar = Get-Command + $res = TabExpansion2 -inputScript '$tempVar[0].CommandType -notlike "Ali"' + $res.CompletionMatches[0].CompletionText | Should -BeExactly "'Alias'" + } + + it 'Should complete the right hand side of a comparison operator when left is an Enum when cursor is on a newline' { + $res = TabExpansion2 -inputScript "Get-Command | Where-Object -FilterScript {`$_.CommandType -like`n" + $res.CompletionMatches[0].CompletionText | Should -BeExactly "'Alias'" + } + it 'Should complete provider dynamic parameters with quoted path' { $Script = if ($IsWindows) {