mirror of
https://github.com/PowerShell/PowerShell
synced 2026-06-08 12:12:50 +00:00
Handle empty-string and null-value results returned from custom argument completer more properly (#27398)
This commit is contained in:
@@ -42,7 +42,7 @@
|
||||
<PackageReference Include="Microsoft.Management.Infrastructure" Version="3.0.0" />
|
||||
<PackageReference Include="Microsoft.PowerShell.Native" Version="700.0.0" />
|
||||
<!-- Signing APIs -->
|
||||
<PackageReference Include="Microsoft.Security.Extensions" Version="1.4.0" />
|
||||
<PackageReference Include="Microsoft.Security.Extensions" Version="1.4.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
@@ -601,7 +601,10 @@ namespace System.Management.Automation
|
||||
completionContext.ExecutionContext.LanguageMode = PSLanguageMode.ConstrainedLanguage;
|
||||
}
|
||||
|
||||
return GetResultHelper(completionContext, out replacementIndex, out replacementLength);
|
||||
List<CompletionResult> results = GetResultHelper(completionContext, out replacementIndex, out replacementLength);
|
||||
CompletionCompleters.RemoveLastNullCompletionResult(results);
|
||||
|
||||
return results;
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -2675,14 +2675,17 @@ namespace System.Management.Automation
|
||||
scriptBlock,
|
||||
new object[] { commandName, parameterName, wordToComplete, commandAst, GetBoundArgumentsAsHashtable(context) },
|
||||
resultList);
|
||||
if (result)
|
||||
{
|
||||
resultList.Add(CompletionResult.Null);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke the custom argument completer and process its return values.
|
||||
/// If we consider the completion successful, we add a null instance of the type 'CompletionResult'
|
||||
/// to the end of the 'result' list to indicate that the argument completion has been processed, so we
|
||||
/// will not go through the default argument completion even if the 'result' list is still empty.
|
||||
/// </summary>
|
||||
/// <returns>'true' if the argument completion was successful. 'false' otherwise.</returns>
|
||||
private static bool InvokeScriptArgumentCompleter(
|
||||
ScriptBlock scriptBlock,
|
||||
object[] argumentsToCompleter,
|
||||
@@ -2702,20 +2705,44 @@ namespace System.Management.Automation
|
||||
return false;
|
||||
}
|
||||
|
||||
if (customResults.Count is 1 && customResults[0] is { BaseObject: "" } or null)
|
||||
{
|
||||
// If the script block returns a single empty string or a null value, we will treat it as if it has
|
||||
// completed successfully but has no results to return.
|
||||
// This allows a custom completer to suppress the default completions that we may fall back otherwise.
|
||||
result.Add(CompletionResult.Null);
|
||||
return true;
|
||||
}
|
||||
|
||||
int initialCount = result.Count;
|
||||
|
||||
foreach (var customResult in customResults)
|
||||
{
|
||||
var resultAsCompletion = customResult.BaseObject as CompletionResult;
|
||||
if (resultAsCompletion != null)
|
||||
if (customResult is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (customResult.BaseObject is CompletionResult resultAsCompletion)
|
||||
{
|
||||
result.Add(resultAsCompletion);
|
||||
continue;
|
||||
}
|
||||
|
||||
var resultAsString = customResult.ToString();
|
||||
result.Add(new CompletionResult(resultAsString));
|
||||
if (!string.IsNullOrEmpty(resultAsString))
|
||||
{
|
||||
result.Add(new CompletionResult(resultAsString));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
bool success = result.Count > initialCount;
|
||||
if (success)
|
||||
{
|
||||
result.Add(CompletionResult.Null);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
// All the methods for native command argument completion will add a null instance of the type CompletionResult to the end of the
|
||||
@@ -2723,9 +2750,9 @@ namespace System.Management.Automation
|
||||
// and has been processed already. So if the "result" list is still empty afterward, we will not go through the default argument completion anymore.
|
||||
#region Native Command Argument Completion
|
||||
|
||||
private static void RemoveLastNullCompletionResult(List<CompletionResult> result)
|
||||
internal static void RemoveLastNullCompletionResult(List<CompletionResult> result)
|
||||
{
|
||||
if (result.Count > 0 && result[result.Count - 1].Equals(CompletionResult.Null))
|
||||
if (result?.Count > 0 && result[^1].Equals(CompletionResult.Null))
|
||||
{
|
||||
result.RemoveAt(result.Count - 1);
|
||||
}
|
||||
|
||||
@@ -149,4 +149,150 @@ Describe "Tab completion bug fix" -Tags "CI" {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
Context 'Native CLI argument completion' {
|
||||
BeforeAll {
|
||||
$testDir = Join-Path $TestDrive "TempTestDir"
|
||||
$file1 = Join-Path $testDir "abc.ps1"
|
||||
$file2 = Join-Path $testDir "def.py"
|
||||
|
||||
New-Item -ItemType Directory -Path $testDir > $null
|
||||
New-Item -ItemType File -Path $file1 > $null
|
||||
New-Item -ItemType File -Path $file2 > $null
|
||||
|
||||
$dirSep = [System.IO.Path]::DirectorySeparatorChar
|
||||
$relative_name_abc = ".${dirSep}abc.ps1"
|
||||
$relative_name_def = ".${dirSep}def.py"
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
## Unregister the completer for 'ping' to avoid affecting other tests.
|
||||
register-ArgumentCompleter -Native -CommandName ping -ScriptBlock $null
|
||||
}
|
||||
|
||||
It 'Completer script block returning nothing should fall back to file name completion' {
|
||||
register-ArgumentCompleter -Native -CommandName ping -ScriptBlock {
|
||||
param($WordToComplete, $CommandAst, $CursorPosition)
|
||||
}
|
||||
|
||||
try {
|
||||
Push-Location -Path $testDir
|
||||
$cmd = "ping "
|
||||
$result = TabExpansion2 -inputScript $cmd -cursorColumn $cmd.Length
|
||||
$result.CompletionMatches | Should -Not -BeNullOrEmpty
|
||||
$result.CompletionMatches.Count | Should -Be 2
|
||||
$result.CompletionMatches[0].CompletionText | Should -BeExactly $relative_name_abc
|
||||
$result.CompletionMatches[1].CompletionText | Should -BeExactly $relative_name_def
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
It 'Completer script block returning $null should suppress default completion fallback' {
|
||||
register-ArgumentCompleter -Native -CommandName ping -ScriptBlock {
|
||||
param($WordToComplete, $CommandAst, $CursorPosition)
|
||||
return $null
|
||||
}
|
||||
|
||||
try {
|
||||
Push-Location -Path $testDir
|
||||
$cmd = "ping "
|
||||
## This call should not throw, and should suppress the default file name completion fallback, returning no results.
|
||||
$result = TabExpansion2 -inputScript $cmd -cursorColumn $cmd.Length
|
||||
$result.CompletionMatches.Count | Should -Be 0
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
It 'Completer script block returning empty string should suppress default completion fallback' {
|
||||
register-ArgumentCompleter -Native -CommandName ping -ScriptBlock {
|
||||
param($WordToComplete, $CommandAst, $CursorPosition)
|
||||
return ''
|
||||
}
|
||||
|
||||
try {
|
||||
Push-Location -Path $testDir
|
||||
$cmd = "ping "
|
||||
## This call should not throw, and should suppress the default file name completion fallback, returning no results.
|
||||
$result = TabExpansion2 -inputScript $cmd -cursorColumn $cmd.Length
|
||||
$result.CompletionMatches.Count | Should -Be 0
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
It 'Completer script block returning empty-string-only array should fall back to default completion' {
|
||||
register-ArgumentCompleter -Native -CommandName ping -ScriptBlock {
|
||||
param($WordToComplete, $CommandAst, $CursorPosition)
|
||||
return '', ''
|
||||
}
|
||||
|
||||
try {
|
||||
Push-Location -Path $testDir
|
||||
$cmd = "ping "
|
||||
## This call should not throw, and should fall back to the default completion.
|
||||
$result = TabExpansion2 -inputScript $cmd -cursorColumn $cmd.Length
|
||||
$result.CompletionMatches.Count | Should -Be 2
|
||||
$result.CompletionMatches[0].CompletionText | Should -BeExactly $relative_name_abc
|
||||
$result.CompletionMatches[1].CompletionText | Should -BeExactly $relative_name_def
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
It 'Completer script block returning null-value-only array should fall back to default completion' {
|
||||
register-ArgumentCompleter -Native -CommandName ping -ScriptBlock {
|
||||
param($WordToComplete, $CommandAst, $CursorPosition)
|
||||
return $null, $null
|
||||
}
|
||||
|
||||
try {
|
||||
Push-Location -Path $testDir
|
||||
$cmd = "ping "
|
||||
## This call should not throw, and should fall back to the default completion.
|
||||
$result = TabExpansion2 -inputScript $cmd -cursorColumn $cmd.Length
|
||||
$result.CompletionMatches.Count | Should -Be 2
|
||||
$result.CompletionMatches[0].CompletionText | Should -BeExactly $relative_name_abc
|
||||
$result.CompletionMatches[1].CompletionText | Should -BeExactly $relative_name_def
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
It 'Completer script block returning a single string works as expected' {
|
||||
register-ArgumentCompleter -Native -CommandName ping -ScriptBlock {
|
||||
param($WordToComplete, $CommandAst, $CursorPosition)
|
||||
return 'hello'
|
||||
}
|
||||
|
||||
try {
|
||||
Push-Location -Path $testDir
|
||||
$cmd = "ping "
|
||||
$result = TabExpansion2 -inputScript $cmd -cursorColumn $cmd.Length
|
||||
$result.CompletionMatches.Count | Should -Be 1
|
||||
$result.CompletionMatches[0].CompletionText | Should -BeExactly "hello"
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
It 'Completer script block returning an array that contains non-empty-or-null strings works as expected' {
|
||||
register-ArgumentCompleter -Native -CommandName ping -ScriptBlock {
|
||||
param($WordToComplete, $CommandAst, $CursorPosition)
|
||||
return '', 'hello', $null, 'world'
|
||||
}
|
||||
|
||||
try {
|
||||
Push-Location -Path $testDir
|
||||
$cmd = "ping "
|
||||
$result = TabExpansion2 -inputScript $cmd -cursorColumn $cmd.Length
|
||||
$result.CompletionMatches.Count | Should -Be 2
|
||||
$result.CompletionMatches[0].CompletionText | Should -BeExactly "hello"
|
||||
$result.CompletionMatches[1].CompletionText | Should -BeExactly "world"
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user