Don't complete duplicate command names (#21113)

This commit is contained in:
MartinGC94
2025-03-04 12:57:39 +00:00
committed by GitHub
parent 0e6ffc52e9
commit 6661f6f100
2 changed files with 82 additions and 34 deletions
@@ -329,50 +329,72 @@ namespace System.Management.Automation
}
}
List<CompletionResult> endResults = null;
foreach (var keyValuePair in commandTable)
{
var commandList = keyValuePair.Value as List<object>;
if (commandList != null)
if (keyValuePair.Value is List<object> commandList)
{
endResults ??= new List<CompletionResult>();
// The first command might be an un-prefixed commandInfo that we get by importing a module with the -Prefix parameter,
// in that case, we should add the module name qualification because if the module is not in the module path, calling
// 'Get-Foo' directly doesn't work
string completionName = keyValuePair.Key;
if (!includeModulePrefix)
var modulesWithCommand = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var importedModules = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var commandInfoList = new List<CommandInfo>(commandList.Count);
for (int i = 0; i < commandList.Count; i++)
{
var commandInfo = commandList[0] as CommandInfo;
if (commandInfo != null && !string.IsNullOrEmpty(commandInfo.Prefix))
if (commandList[i] is not CommandInfo commandInfo)
{
Diagnostics.Assert(!string.IsNullOrEmpty(commandInfo.ModuleName), "the module name should exist if commandInfo.Prefix is not an empty string");
if (!ModuleCmdletBase.IsPrefixedCommand(commandInfo))
{
completionName = commandInfo.ModuleName + "\\" + completionName;
}
continue;
}
commandInfoList.Add(commandInfo);
if (commandInfo.CommandType == CommandTypes.Application)
{
continue;
}
modulesWithCommand.Add(commandInfo.ModuleName);
if ((commandInfo.CommandType == CommandTypes.Cmdlet && commandInfo.CommandMetadata.CommandType is not null)
|| (commandInfo.CommandType is CommandTypes.Function or CommandTypes.Filter && commandInfo.Definition != string.Empty)
|| (commandInfo.CommandType == CommandTypes.Alias && commandInfo.Definition is not null))
{
// Checks if the command or source module has been imported.
_ = importedModules.Add(commandInfo.ModuleName);
}
}
results.Add(GetCommandNameCompletionResult(completionName, commandList[0], addAmpersandIfNecessary, quote));
// For the other commands that are hidden, we need to disambiguate,
// but put these at the end as it's less likely any of the hidden
// commands are desired. If we can't add anything to disambiguate,
// then we'll skip adding a completion result.
for (int index = 1; index < commandList.Count; index++)
if (commandInfoList.Count == 0)
{
var commandInfo = commandList[index] as CommandInfo;
Diagnostics.Assert(commandInfo != null, "Elements should always be CommandInfo");
continue;
}
int moduleCount = modulesWithCommand.Count;
modulesWithCommand.Clear();
int index;
if (commandInfoList[0].CommandType == CommandTypes.Application
|| importedModules.Count == 1
|| moduleCount < 2)
{
// We can use the short name for this command because there's no ambiguity about which command it resolves to.
// If the first element is an application then we know there's no conflicting commands/aliases (because of the command precedence).
// If there's just 1 module imported then the short name refers to that module (and it will be the first element in the list)
// If there's less than 2 unique modules exporting that command then we can use the short name because it can only refer to that module.
index = 1;
results.Add(GetCommandNameCompletionResult(keyValuePair.Key, commandInfoList[0], addAmpersandIfNecessary, quote));
modulesWithCommand.Add(commandInfoList[0].ModuleName);
}
else
{
index = 0;
}
for (; index < commandInfoList.Count; index++)
{
CommandInfo commandInfo = commandInfoList[index];
if (commandInfo.CommandType == CommandTypes.Application)
{
endResults.Add(GetCommandNameCompletionResult(commandInfo.Definition, commandInfo, addAmpersandIfNecessary, quote));
results.Add(GetCommandNameCompletionResult(commandInfo.Definition, commandInfo, addAmpersandIfNecessary, quote));
}
else if (!string.IsNullOrEmpty(commandInfo.ModuleName))
else if (!string.IsNullOrEmpty(commandInfo.ModuleName) && modulesWithCommand.Add(commandInfo.ModuleName))
{
var name = commandInfo.ModuleName + "\\" + commandInfo.Name;
endResults.Add(GetCommandNameCompletionResult(name, commandInfo, addAmpersandIfNecessary, quote));
results.Add(GetCommandNameCompletionResult(name, commandInfo, addAmpersandIfNecessary, quote));
}
}
}
@@ -399,11 +421,6 @@ namespace System.Management.Automation
}
}
if (endResults != null && endResults.Count > 0)
{
results.AddRange(endResults);
}
return results;
}
@@ -33,6 +33,37 @@ Describe "TabCompletion" -Tags CI {
$res.CompletionMatches[0].CompletionText | Should -BeExactly 'notepad.exe'
}
It 'Should not include duplicate command results' {
$OldModulePath = $env:PSModulePath
$tempDir = Join-Path -Path $TestDrive -ChildPath "TempPsModuleDir"
try
{
$ModuleDirs = @(
Join-Path $tempDir "TestModule1\1.0"
Join-Path $tempDir "TestModule1\1.1"
Join-Path $tempDir "TestModule2\1.0"
)
foreach ($Dir in $ModuleDirs)
{
$NewDir = New-Item -Path $Dir -ItemType Directory -Force
$ModuleName = $NewDir.Parent.Name
Set-Content -Value 'MyTestFunction{}' -LiteralPath "$($NewDir.FullName)\$ModuleName.psm1"
New-ModuleManifest -Path "$($NewDir.FullName)\$ModuleName.psd1" -RootModule "$ModuleName.psm1" -FunctionsToExport "MyTestFunction" -ModuleVersion $NewDir.Name
}
$env:PSModulePath += [System.IO.Path]::PathSeparator + $tempDir
$Res = TabExpansion2 -inputScript MyTestFunction
$Res.CompletionMatches.Count | Should -Be 2
$SortedMatches = $Res.CompletionMatches.CompletionText | Sort-Object
$SortedMatches[0] | Should -Be "TestModule1\MyTestFunction"
$SortedMatches[1] | Should -Be "TestModule2\MyTestFunction"
}
finally
{
$env:PSModulePath = $OldModulePath
}
}
It 'Should complete dotnet method' {
$res = TabExpansion2 -inputScript '(1).ToSt' -cursorColumn '(1).ToSt'.Length
$res.CompletionMatches[0].CompletionText | Should -BeExactly 'ToString('