diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 0dce0d9f88d..3e30a0c13f3 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -6455,7 +6455,9 @@ private static List CompleteCommentParameterValue(CompletionCo new List> { new Tuple("Where", "Where({ expression } [, mode [, numberToReturn]])"), - new Tuple("ForEach", "ForEach(expression [, arguments...])") + new Tuple("ForEach", "ForEach(expression [, arguments...])"), + new Tuple("PSWhere", "PSWhere({ expression } [, mode [, numberToReturn]])"), + new Tuple("PSForEach", "PSForEach(expression [, arguments...])"), }; // List of DSC collection-value variables diff --git a/src/System.Management.Automation/engine/runtime/Binding/Binders.cs b/src/System.Management.Automation/engine/runtime/Binding/Binders.cs index f4020547432..54b23ee412a 100644 --- a/src/System.Management.Automation/engine/runtime/Binding/Binders.cs +++ b/src/System.Management.Automation/engine/runtime/Binding/Binders.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Buffers; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; @@ -6533,6 +6534,14 @@ public override DynamicMetaObject FallbackInvoke(DynamicMetaObject target, Dynam internal sealed class PSInvokeMemberBinder : InvokeMemberBinder { + private static readonly SearchValues s_whereSearchValues = SearchValues.Create( + ["Where", "PSWhere"], + StringComparison.OrdinalIgnoreCase); + + private static readonly SearchValues s_foreachSearchValues = SearchValues.Create( + ["ForEach", "PSForEach"], + StringComparison.OrdinalIgnoreCase); + internal enum MethodInvocationType { Ordinary, @@ -6681,12 +6690,14 @@ public override DynamicMetaObject FallbackInvokeMember(DynamicMetaObject target, .WriteToDebugLog(this); BindingRestrictions argRestrictions = args.Aggregate(BindingRestrictions.Empty, static (current, arg) => current.Merge(arg.PSGetMethodArgumentRestriction())); - if (string.Equals(Name, "Where", StringComparison.OrdinalIgnoreCase)) + // We need to pass the empty enumerator to the ForEach/Where operators, so that they can return an empty collection. + // The ForEach/Where operators will not be able to call the script block if the enumerator is empty. + if (s_whereSearchValues.Contains(Name)) { return InvokeWhereOnCollection(emptyEnumerator, args, argRestrictions).WriteToDebugLog(this); } - if (string.Equals(Name, "ForEach", StringComparison.OrdinalIgnoreCase)) + if (s_foreachSearchValues.Contains(Name)) { return InvokeForEachOnCollection(emptyEnumerator, args, argRestrictions).WriteToDebugLog(this); } @@ -6866,12 +6877,12 @@ public override DynamicMetaObject FallbackInvokeMember(DynamicMetaObject target, if (!_static && !_nonEnumerating && target.Value != AutomationNull.Value) { // Invoking Where and ForEach operators on collections. - if (string.Equals(Name, "Where", StringComparison.OrdinalIgnoreCase)) + if (s_whereSearchValues.Contains(Name)) { return InvokeWhereOnCollection(target, args, restrictions).WriteToDebugLog(this); } - if (string.Equals(Name, "ForEach", StringComparison.OrdinalIgnoreCase)) + if (s_foreachSearchValues.Contains(Name)) { return InvokeForEachOnCollection(target, args, restrictions).WriteToDebugLog(this); } @@ -7490,7 +7501,7 @@ internal static object InvokeAdaptedMember(object obj, string methodName, object // As a last resort, we invoke 'Where' and 'ForEach' operators on singletons like // ([pscustomobject]@{ foo = 'bar' }).Foreach({$_}) // ([pscustomobject]@{ foo = 'bar' }).Where({1}) - if (string.Equals(methodName, "Where", StringComparison.OrdinalIgnoreCase)) + if (s_whereSearchValues.Contains(methodName)) { var enumerator = (new object[] { obj }).GetEnumerator(); switch (args.Length) @@ -7506,7 +7517,7 @@ internal static object InvokeAdaptedMember(object obj, string methodName, object } } - if (string.Equals(methodName, "Foreach", StringComparison.OrdinalIgnoreCase)) + if (s_foreachSearchValues.Contains(methodName)) { var enumerator = (new object[] { obj }).GetEnumerator(); object[] argsToPass; diff --git a/test/powershell/engine/ETS/Adapter.Tests.ps1 b/test/powershell/engine/ETS/Adapter.Tests.ps1 index 7876dd778a6..9e513b5d80e 100644 --- a/test/powershell/engine/ETS/Adapter.Tests.ps1 +++ b/test/powershell/engine/ETS/Adapter.Tests.ps1 @@ -200,6 +200,11 @@ Describe "Adapter Tests" -tags "CI" { # TODO: dynamic method calls } + + It "Can use PSForEach as an alias for the Foreach magic method" { + $x = 5 + $x.PSForEach({$_}) | Should -Be 5 + } } Context "Where Magic Method Adapter Tests" { @@ -240,6 +245,11 @@ Describe "Adapter Tests" -tags "CI" { } -PassThru -Force $x.Where(5) | Should -Be 10 } + + It "Can use PSWhere as an alias for the Where magic method" { + $x = 5 + $x.PSWhere{$true} | Should -Be 5 + } } }