Skip to content

Commit f7ae422

Browse files
authored
Fix parameter completion when script requirements fail (PowerShell#17687)
1 parent d820692 commit f7ae422

File tree

4 files changed

+60
-17
lines changed

4 files changed

+60
-17
lines changed

src/System.Management.Automation/engine/CommandCompletion/PseudoParameterBinder.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1198,7 +1198,7 @@ private bool PrepareCommandElements(ExecutionContext context, CommandParameterAs
11981198
string commandName = null;
11991199
try
12001200
{
1201-
processor = PrepareFromAst(context, out commandName) ?? context.CreateCommand(commandName, dotSource);
1201+
processor = PrepareFromAst(context, out commandName) ?? context.CreateCommand(commandName, dotSource, forCompletion:true);
12021202
}
12031203
catch (RuntimeException)
12041204
{

src/System.Management.Automation/engine/CommandDiscovery.cs

+26-13
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,9 @@ internal void AddSessionStateCmdletEntryToCache(SessionStateCmdletEntry entry, b
262262
/// False if not. Null if command discovery should default to something reasonable
263263
/// for the command discovered.
264264
/// </param>
265+
/// <param name="forCompletion">
266+
/// True if this for parameter completion and script requirements should be ignored.
267+
/// </param>
265268
/// <returns>
266269
/// </returns>
267270
/// <exception cref="CommandNotFoundException">
@@ -271,22 +274,23 @@ internal void AddSessionStateCmdletEntryToCache(SessionStateCmdletEntry entry, b
271274
/// If the security manager is preventing the command from running.
272275
/// </exception>
273276
internal CommandProcessorBase LookupCommandProcessor(string commandName,
274-
CommandOrigin commandOrigin, bool? useLocalScope)
277+
CommandOrigin commandOrigin, bool? useLocalScope, bool forCompletion = false)
275278
{
276279
CommandProcessorBase processor = null;
277280
CommandInfo commandInfo = LookupCommandInfo(commandName, commandOrigin);
278281

279282
if (commandInfo != null)
280283
{
281-
processor = LookupCommandProcessor(commandInfo, commandOrigin, useLocalScope, null);
284+
processor = LookupCommandProcessor(commandInfo, commandOrigin, useLocalScope, null, forCompletion);
285+
282286
// commandInfo.Name might be different than commandName - restore the original invocation name
283287
processor.Command.MyInvocation.InvocationName = commandName;
284288
}
285289

286290
return processor;
287291
}
288292

289-
internal static void VerifyRequiredModules(ExternalScriptInfo scriptInfo, ExecutionContext context)
293+
internal static void VerifyRequiredModules(ExternalScriptInfo scriptInfo, ExecutionContext context, bool forCompletion = false)
290294
{
291295
// Check Required Modules
292296
if (scriptInfo.RequiresModules != null)
@@ -301,7 +305,7 @@ internal static void VerifyRequiredModules(ExternalScriptInfo scriptInfo, Execut
301305
moduleManifestPath: null,
302306
manifestProcessingFlags: ModuleCmdletBase.ManifestProcessingFlags.LoadElements | ModuleCmdletBase.ManifestProcessingFlags.WriteErrors,
303307
error: out error);
304-
if (error != null)
308+
if (!forCompletion && error is not null)
305309
{
306310
ScriptRequiresException scriptRequiresException =
307311
new ScriptRequiresException(
@@ -316,9 +320,9 @@ internal static void VerifyRequiredModules(ExternalScriptInfo scriptInfo, Execut
316320
}
317321
}
318322

319-
private CommandProcessorBase CreateScriptProcessorForSingleShell(ExternalScriptInfo scriptInfo, ExecutionContext context, bool useLocalScope, SessionStateInternal sessionState)
323+
private CommandProcessorBase CreateScriptProcessorForSingleShell(ExternalScriptInfo scriptInfo, ExecutionContext context, bool useLocalScope, SessionStateInternal sessionState, bool forCompletion = false)
320324
{
321-
VerifyScriptRequirements(scriptInfo, Context);
325+
VerifyScriptRequirements(scriptInfo, Context, forCompletion);
322326

323327
if (!string.IsNullOrEmpty(scriptInfo.RequiresApplicationID))
324328
{
@@ -340,12 +344,18 @@ private CommandProcessorBase CreateScriptProcessorForSingleShell(ExternalScriptI
340344
// #Requires -PSVersion
341345
// #Requires -PSEdition
342346
// #Requires -Module
343-
internal static void VerifyScriptRequirements(ExternalScriptInfo scriptInfo, ExecutionContext context)
347+
internal static void VerifyScriptRequirements(ExternalScriptInfo scriptInfo, ExecutionContext context, bool forCompletion = false)
344348
{
345-
VerifyElevatedPrivileges(scriptInfo);
346-
VerifyPSVersion(scriptInfo);
347-
VerifyPSEdition(scriptInfo);
348-
VerifyRequiredModules(scriptInfo, context);
349+
// When completing script parameters we don't care if these requirements are met.
350+
// VerifyRequiredModules will attempt to load the required modules which is useful for completion (so the correct types are loaded).
351+
if (!forCompletion)
352+
{
353+
VerifyElevatedPrivileges(scriptInfo);
354+
VerifyPSVersion(scriptInfo);
355+
VerifyPSEdition(scriptInfo);
356+
}
357+
358+
VerifyRequiredModules(scriptInfo, context, forCompletion);
349359
}
350360

351361
internal static void VerifyPSVersion(ExternalScriptInfo scriptInfo)
@@ -426,6 +436,9 @@ internal static void VerifyElevatedPrivileges(ExternalScriptInfo scriptInfo)
426436
/// False if not. Null if command discovery should default to something reasonable
427437
/// for the command discovered.
428438
/// </param>
439+
/// <param name="forCompletion">
440+
/// True if this for parameter completion and script requirements should be ignored.
441+
/// </param>
429442
/// <param name="sessionState">The session state the commandInfo should be run in.</param>
430443
/// <returns>
431444
/// </returns>
@@ -436,7 +449,7 @@ internal static void VerifyElevatedPrivileges(ExternalScriptInfo scriptInfo)
436449
/// If the security manager is preventing the command from running.
437450
/// </exception>
438451
internal CommandProcessorBase LookupCommandProcessor(CommandInfo commandInfo,
439-
CommandOrigin commandOrigin, bool? useLocalScope, SessionStateInternal sessionState)
452+
CommandOrigin commandOrigin, bool? useLocalScope, SessionStateInternal sessionState, bool forCompletion = false)
440453
{
441454
CommandProcessorBase processor = null;
442455

@@ -482,7 +495,7 @@ internal CommandProcessorBase LookupCommandProcessor(CommandInfo commandInfo,
482495
scriptInfo.SignatureChecked = true;
483496
try
484497
{
485-
processor = CreateScriptProcessorForSingleShell(scriptInfo, Context, useLocalScope ?? true, sessionState);
498+
processor = CreateScriptProcessorForSingleShell(scriptInfo, Context, useLocalScope ?? true, sessionState, forCompletion);
486499
}
487500
catch (ScriptRequiresSyntaxException reqSyntaxException)
488501
{

src/System.Management.Automation/engine/ExecutionContext.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,6 @@ internal LocationGlobber LocationGlobber
477477
/// The assemblies that have been loaded for this runspace.
478478
/// </summary>
479479
internal Dictionary<string, Assembly> AssemblyCache { get; private set; }
480-
481480
#endregion Properties
482481

483482
#region Engine State
@@ -634,12 +633,13 @@ internal HelpSystem HelpSystem
634633
/// </summary>
635634
/// <param name="command">The name of the command to lookup.</param>
636635
/// <param name="dotSource"></param>
636+
/// <param name="forCompletion"></param>
637637
/// <returns>The command processor object.</returns>
638-
internal CommandProcessorBase CreateCommand(string command, bool dotSource)
638+
internal CommandProcessorBase CreateCommand(string command, bool dotSource, bool forCompletion = false)
639639
{
640640
CommandOrigin commandOrigin = this.EngineSessionState.CurrentScope.ScopeOrigin;
641641
CommandProcessorBase commandProcessor =
642-
CommandDiscovery.LookupCommandProcessor(command, commandOrigin, !dotSource);
642+
CommandDiscovery.LookupCommandProcessor(command, commandOrigin, !dotSource, forCompletion);
643643
// Reset the command origin for script commands... // BUGBUG - dotting can get around command origin checks???
644644
if (commandProcessor != null && commandProcessor is ScriptCommandProcessorBase)
645645
{

test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1

+30
Original file line numberDiff line numberDiff line change
@@ -1783,6 +1783,36 @@ class InheritedClassTest : System.Attribute
17831783
}
17841784
}
17851785

1786+
Context "Script parameter completion" {
1787+
BeforeAll {
1788+
Setup -File -Path 'ModuleReqTest.ps1' -Content @'
1789+
#requires -Modules ThisModuleDoesNotExist
1790+
param ($Param1)
1791+
'@
1792+
Setup -File -Path 'AdminReqTest.ps1' -Content @'
1793+
#requires -RunAsAdministrator
1794+
param ($Param1)
1795+
'@
1796+
Push-Location ${TestDrive}\
1797+
}
1798+
1799+
AfterAll {
1800+
Pop-Location
1801+
}
1802+
1803+
It "Input should successfully complete script parameter for script with failed script requirements" {
1804+
$res = TabExpansion2 -inputScript '.\ModuleReqTest.ps1 -'
1805+
$res.CompletionMatches.Count | Should -BeGreaterThan 0
1806+
$res.CompletionMatches[0].CompletionText | Should -BeExactly '-Param1'
1807+
}
1808+
1809+
It "Input should successfully complete script parameter for admin script while not elevated" {
1810+
$res = TabExpansion2 -inputScript '.\AdminReqTest.ps1 -'
1811+
$res.CompletionMatches.Count | Should -BeGreaterThan 0
1812+
$res.CompletionMatches[0].CompletionText | Should -BeExactly '-Param1'
1813+
}
1814+
}
1815+
17861816
Context "File name completion" {
17871817
BeforeAll {
17881818
$tempDir = Join-Path -Path $TestDrive -ChildPath "baseDir"

0 commit comments

Comments
 (0)