Fix parameter completion when script requirements fail (#17687)

This commit is contained in:
MartinGC94
2025-03-07 04:26:03 +00:00
committed by GitHub
parent d820692939
commit f7ae4229dc
4 changed files with 60 additions and 17 deletions
@@ -1198,7 +1198,7 @@ namespace System.Management.Automation.Language
string commandName = null;
try
{
processor = PrepareFromAst(context, out commandName) ?? context.CreateCommand(commandName, dotSource);
processor = PrepareFromAst(context, out commandName) ?? context.CreateCommand(commandName, dotSource, forCompletion:true);
}
catch (RuntimeException)
{
@@ -262,6 +262,9 @@ namespace System.Management.Automation
/// False if not. Null if command discovery should default to something reasonable
/// for the command discovered.
/// </param>
/// <param name="forCompletion">
/// True if this for parameter completion and script requirements should be ignored.
/// </param>
/// <returns>
/// </returns>
/// <exception cref="CommandNotFoundException">
@@ -271,14 +274,15 @@ namespace System.Management.Automation
/// If the security manager is preventing the command from running.
/// </exception>
internal CommandProcessorBase LookupCommandProcessor(string commandName,
CommandOrigin commandOrigin, bool? useLocalScope)
CommandOrigin commandOrigin, bool? useLocalScope, bool forCompletion = false)
{
CommandProcessorBase processor = null;
CommandInfo commandInfo = LookupCommandInfo(commandName, commandOrigin);
if (commandInfo != null)
{
processor = LookupCommandProcessor(commandInfo, commandOrigin, useLocalScope, null);
processor = LookupCommandProcessor(commandInfo, commandOrigin, useLocalScope, null, forCompletion);
// commandInfo.Name might be different than commandName - restore the original invocation name
processor.Command.MyInvocation.InvocationName = commandName;
}
@@ -286,7 +290,7 @@ namespace System.Management.Automation
return processor;
}
internal static void VerifyRequiredModules(ExternalScriptInfo scriptInfo, ExecutionContext context)
internal static void VerifyRequiredModules(ExternalScriptInfo scriptInfo, ExecutionContext context, bool forCompletion = false)
{
// Check Required Modules
if (scriptInfo.RequiresModules != null)
@@ -301,7 +305,7 @@ namespace System.Management.Automation
moduleManifestPath: null,
manifestProcessingFlags: ModuleCmdletBase.ManifestProcessingFlags.LoadElements | ModuleCmdletBase.ManifestProcessingFlags.WriteErrors,
error: out error);
if (error != null)
if (!forCompletion && error is not null)
{
ScriptRequiresException scriptRequiresException =
new ScriptRequiresException(
@@ -316,9 +320,9 @@ namespace System.Management.Automation
}
}
private CommandProcessorBase CreateScriptProcessorForSingleShell(ExternalScriptInfo scriptInfo, ExecutionContext context, bool useLocalScope, SessionStateInternal sessionState)
private CommandProcessorBase CreateScriptProcessorForSingleShell(ExternalScriptInfo scriptInfo, ExecutionContext context, bool useLocalScope, SessionStateInternal sessionState, bool forCompletion = false)
{
VerifyScriptRequirements(scriptInfo, Context);
VerifyScriptRequirements(scriptInfo, Context, forCompletion);
if (!string.IsNullOrEmpty(scriptInfo.RequiresApplicationID))
{
@@ -340,12 +344,18 @@ namespace System.Management.Automation
// #Requires -PSVersion
// #Requires -PSEdition
// #Requires -Module
internal static void VerifyScriptRequirements(ExternalScriptInfo scriptInfo, ExecutionContext context)
internal static void VerifyScriptRequirements(ExternalScriptInfo scriptInfo, ExecutionContext context, bool forCompletion = false)
{
VerifyElevatedPrivileges(scriptInfo);
VerifyPSVersion(scriptInfo);
VerifyPSEdition(scriptInfo);
VerifyRequiredModules(scriptInfo, context);
// When completing script parameters we don't care if these requirements are met.
// VerifyRequiredModules will attempt to load the required modules which is useful for completion (so the correct types are loaded).
if (!forCompletion)
{
VerifyElevatedPrivileges(scriptInfo);
VerifyPSVersion(scriptInfo);
VerifyPSEdition(scriptInfo);
}
VerifyRequiredModules(scriptInfo, context, forCompletion);
}
internal static void VerifyPSVersion(ExternalScriptInfo scriptInfo)
@@ -426,6 +436,9 @@ namespace System.Management.Automation
/// False if not. Null if command discovery should default to something reasonable
/// for the command discovered.
/// </param>
/// <param name="forCompletion">
/// True if this for parameter completion and script requirements should be ignored.
/// </param>
/// <param name="sessionState">The session state the commandInfo should be run in.</param>
/// <returns>
/// </returns>
@@ -436,7 +449,7 @@ namespace System.Management.Automation
/// If the security manager is preventing the command from running.
/// </exception>
internal CommandProcessorBase LookupCommandProcessor(CommandInfo commandInfo,
CommandOrigin commandOrigin, bool? useLocalScope, SessionStateInternal sessionState)
CommandOrigin commandOrigin, bool? useLocalScope, SessionStateInternal sessionState, bool forCompletion = false)
{
CommandProcessorBase processor = null;
@@ -482,7 +495,7 @@ namespace System.Management.Automation
scriptInfo.SignatureChecked = true;
try
{
processor = CreateScriptProcessorForSingleShell(scriptInfo, Context, useLocalScope ?? true, sessionState);
processor = CreateScriptProcessorForSingleShell(scriptInfo, Context, useLocalScope ?? true, sessionState, forCompletion);
}
catch (ScriptRequiresSyntaxException reqSyntaxException)
{
@@ -477,7 +477,6 @@ namespace System.Management.Automation
/// The assemblies that have been loaded for this runspace.
/// </summary>
internal Dictionary<string, Assembly> AssemblyCache { get; private set; }
#endregion Properties
#region Engine State
@@ -634,12 +633,13 @@ namespace System.Management.Automation
/// </summary>
/// <param name="command">The name of the command to lookup.</param>
/// <param name="dotSource"></param>
/// <param name="forCompletion"></param>
/// <returns>The command processor object.</returns>
internal CommandProcessorBase CreateCommand(string command, bool dotSource)
internal CommandProcessorBase CreateCommand(string command, bool dotSource, bool forCompletion = false)
{
CommandOrigin commandOrigin = this.EngineSessionState.CurrentScope.ScopeOrigin;
CommandProcessorBase commandProcessor =
CommandDiscovery.LookupCommandProcessor(command, commandOrigin, !dotSource);
CommandDiscovery.LookupCommandProcessor(command, commandOrigin, !dotSource, forCompletion);
// Reset the command origin for script commands... // BUGBUG - dotting can get around command origin checks???
if (commandProcessor != null && commandProcessor is ScriptCommandProcessorBase)
{
@@ -1783,6 +1783,36 @@ class InheritedClassTest : System.Attribute
}
}
Context "Script parameter completion" {
BeforeAll {
Setup -File -Path 'ModuleReqTest.ps1' -Content @'
#requires -Modules ThisModuleDoesNotExist
param ($Param1)
'@
Setup -File -Path 'AdminReqTest.ps1' -Content @'
#requires -RunAsAdministrator
param ($Param1)
'@
Push-Location ${TestDrive}\
}
AfterAll {
Pop-Location
}
It "Input should successfully complete script parameter for script with failed script requirements" {
$res = TabExpansion2 -inputScript '.\ModuleReqTest.ps1 -'
$res.CompletionMatches.Count | Should -BeGreaterThan 0
$res.CompletionMatches[0].CompletionText | Should -BeExactly '-Param1'
}
It "Input should successfully complete script parameter for admin script while not elevated" {
$res = TabExpansion2 -inputScript '.\AdminReqTest.ps1 -'
$res.CompletionMatches.Count | Should -BeGreaterThan 0
$res.CompletionMatches[0].CompletionText | Should -BeExactly '-Param1'
}
}
Context "File name completion" {
BeforeAll {
$tempDir = Join-Path -Path $TestDrive -ChildPath "baseDir"