From fdb0b66412373075fc549e2d40b0e6f36a934f2f Mon Sep 17 00:00:00 2001 From: yotsuda Date: Wed, 19 Nov 2025 10:20:48 +0900 Subject: [PATCH 01/20] Add tab completion for $PSBoundParameters access patterns When working with $PSBoundParameters, tab completion now suggests parameter names from the enclosing function or scriptblock's param block in the following scenarios: - switch ($PSBoundParameters.Keys) { } - $PSBoundParameters.ContainsKey('') - $PSBoundParameters[''] - $PSBoundParameters.Remove('') This helps prevent typos when writing code that checks or accesses bound parameter names. Fixes #25349 --- .../CommandCompletion/CompletionAnalysis.cs | 292 ++++++++++++++++++ .../TabCompletion/TabCompletion.Tests.ps1 | 137 ++++++++ 2 files changed, 429 insertions(+) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index 8aafa939d84..b2125355d8a 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -280,6 +280,262 @@ private static bool CompleteAgainstSwitchFile(Ast lastAst, Token tokenBeforeCurs return false; } + /// + /// Check if we should complete parameter names for switch cases on $PSBoundParameters.Keys + /// + private static List CompleteAgainstSwitchCaseCondition(CompletionContext completionContext) + { + var lastAst = completionContext.RelatedAsts.Last(); + + PipelineAst conditionPipeline = null; + Ast switchAst = null; + + // Check if we're in a switch statement (complete) or error statement (incomplete switch) + var switchStatementAst = lastAst.Parent as SwitchStatementAst; + if (switchStatementAst != null) + { + // Verify that the lastAst is one of the clause conditions (not in the body) + bool isClauseCondition = false; + foreach (var clause in switchStatementAst.Clauses) + { + if (clause.Item1 == lastAst) + { + isClauseCondition = true; + break; + } + } + + if (!isClauseCondition) + { + return null; + } + + conditionPipeline = switchStatementAst.Condition as PipelineAst; + switchAst = switchStatementAst; + } + else + { + // Check for incomplete switch parsed as ErrorStatementAst + var errorStatementAst = lastAst.Parent as ErrorStatementAst; + if (errorStatementAst == null || errorStatementAst.Kind == null || + errorStatementAst.Kind.Kind != TokenKind.Switch) + { + return null; + } + + // For ErrorStatementAst, the case value is in Bodies, condition is in Conditions + bool isInBodies = false; + if (errorStatementAst.Bodies != null) + { + foreach (var body in errorStatementAst.Bodies) + { + if (body == lastAst) + { + isInBodies = true; + break; + } + } + } + + if (!isInBodies) + { + return null; + } + + // Get the condition from ErrorStatementAst.Conditions + if (errorStatementAst.Conditions != null && errorStatementAst.Conditions.Count > 0) + { + conditionPipeline = errorStatementAst.Conditions[0] as PipelineAst; + } + switchAst = errorStatementAst; + } + + if (conditionPipeline == null || conditionPipeline.PipelineElements.Count != 1) + { + return null; + } + + var commandExpressionAst = conditionPipeline.PipelineElements[0] as CommandExpressionAst; + if (commandExpressionAst == null) + { + return null; + } + + // Check if the expression is a member access on $PSBoundParameters.Keys + var memberExpressionAst = commandExpressionAst.Expression as MemberExpressionAst; + if (memberExpressionAst == null) + { + return null; + } + + // Check if the target is $PSBoundParameters + var variableExpressionAst = memberExpressionAst.Expression as VariableExpressionAst; + if (variableExpressionAst == null || + !variableExpressionAst.VariablePath.UserPath.Equals("PSBoundParameters", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + // Check if the member is "Keys" + var memberNameAst = memberExpressionAst.Member as StringConstantExpressionAst; + if (memberNameAst == null || + !memberNameAst.Value.Equals("Keys", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + // Find the nearest param block by traversing up the AST + ParamBlockAst paramBlockAst = null; + Ast current = switchAst.Parent; + while (current != null) + { + if (current is FunctionDefinitionAst functionDefinitionAst) + { + paramBlockAst = functionDefinitionAst.Body?.ParamBlock; + break; + } + else if (current is ScriptBlockAst scriptBlockAst) + { + paramBlockAst = scriptBlockAst.ParamBlock; + if (paramBlockAst != null) + { + break; + } + } + + current = current.Parent; + } + + if (paramBlockAst == null || paramBlockAst.Parameters.Count == 0) + { + return null; + } + + // Generate completion results from parameter names + var result = new List(); + var wordToComplete = completionContext.WordToComplete ?? string.Empty; + + foreach (var parameter in paramBlockAst.Parameters) + { + var parameterName = parameter.Name.VariablePath.UserPath; + if (parameterName.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase)) + { + result.Add(new CompletionResult( + parameterName, + parameterName, + CompletionResultType.ParameterValue, + parameterName)); + } + } + + return result.Count > 0 ? result : null; + } + + /// + /// Check if we should complete parameter names for $PSBoundParameters access patterns + /// Supports: $PSBoundParameters.ContainsKey('...'), $PSBoundParameters['...'], $PSBoundParameters.Remove('...') + /// + private static List CompleteAgainstPSBoundParametersAccess(CompletionContext completionContext) + { + var lastAst = completionContext.RelatedAsts.Last(); + + // Must be a string constant + if (!(lastAst is StringConstantExpressionAst stringAst)) + { + return null; + } + + Ast targetAst = null; + + // Check for method invocation: $PSBoundParameters.ContainsKey('...') or $PSBoundParameters.Remove('...') + if (lastAst.Parent is InvokeMemberExpressionAst invokeMemberAst) + { + var memberName = invokeMemberAst.Member as StringConstantExpressionAst; + if (memberName != null && + (memberName.Value.Equals("ContainsKey", StringComparison.OrdinalIgnoreCase) || + memberName.Value.Equals("Remove", StringComparison.OrdinalIgnoreCase))) + { + targetAst = invokeMemberAst.Expression; + } + } + // Check for indexer: $PSBoundParameters['...'] + else if (lastAst.Parent is IndexExpressionAst indexAst) + { + targetAst = indexAst.Target; + } + + if (targetAst == null) + { + return null; + } + + // Check if target is $PSBoundParameters + var variableAst = targetAst as VariableExpressionAst; + if (variableAst == null || + !variableAst.VariablePath.UserPath.Equals("PSBoundParameters", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + // Find the nearest param block + ParamBlockAst paramBlockAst = null; + Ast current = lastAst.Parent; + while (current != null) + { + if (current is FunctionDefinitionAst functionDefinitionAst) + { + paramBlockAst = functionDefinitionAst.Body?.ParamBlock; + break; + } + else if (current is ScriptBlockAst scriptBlockAst) + { + paramBlockAst = scriptBlockAst.ParamBlock; + if (paramBlockAst != null) + { + break; + } + } + + current = current.Parent; + } + + if (paramBlockAst == null || paramBlockAst.Parameters.Count == 0) + { + return null; + } + + // Generate completion results from parameter names + var result = new List(); + var wordToComplete = completionContext.WordToComplete ?? string.Empty; + + // Determine quote style based on the string constant type + string quoteChar = string.Empty; + if (stringAst.StringConstantType == StringConstantType.SingleQuoted) + { + quoteChar = "'"; + } + else if (stringAst.StringConstantType == StringConstantType.DoubleQuoted) + { + quoteChar = "\""; + } + + foreach (var parameter in paramBlockAst.Parameters) + { + var parameterName = parameter.Name.VariablePath.UserPath; + if (parameterName.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase)) + { + var completionText = quoteChar + parameterName + quoteChar; + result.Add(new CompletionResult( + completionText, + parameterName, + CompletionResultType.ParameterValue, + parameterName)); + } + } + + return result.Count > 0 ? result : null; + } + private static bool CompleteOperator(Token tokenAtCursor, Ast lastAst) { if (tokenAtCursor.Kind == TokenKind.Minus) @@ -516,6 +772,13 @@ internal List GetResultHelper(CompletionContext completionCont { // Handles quoted string inside index expression like: $PSVersionTable[""] completionContext.WordToComplete = (tokenAtCursor as StringToken).Value; + // Check for $PSBoundParameters indexer first + var psBoundResult = CompleteAgainstPSBoundParametersAccess(completionContext); + if (psBoundResult != null && psBoundResult.Count > 0) + { + return psBoundResult; + } + return CompletionCompleters.CompleteIndexExpression(completionContext, indexExpressionAst.Target); } @@ -1915,6 +2178,21 @@ private static List GetResultForString(CompletionContext compl string strValue = constantString != null ? constantString.Value : expandableString.Value; + // Check for switch case completion on $PSBoundParameters.Keys + completionContext.WordToComplete = strValue; + var switchCaseResult = CompleteAgainstSwitchCaseCondition(completionContext); + if (switchCaseResult != null && switchCaseResult.Count > 0) + { + return switchCaseResult; + } + + // Check for $PSBoundParameters access patterns (ContainsKey, indexer, Remove) + var psBoundResult = CompleteAgainstPSBoundParametersAccess(completionContext); + if (psBoundResult != null && psBoundResult.Count > 0) + { + return psBoundResult; + } + bool shouldContinue; List result = GetResultForEnumPropertyValueOfDSCResource(completionContext, strValue, ref replacementIndex, ref replacementLength, out shouldContinue); if (!shouldContinue || (result != null && result.Count > 0)) @@ -2076,6 +2354,20 @@ private static List GetResultForIdentifier(CompletionContext c var tokenAtCursorText = tokenAtCursor.Text; completionContext.WordToComplete = tokenAtCursorText; + // Check for switch case completion on $PSBoundParameters.Keys + var switchCaseResult = CompleteAgainstSwitchCaseCondition(completionContext); + if (switchCaseResult != null && switchCaseResult.Count > 0) + { + return switchCaseResult; + } + + // Check for $PSBoundParameters access patterns (ContainsKey, indexer, Remove) + var psBoundResult = CompleteAgainstPSBoundParametersAccess(completionContext); + if (psBoundResult != null && psBoundResult.Count > 0) + { + return psBoundResult; + } + if (lastAst.Parent is BreakStatementAst || lastAst.Parent is ContinueStatementAst) { return CompleteLoopLabel(completionContext); diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index 046d00b442f..ab6617d29b0 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -4012,4 +4012,141 @@ Describe "WSMan Config Provider tab complete tests" -Tags Feature,RequireAdminOn # https://github.com/PowerShell/PowerShell/issues/4744 # TODO: move to test cases above once working } + + Context "Tab completion for switch cases on `$PSBoundParameters.Keys" { + It "Should complete parameter names in switch case for `$PSBoundParameters.Keys" { + $inputScript = @" +function Test-Func { + param( + [string]`$Param1, + [string]`$Param2, + [int]`$Count + ) + switch (`$PSBoundParameters.Keys) { + P + } +} +"@ + $cursorPosition = $inputScript.IndexOf("P", $inputScript.IndexOf("Keys)")) + 1 + $res = TabExpansion2 -inputScript $inputScript -cursorColumn $cursorPosition + $res.CompletionMatches | Should -HaveCount 2 + $completionTexts = $res.CompletionMatches.CompletionText | Sort-Object + $completionTexts[0] | Should -BeExactly "Param1" + $completionTexts[1] | Should -BeExactly "Param2" + } + + It "Should complete all parameter names when prefix matches single param" { + $inputScript = @" +function Test-Func { + param( + [string]`$Name, + [int]`$Value + ) + switch (`$PSBoundParameters.Keys) { + N + } +} +"@ + $cursorPosition = $inputScript.IndexOf("N", $inputScript.IndexOf("Keys)")) + 1 + $res = TabExpansion2 -inputScript $inputScript -cursorColumn $cursorPosition + $res.CompletionMatches | Should -HaveCount 1 + $res.CompletionMatches[0].CompletionText | Should -BeExactly "Name" + } + + It "Should complete parameter names in scriptblock param" { + $inputScript = @" +`$sb = { + param( + [string]`$ScriptParam1, + [string]`$ScriptParam2 + ) + switch (`$PSBoundParameters.Keys) { + S + } +} +"@ + $cursorPosition = $inputScript.IndexOf("S", $inputScript.IndexOf("Keys)")) + 1 + $res = TabExpansion2 -inputScript $inputScript -cursorColumn $cursorPosition + $res.CompletionMatches | Should -HaveCount 2 + $completionTexts = $res.CompletionMatches.CompletionText | Sort-Object + $completionTexts[0] | Should -BeExactly "ScriptParam1" + $completionTexts[1] | Should -BeExactly "ScriptParam2" + } + + Context "Tab completion for `$PSBoundParameters access patterns" { + It "Should complete parameter names for ContainsKey method" { + $inputScript = @" +function Test-Func { + param([string]`$Param1, [string]`$Param2, [int]`$Count) + if (`$PSBoundParameters.ContainsKey('P')) { } +} +"@ + $cursorPosition = $inputScript.IndexOf("'P'") + 2 + $res = TabExpansion2 -inputScript $inputScript -cursorColumn $cursorPosition + $res.CompletionMatches | Should -HaveCount 2 + $completionTexts = $res.CompletionMatches.CompletionText | Sort-Object + $completionTexts[0] | Should -BeExactly "'Param1'" + $completionTexts[1] | Should -BeExactly "'Param2'" + } + + It "Should complete parameter names for indexer access" { + $inputScript = @" +function Test-Func { + param([string]`$Param1, [string]`$Param2, [int]`$Count) + `$value = `$PSBoundParameters['P'] +} +"@ + $cursorPosition = $inputScript.IndexOf("'P'") + 2 + $res = TabExpansion2 -inputScript $inputScript -cursorColumn $cursorPosition + $res.CompletionMatches | Should -HaveCount 2 + $completionTexts = $res.CompletionMatches.CompletionText | Sort-Object + $completionTexts[0] | Should -BeExactly "'Param1'" + $completionTexts[1] | Should -BeExactly "'Param2'" + } + + It "Should complete parameter names for Remove method" { + $inputScript = @" +function Test-Func { + param([string]`$Param1, [string]`$Param2, [int]`$Count) + `$PSBoundParameters.Remove('C') +} +"@ + $cursorPosition = $inputScript.IndexOf("'C'") + 2 + $res = TabExpansion2 -inputScript $inputScript -cursorColumn $cursorPosition + $res.CompletionMatches | Should -HaveCount 1 + $res.CompletionMatches[0].CompletionText | Should -BeExactly "'Count'" + } + + It "Should complete with double quotes when using double-quoted string" { + $inputScript = @" +function Test-Func { + param([string]`$Param1, [string]`$Param2) + if (`$PSBoundParameters.ContainsKey("P")) { } +} +"@ + $cursorPosition = $inputScript.IndexOf('"P"') + 2 + $res = TabExpansion2 -inputScript $inputScript -cursorColumn $cursorPosition + $res.CompletionMatches | Should -HaveCount 2 + $completionTexts = $res.CompletionMatches.CompletionText | Sort-Object + $completionTexts[0] | Should -BeExactly '"Param1"' + $completionTexts[1] | Should -BeExactly '"Param2"' + } + + It "Should not complete for non-PSBoundParameters variable" { + $inputScript = @" +function Test-Func { + param([string]`$Param1) + `$hash = @{} + `$value = `$hash['P'] +} +"@ + $cursorPosition = $inputScript.IndexOf("'P'") + 2 + $res = TabExpansion2 -inputScript $inputScript -cursorColumn $cursorPosition + # Should not return Param1 as completion + $paramCompletion = $res.CompletionMatches | Where-Object { $_.CompletionText -eq "'Param1'" } + $paramCompletion | Should -BeNullOrEmpty + } + } + } + } From da9408d0b0721535990d54c28a8ec26ee8be6146 Mon Sep 17 00:00:00 2001 From: yotsuda Date: Wed, 19 Nov 2025 13:24:37 +0900 Subject: [PATCH 02/20] Fix trailing whitespace and test Context block structure --- .../engine/CommandCompletion/CompletionAnalysis.cs | 8 ++++---- .../powershell/Host/TabCompletion/TabCompletion.Tests.ps1 | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index b2125355d8a..44b10d8ad9e 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -286,7 +286,7 @@ private static bool CompleteAgainstSwitchFile(Ast lastAst, Token tokenBeforeCurs private static List CompleteAgainstSwitchCaseCondition(CompletionContext completionContext) { var lastAst = completionContext.RelatedAsts.Last(); - + PipelineAst conditionPipeline = null; Ast switchAst = null; @@ -317,7 +317,7 @@ private static List CompleteAgainstSwitchCaseCondition(Complet { // Check for incomplete switch parsed as ErrorStatementAst var errorStatementAst = lastAst.Parent as ErrorStatementAst; - if (errorStatementAst == null || errorStatementAst.Kind == null || + if (errorStatementAst == null || errorStatementAst.Kind == null || errorStatementAst.Kind.Kind != TokenKind.Switch) { return null; @@ -438,7 +438,7 @@ private static List CompleteAgainstSwitchCaseCondition(Complet private static List CompleteAgainstPSBoundParametersAccess(CompletionContext completionContext) { var lastAst = completionContext.RelatedAsts.Last(); - + // Must be a string constant if (!(lastAst is StringConstantExpressionAst stringAst)) { @@ -451,7 +451,7 @@ private static List CompleteAgainstPSBoundParametersAccess(Com if (lastAst.Parent is InvokeMemberExpressionAst invokeMemberAst) { var memberName = invokeMemberAst.Member as StringConstantExpressionAst; - if (memberName != null && + if (memberName != null && (memberName.Value.Equals("ContainsKey", StringComparison.OrdinalIgnoreCase) || memberName.Value.Equals("Remove", StringComparison.OrdinalIgnoreCase))) { diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index ab6617d29b0..e707753fe5f 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -4072,6 +4072,7 @@ function Test-Func { $completionTexts[0] | Should -BeExactly "ScriptParam1" $completionTexts[1] | Should -BeExactly "ScriptParam2" } + } Context "Tab completion for `$PSBoundParameters access patterns" { It "Should complete parameter names for ContainsKey method" { @@ -4147,6 +4148,4 @@ function Test-Func { $paramCompletion | Should -BeNullOrEmpty } } - } - } From f87ff1a6c3012e54d4ea7a7e35e0e1549ea0ee9b Mon Sep 17 00:00:00 2001 From: yotsuda Date: Wed, 19 Nov 2025 16:27:51 +0900 Subject: [PATCH 03/20] Update src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../engine/CommandCompletion/CompletionAnalysis.cs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index 44b10d8ad9e..8f77c4323fb 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -324,18 +324,7 @@ private static List CompleteAgainstSwitchCaseCondition(Complet } // For ErrorStatementAst, the case value is in Bodies, condition is in Conditions - bool isInBodies = false; - if (errorStatementAst.Bodies != null) - { - foreach (var body in errorStatementAst.Bodies) - { - if (body == lastAst) - { - isInBodies = true; - break; - } - } - } + bool isInBodies = errorStatementAst.Bodies != null && errorStatementAst.Bodies.Any(body => body == lastAst); if (!isInBodies) { From be012485d36e7ef79b6e5e8d7965567361b786cc Mon Sep 17 00:00:00 2001 From: yotsuda Date: Wed, 19 Nov 2025 18:47:20 +0900 Subject: [PATCH 04/20] Update src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../CommandCompletion/CompletionAnalysis.cs | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index 8f77c4323fb..ca4b3649396 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -404,18 +404,15 @@ private static List CompleteAgainstSwitchCaseCondition(Complet var result = new List(); var wordToComplete = completionContext.WordToComplete ?? string.Empty; - foreach (var parameter in paramBlockAst.Parameters) - { - var parameterName = parameter.Name.VariablePath.UserPath; - if (parameterName.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase)) - { - result.Add(new CompletionResult( - parameterName, - parameterName, - CompletionResultType.ParameterValue, - parameterName)); - } - } + result = paramBlockAst.Parameters + .Select(parameter => parameter.Name.VariablePath.UserPath) + .Where(parameterName => parameterName.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase)) + .Select(parameterName => new CompletionResult( + parameterName, + parameterName, + CompletionResultType.ParameterValue, + parameterName)) + .ToList(); return result.Count > 0 ? result : null; } From 8412e172e7f43a2590e4ee22b415d4b94c661ce6 Mon Sep 17 00:00:00 2001 From: yotsuda Date: Wed, 19 Nov 2025 18:47:45 +0900 Subject: [PATCH 05/20] Update src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../CommandCompletion/CompletionAnalysis.cs | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index ca4b3649396..d8c75b8cf47 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -505,19 +505,17 @@ private static List CompleteAgainstPSBoundParametersAccess(Com quoteChar = "\""; } - foreach (var parameter in paramBlockAst.Parameters) - { - var parameterName = parameter.Name.VariablePath.UserPath; - if (parameterName.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase)) - { - var completionText = quoteChar + parameterName + quoteChar; - result.Add(new CompletionResult( - completionText, - parameterName, - CompletionResultType.ParameterValue, - parameterName)); - } - } + result.AddRange( + paramBlockAst.Parameters + .Select(parameter => parameter.Name.VariablePath.UserPath) + .Where(parameterName => parameterName.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase)) + .Select(parameterName => + new CompletionResult( + quoteChar + parameterName + quoteChar, + parameterName, + CompletionResultType.ParameterValue, + parameterName)) + ); return result.Count > 0 ? result : null; } From 867fc9ec705928a6cc68e90353c8981b30bd3c63 Mon Sep 17 00:00:00 2001 From: yotsuda Date: Wed, 19 Nov 2025 18:50:17 +0900 Subject: [PATCH 06/20] Refactor foreach loop to LINQ Any() for clause condition check --- .../engine/CommandCompletion/CompletionAnalysis.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index d8c75b8cf47..ffdb77d2d18 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -295,15 +295,7 @@ private static List CompleteAgainstSwitchCaseCondition(Complet if (switchStatementAst != null) { // Verify that the lastAst is one of the clause conditions (not in the body) - bool isClauseCondition = false; - foreach (var clause in switchStatementAst.Clauses) - { - if (clause.Item1 == lastAst) - { - isClauseCondition = true; - break; - } - } + bool isClauseCondition = switchStatementAst.Clauses.Any(clause => clause.Item1 == lastAst); if (!isClauseCondition) { From c107e4455e8c38fe728109634439f4c11e332ba1 Mon Sep 17 00:00:00 2001 From: yotsuda Date: Thu, 20 Nov 2025 09:28:27 +0900 Subject: [PATCH 07/20] Update src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs Co-authored-by: MartinGC94 <42123497+MartinGC94@users.noreply.github.com> --- .../engine/CommandCompletion/CompletionAnalysis.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index ffdb77d2d18..5bb774f3315 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -418,7 +418,7 @@ private static List CompleteAgainstPSBoundParametersAccess(Com var lastAst = completionContext.RelatedAsts.Last(); // Must be a string constant - if (!(lastAst is StringConstantExpressionAst stringAst)) + if (lastAst is not StringConstantExpressionAst stringAst) { return null; } From f6a51190378380b15756362a27f1b5e7b5481343 Mon Sep 17 00:00:00 2001 From: yotsuda Date: Thu, 20 Nov 2025 09:28:48 +0900 Subject: [PATCH 08/20] Update src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs Co-authored-by: MartinGC94 <42123497+MartinGC94@users.noreply.github.com> --- .../engine/CommandCompletion/CompletionAnalysis.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index 5bb774f3315..2fb8b029ca5 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -428,10 +428,9 @@ private static List CompleteAgainstPSBoundParametersAccess(Com // Check for method invocation: $PSBoundParameters.ContainsKey('...') or $PSBoundParameters.Remove('...') if (lastAst.Parent is InvokeMemberExpressionAst invokeMemberAst) { - var memberName = invokeMemberAst.Member as StringConstantExpressionAst; - if (memberName != null && - (memberName.Value.Equals("ContainsKey", StringComparison.OrdinalIgnoreCase) || - memberName.Value.Equals("Remove", StringComparison.OrdinalIgnoreCase))) + if (invokeMemberAst.Member is StringConstantExpressionAst memberName && + (memberName.Value.Equals("ContainsKey", StringComparison.OrdinalIgnoreCase) || + memberName.Value.Equals("Remove", StringComparison.OrdinalIgnoreCase))) { targetAst = invokeMemberAst.Expression; } From a17c6cfbf64199f25e355f7eef7dc20dda96420d Mon Sep 17 00:00:00 2001 From: yotsuda Date: Thu, 20 Nov 2025 09:28:59 +0900 Subject: [PATCH 09/20] Update src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs Co-authored-by: MartinGC94 <42123497+MartinGC94@users.noreply.github.com> --- .../engine/CommandCompletion/CompletionAnalysis.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index 2fb8b029ca5..6233d05b926 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -441,7 +441,7 @@ private static List CompleteAgainstPSBoundParametersAccess(Com targetAst = indexAst.Target; } - if (targetAst == null) + if (targetAst is null) { return null; } From 12cbe5cddf5096b00306b104f5b42375b76398c9 Mon Sep 17 00:00:00 2001 From: yotsuda Date: Thu, 20 Nov 2025 09:29:12 +0900 Subject: [PATCH 10/20] Update src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs Co-authored-by: MartinGC94 <42123497+MartinGC94@users.noreply.github.com> --- .../engine/CommandCompletion/CompletionAnalysis.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index 6233d05b926..5165b501056 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -447,9 +447,8 @@ private static List CompleteAgainstPSBoundParametersAccess(Com } // Check if target is $PSBoundParameters - var variableAst = targetAst as VariableExpressionAst; - if (variableAst == null || - !variableAst.VariablePath.UserPath.Equals("PSBoundParameters", StringComparison.OrdinalIgnoreCase)) + if (targetAst is not VariableExpressionAst variableAst || + !variableAst.VariablePath.UserPath.Equals("PSBoundParameters", StringComparison.OrdinalIgnoreCase)) { return null; } From c673ff4a30f13ec21318a1c9a581141a097a5bfe Mon Sep 17 00:00:00 2001 From: yotsuda Date: Thu, 20 Nov 2025 09:29:25 +0900 Subject: [PATCH 11/20] Update src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs Co-authored-by: MartinGC94 <42123497+MartinGC94@users.noreply.github.com> --- .../engine/CommandCompletion/CompletionAnalysis.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index 5165b501056..7acfb5b2fa1 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -423,7 +423,7 @@ private static List CompleteAgainstPSBoundParametersAccess(Com return null; } - Ast targetAst = null; + ExpressionAst targetAst = null; // Check for method invocation: $PSBoundParameters.ContainsKey('...') or $PSBoundParameters.Remove('...') if (lastAst.Parent is InvokeMemberExpressionAst invokeMemberAst) From ed1d034d0ee1bbe6a8d8a77fdce08fa88fa0f8bb Mon Sep 17 00:00:00 2001 From: yotsuda Date: Thu, 20 Nov 2025 09:29:53 +0900 Subject: [PATCH 12/20] Update src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs Co-authored-by: MartinGC94 <42123497+MartinGC94@users.noreply.github.com> --- .../engine/CommandCompletion/CompletionAnalysis.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index 7acfb5b2fa1..dc0dce46aa0 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -291,8 +291,7 @@ private static List CompleteAgainstSwitchCaseCondition(Complet Ast switchAst = null; // Check if we're in a switch statement (complete) or error statement (incomplete switch) - var switchStatementAst = lastAst.Parent as SwitchStatementAst; - if (switchStatementAst != null) + if (lastAst.Parent is SwitchStatementAst switchStatementAst) { // Verify that the lastAst is one of the clause conditions (not in the body) bool isClauseCondition = switchStatementAst.Clauses.Any(clause => clause.Item1 == lastAst); From 7303784610d8c5f42be6cfe632c575ef3f7eb566 Mon Sep 17 00:00:00 2001 From: yotsuda Date: Thu, 20 Nov 2025 21:35:06 +0900 Subject: [PATCH 13/20] Update src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../engine/CommandCompletion/CompletionAnalysis.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index dc0dce46aa0..32f7edd1693 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -428,8 +428,8 @@ private static List CompleteAgainstPSBoundParametersAccess(Com if (lastAst.Parent is InvokeMemberExpressionAst invokeMemberAst) { if (invokeMemberAst.Member is StringConstantExpressionAst memberName && - (memberName.Value.Equals("ContainsKey", StringComparison.OrdinalIgnoreCase) || - memberName.Value.Equals("Remove", StringComparison.OrdinalIgnoreCase))) + (memberName.Value.Equals("ContainsKey", StringComparison.OrdinalIgnoreCase) || + memberName.Value.Equals("Remove", StringComparison.OrdinalIgnoreCase))) { targetAst = invokeMemberAst.Expression; } From c66e94f9bb16d7f3301d5f2d8ec7419d1305fb8c Mon Sep 17 00:00:00 2001 From: yotsuda Date: Thu, 20 Nov 2025 21:35:19 +0900 Subject: [PATCH 14/20] Update src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../engine/CommandCompletion/CompletionAnalysis.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index 32f7edd1693..7d2fbfb7817 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -392,10 +392,9 @@ private static List CompleteAgainstSwitchCaseCondition(Complet } // Generate completion results from parameter names - var result = new List(); var wordToComplete = completionContext.WordToComplete ?? string.Empty; - result = paramBlockAst.Parameters + var result = paramBlockAst.Parameters .Select(parameter => parameter.Name.VariablePath.UserPath) .Where(parameterName => parameterName.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase)) .Select(parameterName => new CompletionResult( From 3ad98c54477aab38582966ecb8f96725e5a3c6aa Mon Sep 17 00:00:00 2001 From: yotsuda Date: Thu, 20 Nov 2025 21:35:35 +0900 Subject: [PATCH 15/20] Update src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../engine/CommandCompletion/CompletionAnalysis.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index 7d2fbfb7817..315e8ca0792 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -446,7 +446,7 @@ private static List CompleteAgainstPSBoundParametersAccess(Com // Check if target is $PSBoundParameters if (targetAst is not VariableExpressionAst variableAst || - !variableAst.VariablePath.UserPath.Equals("PSBoundParameters", StringComparison.OrdinalIgnoreCase)) + !variableAst.VariablePath.UserPath.Equals("PSBoundParameters", StringComparison.OrdinalIgnoreCase)) { return null; } From 632bf0178a1a2a141cb04d243bc8588706b2c82a Mon Sep 17 00:00:00 2001 From: yotsuda Date: Fri, 21 Nov 2025 15:00:41 +0900 Subject: [PATCH 16/20] Address Copilot code review feedback - Remove useless initialization of result variable in CompleteAgainstPSBoundParametersAccess - Extract duplicate ParamBlock finding logic into FindNearestParamBlock helper method This refactoring improves code maintainability by reducing duplication between CompleteAgainstSwitchCaseCondition and CompleteAgainstPSBoundParametersAccess methods. --- .../CommandCompletion/CompletionAnalysis.cs | 93 ++++++++----------- 1 file changed, 41 insertions(+), 52 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index 315e8ca0792..f405a9b7cab 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -365,26 +365,7 @@ private static List CompleteAgainstSwitchCaseCondition(Complet } // Find the nearest param block by traversing up the AST - ParamBlockAst paramBlockAst = null; - Ast current = switchAst.Parent; - while (current != null) - { - if (current is FunctionDefinitionAst functionDefinitionAst) - { - paramBlockAst = functionDefinitionAst.Body?.ParamBlock; - break; - } - else if (current is ScriptBlockAst scriptBlockAst) - { - paramBlockAst = scriptBlockAst.ParamBlock; - if (paramBlockAst != null) - { - break; - } - } - - current = current.Parent; - } + var paramBlockAst = FindNearestParamBlock(switchAst.Parent); if (paramBlockAst == null || paramBlockAst.Parameters.Count == 0) { @@ -452,26 +433,7 @@ private static List CompleteAgainstPSBoundParametersAccess(Com } // Find the nearest param block - ParamBlockAst paramBlockAst = null; - Ast current = lastAst.Parent; - while (current != null) - { - if (current is FunctionDefinitionAst functionDefinitionAst) - { - paramBlockAst = functionDefinitionAst.Body?.ParamBlock; - break; - } - else if (current is ScriptBlockAst scriptBlockAst) - { - paramBlockAst = scriptBlockAst.ParamBlock; - if (paramBlockAst != null) - { - break; - } - } - - current = current.Parent; - } + var paramBlockAst = FindNearestParamBlock(lastAst.Parent); if (paramBlockAst == null || paramBlockAst.Parameters.Count == 0) { @@ -479,7 +441,6 @@ private static List CompleteAgainstPSBoundParametersAccess(Com } // Generate completion results from parameter names - var result = new List(); var wordToComplete = completionContext.WordToComplete ?? string.Empty; // Determine quote style based on the string constant type @@ -493,21 +454,49 @@ private static List CompleteAgainstPSBoundParametersAccess(Com quoteChar = "\""; } - result.AddRange( - paramBlockAst.Parameters - .Select(parameter => parameter.Name.VariablePath.UserPath) - .Where(parameterName => parameterName.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase)) - .Select(parameterName => - new CompletionResult( - quoteChar + parameterName + quoteChar, - parameterName, - CompletionResultType.ParameterValue, - parameterName)) - ); + var result = paramBlockAst.Parameters + .Select(parameter => parameter.Name.VariablePath.UserPath) + .Where(parameterName => parameterName.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase)) + .Select(parameterName => + new CompletionResult( + quoteChar + parameterName + quoteChar, + parameterName, + CompletionResultType.ParameterValue, + parameterName)) + .ToList(); return result.Count > 0 ? result : null; } + /// + /// Finds the nearest ParamBlockAst by traversing up the AST hierarchy. + /// + /// The AST node to start searching from. + /// The nearest ParamBlockAst if found; otherwise, null. + private static ParamBlockAst FindNearestParamBlock(Ast startAst) + { + Ast current = startAst; + while (current != null) + { + if (current is FunctionDefinitionAst functionDefinitionAst) + { + return functionDefinitionAst.Body?.ParamBlock; + } + else if (current is ScriptBlockAst scriptBlockAst) + { + var paramBlock = scriptBlockAst.ParamBlock; + if (paramBlock != null) + { + return paramBlock; + } + } + + current = current.Parent; + } + + return null; + } + private static bool CompleteOperator(Token tokenAtCursor, Ast lastAst) { if (tokenAtCursor.Kind == TokenKind.Minus) From 03e0e02e0202a95b3d9cc013fa287b4b65995f81 Mon Sep 17 00:00:00 2001 From: yotsuda Date: Fri, 21 Nov 2025 15:42:39 +0900 Subject: [PATCH 17/20] Extract parameter completion logic into helper method - Create CreateParameterCompletionResults helper method to reduce code duplication - Consolidate completion result generation logic between CompleteAgainstSwitchCaseCondition and CompleteAgainstPSBoundParametersAccess - Add optional quoteChar parameter to support both quoted and unquoted completion This addresses the second Copilot code review feedback and improves code maintainability. --- .../CommandCompletion/CompletionAnalysis.cs | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index f405a9b7cab..d7d093764ce 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -374,18 +374,7 @@ private static List CompleteAgainstSwitchCaseCondition(Complet // Generate completion results from parameter names var wordToComplete = completionContext.WordToComplete ?? string.Empty; - - var result = paramBlockAst.Parameters - .Select(parameter => parameter.Name.VariablePath.UserPath) - .Where(parameterName => parameterName.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase)) - .Select(parameterName => new CompletionResult( - parameterName, - parameterName, - CompletionResultType.ParameterValue, - parameterName)) - .ToList(); - - return result.Count > 0 ? result : null; + return CreateParameterCompletionResults(paramBlockAst, wordToComplete); } /// @@ -454,18 +443,7 @@ private static List CompleteAgainstPSBoundParametersAccess(Com quoteChar = "\""; } - var result = paramBlockAst.Parameters - .Select(parameter => parameter.Name.VariablePath.UserPath) - .Where(parameterName => parameterName.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase)) - .Select(parameterName => - new CompletionResult( - quoteChar + parameterName + quoteChar, - parameterName, - CompletionResultType.ParameterValue, - parameterName)) - .ToList(); - - return result.Count > 0 ? result : null; + return CreateParameterCompletionResults(paramBlockAst, wordToComplete, quoteChar); } /// @@ -497,6 +475,32 @@ private static ParamBlockAst FindNearestParamBlock(Ast startAst) return null; } + /// + /// Creates completion results from parameter names with optional quote wrapping. + /// + /// The parameter block containing parameters to complete. + /// The partial word to match against parameter names. + /// Optional quote character to wrap completion text (empty string for no quotes). + /// A list of completion results, or null if no matches found. + private static List CreateParameterCompletionResults( + ParamBlockAst paramBlockAst, + string wordToComplete, + string quoteChar = "") + { + var result = paramBlockAst.Parameters + .Select(parameter => parameter.Name.VariablePath.UserPath) + .Where(parameterName => parameterName.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase)) + .Select(parameterName => + new CompletionResult( + quoteChar + parameterName + quoteChar, + parameterName, + CompletionResultType.ParameterValue, + parameterName)) + .ToList(); + + return result.Count > 0 ? result : null; + } + private static bool CompleteOperator(Token tokenAtCursor, Ast lastAst) { if (tokenAtCursor.Kind == TokenKind.Minus) From 359f35fedd3c0a64147b3e6e1fd8c0e161a59054 Mon Sep 17 00:00:00 2001 From: yotsuda Date: Tue, 25 Nov 2025 13:03:33 +0900 Subject: [PATCH 18/20] Add negative tests for all PSBoundParameters access patterns Addresses review feedback to test all code paths for non-PSBoundParameters variables. The original single negative test is now split into four separate tests covering indexer, ContainsKey, Remove, and double-quoted string access patterns. --- .../TabCompletion/TabCompletion.Tests.ps1 | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index e707753fe5f..f8762a63929 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -4133,7 +4133,7 @@ function Test-Func { $completionTexts[1] | Should -BeExactly '"Param2"' } - It "Should not complete for non-PSBoundParameters variable" { + It "Should not complete for non-PSBoundParameters variable with indexer" { $inputScript = @" function Test-Func { param([string]`$Param1) @@ -4147,5 +4147,50 @@ function Test-Func { $paramCompletion = $res.CompletionMatches | Where-Object { $_.CompletionText -eq "'Param1'" } $paramCompletion | Should -BeNullOrEmpty } + + It "Should not complete for non-PSBoundParameters variable with ContainsKey" { + $inputScript = @" +function Test-Func { + param([string]`$Param1) + `$hash = @{} + if (`$hash.ContainsKey('P')) { } +} +"@ + $cursorPosition = $inputScript.IndexOf("'P'") + 2 + $res = TabExpansion2 -inputScript $inputScript -cursorColumn $cursorPosition + # Should not return Param1 as completion + $paramCompletion = $res.CompletionMatches | Where-Object { $_.CompletionText -eq "'Param1'" } + $paramCompletion | Should -BeNullOrEmpty + } + + It "Should not complete for non-PSBoundParameters variable with Remove" { + $inputScript = @" +function Test-Func { + param([string]`$Param1) + `$hash = @{} + `$hash.Remove('P') +} +"@ + $cursorPosition = $inputScript.IndexOf("'P'") + 2 + $res = TabExpansion2 -inputScript $inputScript -cursorColumn $cursorPosition + # Should not return Param1 as completion + $paramCompletion = $res.CompletionMatches | Where-Object { $_.CompletionText -eq "'Param1'" } + $paramCompletion | Should -BeNullOrEmpty + } + + It "Should not complete for non-PSBoundParameters variable with double quotes" { + $inputScript = @" +function Test-Func { + param([string]`$Param1) + `$hash = @{} + if (`$hash.ContainsKey("P")) { } +} +"@ + $cursorPosition = $inputScript.IndexOf('"P"') + 2 + $res = TabExpansion2 -inputScript $inputScript -cursorColumn $cursorPosition + # Should not return Param1 as completion + $paramCompletion = $res.CompletionMatches | Where-Object { $_.CompletionText -eq '"Param1"' } + $paramCompletion | Should -BeNullOrEmpty + } } } From 970a1835c11efdec9c5f145663b5fab1a2544322 Mon Sep 17 00:00:00 2001 From: Yoshifumi Date: Tue, 25 Nov 2025 23:43:05 +0900 Subject: [PATCH 19/20] Update src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs Co-authored-by: Ilya --- .../engine/CommandCompletion/CompletionAnalysis.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index d7d093764ce..eee4a1e95af 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -307,8 +307,7 @@ private static List CompleteAgainstSwitchCaseCondition(Complet else { // Check for incomplete switch parsed as ErrorStatementAst - var errorStatementAst = lastAst.Parent as ErrorStatementAst; - if (errorStatementAst == null || errorStatementAst.Kind == null || + if (lastAst.Parent is not ErrorStatementAst errorStatementAst || errorStatementAst.Kind == null || errorStatementAst.Kind.Kind != TokenKind.Switch) { return null; From 4439fbb3165edc758288616da0142b6d4917e6e3 Mon Sep 17 00:00:00 2001 From: yotsuda Date: Wed, 26 Nov 2025 09:24:40 +0900 Subject: [PATCH 20/20] Use modern C# pattern matching and 'is null' syntax --- .../CommandCompletion/CompletionAnalysis.cs | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index eee4a1e95af..a6b6f4170b3 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -251,8 +251,7 @@ private static bool CompleteAgainstSwitchFile(Ast lastAst, Token tokenBeforeCurs { Tuple fileConditionTuple; - var errorStatement = lastAst as ErrorStatementAst; - if (errorStatement != null && errorStatement.Flags != null && errorStatement.Kind != null && tokenBeforeCursor != null && + if (lastAst is ErrorStatementAst errorStatement && errorStatement.Flags is not null && errorStatement.Kind is not null && tokenBeforeCursor is not null && errorStatement.Kind.Kind.Equals(TokenKind.Switch) && errorStatement.Flags.TryGetValue("file", out fileConditionTuple)) { // Handle "switch -file " @@ -267,14 +266,13 @@ private static bool CompleteAgainstSwitchFile(Ast lastAst, Token tokenBeforeCurs return false; } - errorStatement = pipeline.Parent as ErrorStatementAst; - if (errorStatement == null || errorStatement.Kind == null || errorStatement.Flags == null) + if (pipeline.Parent is not ErrorStatementAst parentErrorStatement || parentErrorStatement.Kind is null || parentErrorStatement.Flags is null) { return false; } - return (errorStatement.Kind.Kind.Equals(TokenKind.Switch) && - errorStatement.Flags.TryGetValue("file", out fileConditionTuple) && fileConditionTuple.Item2 == pipeline); + return (parentErrorStatement.Kind.Kind.Equals(TokenKind.Switch) && + parentErrorStatement.Flags.TryGetValue("file", out fileConditionTuple) && fileConditionTuple.Item2 == pipeline); } return false; @@ -307,7 +305,7 @@ private static List CompleteAgainstSwitchCaseCondition(Complet else { // Check for incomplete switch parsed as ErrorStatementAst - if (lastAst.Parent is not ErrorStatementAst errorStatementAst || errorStatementAst.Kind == null || + if (lastAst.Parent is not ErrorStatementAst errorStatementAst || errorStatementAst.Kind is null || errorStatementAst.Kind.Kind != TokenKind.Switch) { return null; @@ -334,30 +332,26 @@ private static List CompleteAgainstSwitchCaseCondition(Complet return null; } - var commandExpressionAst = conditionPipeline.PipelineElements[0] as CommandExpressionAst; - if (commandExpressionAst == null) + if (conditionPipeline.PipelineElements[0] is not CommandExpressionAst commandExpressionAst) { return null; } // Check if the expression is a member access on $PSBoundParameters.Keys - var memberExpressionAst = commandExpressionAst.Expression as MemberExpressionAst; - if (memberExpressionAst == null) + if (commandExpressionAst.Expression is not MemberExpressionAst memberExpressionAst) { return null; } // Check if the target is $PSBoundParameters - var variableExpressionAst = memberExpressionAst.Expression as VariableExpressionAst; - if (variableExpressionAst == null || + if (memberExpressionAst.Expression is not VariableExpressionAst variableExpressionAst || !variableExpressionAst.VariablePath.UserPath.Equals("PSBoundParameters", StringComparison.OrdinalIgnoreCase)) { return null; } // Check if the member is "Keys" - var memberNameAst = memberExpressionAst.Member as StringConstantExpressionAst; - if (memberNameAst == null || + if (memberExpressionAst.Member is not StringConstantExpressionAst memberNameAst || !memberNameAst.Value.Equals("Keys", StringComparison.OrdinalIgnoreCase)) { return null;