From 838b15029740977b353e65056d238caf7344a5b2 Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Tue, 5 Jan 2021 13:56:27 -0800 Subject: [PATCH 1/5] Fix using variable for nested foreach parallel calls --- .../engine/InternalCommands.cs | 9 +- .../remoting/commands/PSRemotingCmdlet.cs | 2 +- .../engine/runtime/ScriptBlockToPowerShell.cs | 139 +++++++++++++++++- .../Foreach-Object-Parallel.Tests.ps1 | 61 ++++++++ 4 files changed, 200 insertions(+), 11 deletions(-) diff --git a/src/System.Management.Automation/engine/InternalCommands.cs b/src/System.Management.Automation/engine/InternalCommands.cs index f02ce3de805..423f94c355f 100644 --- a/src/System.Management.Automation/engine/InternalCommands.cs +++ b/src/System.Management.Automation/engine/InternalCommands.cs @@ -408,11 +408,10 @@ private void InitParallelParameterSet() } bool allowUsingExpression = this.Context.SessionState.LanguageMode != PSLanguageMode.NoLanguage; - _usingValuesMap = ScriptBlockToPowerShellConverter.GetUsingValuesAsDictionary( - Parallel, - allowUsingExpression, - this.Context, - null); + _usingValuesMap = ScriptBlockToPowerShellConverter.GetUsingValuesForEachParallel( + scriptBlock: Parallel, + isTrustedInput: allowUsingExpression, + context: this.Context); // Validate using values map, which is a map of '$using:' variables referenced in the script. // Script block variables are not allowed since their behavior is undefined outside the runspace diff --git a/src/System.Management.Automation/engine/remoting/commands/PSRemotingCmdlet.cs b/src/System.Management.Automation/engine/remoting/commands/PSRemotingCmdlet.cs index bbb2056189c..3752e4dcfd3 100644 --- a/src/System.Management.Automation/engine/remoting/commands/PSRemotingCmdlet.cs +++ b/src/System.Management.Automation/engine/remoting/commands/PSRemotingCmdlet.cs @@ -2434,7 +2434,7 @@ private static List GetUsingVariables(ScriptBlock localSc throw new ArgumentNullException(nameof(localScriptBlock), "Caller needs to make sure the parameter value is not null"); } - var allUsingExprs = UsingExpressionAstSearcher.FindAllUsingExpressionExceptForWorkflow(localScriptBlock.Ast); + var allUsingExprs = UsingExpressionAstSearcher.FindAllUsingExpressions(localScriptBlock.Ast); return allUsingExprs.Select(usingExpr => UsingExpressionAst.ExtractUsingVariable((UsingExpressionAst)usingExpr)).ToList(); } diff --git a/src/System.Management.Automation/engine/runtime/ScriptBlockToPowerShell.cs b/src/System.Management.Automation/engine/runtime/ScriptBlockToPowerShell.cs index f5d2f51f698..abad4910f27 100644 --- a/src/System.Management.Automation/engine/runtime/ScriptBlockToPowerShell.cs +++ b/src/System.Management.Automation/engine/runtime/ScriptBlockToPowerShell.cs @@ -174,11 +174,14 @@ internal static void ThrowError(ScriptBlockToPowerShellNotSupportedException ex, internal class UsingExpressionAstSearcher : AstSearcher { - internal static IEnumerable FindAllUsingExpressionExceptForWorkflow(Ast ast) + internal static IEnumerable FindAllUsingExpressions(Ast ast) { Diagnostics.Assert(ast != null, "caller to verify arguments"); - var searcher = new UsingExpressionAstSearcher(astParam => astParam is UsingExpressionAst, stopOnFirst: false, searchNestedScriptBlocks: true); + var searcher = new UsingExpressionAstSearcher( + callback: astParam => astParam is UsingExpressionAst, + stopOnFirst: false, + searchNestedScriptBlocks: true); ast.InternalVisit(searcher); return searcher.Results; } @@ -313,6 +316,127 @@ internal static PowerShell Convert(ScriptBlockAst body, } } + /// + /// Get using values as dictionary for the Foreach-Object parallel cmdlet. + /// Ignore any using expressions that are associated with inner nested Foreach-Object parallel calls, + /// since they are only effective in the nested call scope and not the current outer scope. + /// + /// Scriptblock to search. + /// True when input is trusted. + /// Execution context. + /// Dictionary of using variable map. + internal static Dictionary GetUsingValuesForEachParallel( + ScriptBlock scriptBlock, + bool isTrustedInput, + ExecutionContext context) + { + // Using variables for Foreach-Object -Parallel use are restricted to be within the + // Foreach-Object -Parallel call scope. This will filter the using variable map to variables + // only within the current (outer) Foreach-Object -Parallel call scope. + var usingAsts = UsingExpressionAstSearcher.FindAllUsingExpressions(scriptBlock.Ast).ToList(); + UsingExpressionAst usingAst = null; + var usingValueMap = new Dictionary(usingAsts.Count); + Version oldStrictVersion = null; + try + { + if (context != null) + { + oldStrictVersion = context.EngineSessionState.CurrentScope.StrictModeVersion; + context.EngineSessionState.CurrentScope.StrictModeVersion = PSVersionInfo.PSVersion; + } + + for (int i = 0; i < usingAsts.Count; ++i) + { + usingAst = (UsingExpressionAst)usingAsts[i]; + if (IsInForeachParallelCallingScope(usingAst)) + { + var value = Compiler.GetExpressionValue(usingAst.SubExpression, isTrustedInput, context); + string usingAstKey = PsUtils.GetUsingExpressionKey(usingAst); + usingValueMap.TryAdd(usingAstKey, value); + } + } + } + catch (RuntimeException rte) + { + if (rte.ErrorRecord.FullyQualifiedErrorId.Equals("VariableIsUndefined", StringComparison.Ordinal)) + { + throw InterpreterError.NewInterpreterException( + targetObject: null, + exceptionType: typeof(RuntimeException), + errorPosition: usingAst.Extent, + resourceIdAndErrorId: "UsingVariableIsUndefined", + resourceString: AutomationExceptions.UsingVariableIsUndefined, + args: rte.ErrorRecord.TargetObject); + } + } + finally + { + if (context != null) + { + context.EngineSessionState.CurrentScope.StrictModeVersion = oldStrictVersion; + } + } + + return usingValueMap; + } + + /// + /// Walks the using Ast to verify it is used within a foreach-object -parallel command + /// and parameter set scope, and not from within a nested foreach-object -parallel call. + /// $Test1 = "Hello" + /// 1 | ForEach-Object -Parallel { + /// $using:Test1 + /// $Test2 = "Goodbye" + /// 1 | ForEach-Object -Parallel { + /// $using:Test1 # Invalid using scope + /// $using:Test2 # Valid using scope + /// } + /// } + /// + private static bool IsInForeachParallelCallingScope(UsingExpressionAst usingAst) + { + Diagnostics.Assert(usingAst != null, "usingAst argument cannot be null."); + + // Search up the parent Ast chain for 'Foreach-Object -Parallel' commands. + Ast currentParent = usingAst.Parent; + int foreachNestedCount = 0; + while (currentParent != null) + { + // Look for Foreach-Object outer commands + if (currentParent is CommandAst commandAst) + { + foreach (var commandElement in commandAst.CommandElements) + { + if (commandElement is StringConstantExpressionAst commandName) + { + if (commandName.Value.Equals("foreach", StringComparison.OrdinalIgnoreCase) || + commandName.Value.Equals("foreach-object", StringComparison.OrdinalIgnoreCase) || + commandName.Value.Equals("%")) + { + // Verify this is foreach-object with parallel parameter set. + var bindingResult = StaticParameterBinder.BindCommand(commandAst); + if (bindingResult.BoundParameters.ContainsKey("Parallel")) + { + foreachNestedCount++; + break; + } + } + } + } + } + + if (foreachNestedCount > 1) + { + // This using expression Ast is outside the original calling scope. + return false; + } + + currentParent = currentParent.Parent; + } + + return (foreachNestedCount == 1); + } + /// /// Get using values in the dictionary form. /// @@ -343,11 +467,16 @@ internal static object[] GetUsingValuesAsArray(ScriptBlock scriptBlock, bool isT /// A tuple of the dictionary-form and the array-form using values. /// If the array-form using value is null, then there are UsingExpressions used in different scopes. /// - private static Tuple, object[]> GetUsingValues(Ast body, bool isTrustedInput, ExecutionContext context, Dictionary variables, bool filterNonUsingVariables) + private static Tuple, object[]> GetUsingValues( + Ast body, + bool isTrustedInput, + ExecutionContext context, + Dictionary variables, + bool filterNonUsingVariables) { Diagnostics.Assert(context != null || variables != null, "can't retrieve variables with no context and no variables"); - var usingAsts = UsingExpressionAstSearcher.FindAllUsingExpressionExceptForWorkflow(body).ToList(); + var usingAsts = UsingExpressionAstSearcher.FindAllUsingExpressions(body).ToList(); var usingValueArray = new object[usingAsts.Count]; var usingValueMap = new Dictionary(usingAsts.Count); HashSet usingVarNames = (variables != null && filterNonUsingVariables) ? new HashSet() : null; @@ -456,7 +585,7 @@ private static Tuple, object[]> GetUsingValues(Ast bo /// Check if the given UsingExpression is in a different scope from the previous UsingExpression that we analyzed. /// /// - /// Note that the value of is retrieved by calling 'UsingExpressionAstSearcher.FindAllUsingExpressionExceptForWorkflow'. + /// Note that the value of is retrieved by calling 'UsingExpressionAstSearcher.FindAllUsingExpressions'. /// So is guaranteed not inside a workflow. /// /// The UsingExpression to analyze. diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Foreach-Object-Parallel.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Foreach-Object-Parallel.Tests.ps1 index 484454e3a85..e8d420f93fa 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Foreach-Object-Parallel.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Foreach-Object-Parallel.Tests.ps1 @@ -26,6 +26,67 @@ Describe 'ForEach-Object -Parallel Basic Tests' -Tags 'CI' { $result[1] | Should -BeExactly $varArray[1] } + It 'Verifies in scope using variables in nested calls' { + + $Test = "Test1" + $results = 1..2 | ForEach-Object -Parallel { + $using:Test + $Test = "Test2" + 1..2 | ForEach-Object -Parallel { + $using:Test + $Test = "Test3" + 1..2 | ForEach-Object -Parallel { + $using:Test + } + } + } + $results.Count | Should -BeExactly 14 + $groups = $results | Group-Object -AsHashTable + $groups['Test1'].Count | Should -BeExactly 2 + $groups['Test2'].Count | Should -BeExactly 4 + $groups['Test3'].Count | Should -BeExactly 8 + } + + It 'Verifies in scope using variables with different names in nested calls' { + $Test1 = "TestA" + $results = 1..2 | ForEach-Object -parallel { + $using:Test1 + $Test2 = "TestB" + 1..2 | ForEach-Object -parallel { + $using:Test2 + } + } + $results.Count | Should -BeExactly 6 + $groups = $results | Group-Object -AsHashTable + $groups['TestA'].Count | Should -BeExactly 2 + $groups['TestB'].Count | Should -BeExactly 4 + } + + It 'Verifies using variable in nested scriptblock' { + + $test = 'testC' + $results = 1..2 | ForEach-Object -parallel { + & { $using:test } + } + $results.Count | Should -BeExactly 2 + $groups = $results | Group-Object -AsHashTable + $groups['TestC'].Count | Should -BeExactly 2 + } + + It 'Verifies expected error for out of scope using variable in nested calls' { + + $Test = "TestZ" + 1..1 | ForEach-Object -Parallel { + $using:Test + # Variable '$Test' is not defined in this scope. + 1..1 | ForEach-Object -Parallel { + $using:Test + } + } -ErrorVariable usingErrors 2>$null + + $usingErrors[0].FullyQualifiedErrorId | Should -BeExactly 'UsingVariableIsUndefined,Microsoft.PowerShell.Commands.ForEachObjectCommand' + } + It 'Verifies terminating error streaming' { $result = 1..1 | ForEach-Object -Parallel { throw 'Terminating Error!'; "Hello" } 2>&1 From 071cdd386815ef07b3ea1bf2c7f3ab69236ed0ff Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Tue, 5 Jan 2021 15:01:14 -0800 Subject: [PATCH 2/5] Fix some codacy issues --- .../engine/runtime/ScriptBlockToPowerShell.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/System.Management.Automation/engine/runtime/ScriptBlockToPowerShell.cs b/src/System.Management.Automation/engine/runtime/ScriptBlockToPowerShell.cs index abad4910f27..3017c49a49a 100644 --- a/src/System.Management.Automation/engine/runtime/ScriptBlockToPowerShell.cs +++ b/src/System.Management.Automation/engine/runtime/ScriptBlockToPowerShell.cs @@ -393,6 +393,8 @@ internal static Dictionary GetUsingValuesForEachParallel( /// } /// } /// + /// Using Ast to check. + /// True if using expression is in current call scope. private static bool IsInForeachParallelCallingScope(UsingExpressionAst usingAst) { Diagnostics.Assert(usingAst != null, "usingAst argument cannot be null."); @@ -434,7 +436,7 @@ private static bool IsInForeachParallelCallingScope(UsingExpressionAst usingAst) currentParent = currentParent.Parent; } - return (foreachNestedCount == 1); + return foreachNestedCount == 1; } /// From 1e465c893364263ee0a55abc24d41dbd14de872a Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Wed, 13 Jan 2021 10:27:50 -0800 Subject: [PATCH 3/5] Add comment about expanding alias list in the future --- .../engine/InternalCommands.cs | 15 ++++++++++-- .../engine/runtime/ScriptBlockToPowerShell.cs | 24 ++++++++++++++----- .../Foreach-Object-Parallel.Tests.ps1 | 10 ++++---- 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/System.Management.Automation/engine/InternalCommands.cs b/src/System.Management.Automation/engine/InternalCommands.cs index 423f94c355f..a5f82dfd8c6 100644 --- a/src/System.Management.Automation/engine/InternalCommands.cs +++ b/src/System.Management.Automation/engine/InternalCommands.cs @@ -381,6 +381,16 @@ public void Dispose() private Exception _taskCollectionException; private string _currentLocationPath; + // List of Foreach-Object command names and aliases. + // TODO: Look into using SessionState.Internal.GetAliasTable() to find all user created aliases. + // But update Alias command logic to maintain reverse table that lists all aliases mapping + // to a single command definition, for performance. + private static string[] ForEachNames = new string[] { + "ForEach-Object", + "foreach", + "%" + }; + private void InitParallelParameterSet() { // The following common parameters are not (yet) supported in this parameter set. @@ -407,11 +417,12 @@ private void InitParallelParameterSet() { } - bool allowUsingExpression = this.Context.SessionState.LanguageMode != PSLanguageMode.NoLanguage; + var allowUsingExpression = this.Context.SessionState.LanguageMode != PSLanguageMode.NoLanguage; _usingValuesMap = ScriptBlockToPowerShellConverter.GetUsingValuesForEachParallel( scriptBlock: Parallel, isTrustedInput: allowUsingExpression, - context: this.Context); + context: this.Context, + foreachNames: ForEachNames); // Validate using values map, which is a map of '$using:' variables referenced in the script. // Script block variables are not allowed since their behavior is undefined outside the runspace diff --git a/src/System.Management.Automation/engine/runtime/ScriptBlockToPowerShell.cs b/src/System.Management.Automation/engine/runtime/ScriptBlockToPowerShell.cs index 3017c49a49a..d205511f4d4 100644 --- a/src/System.Management.Automation/engine/runtime/ScriptBlockToPowerShell.cs +++ b/src/System.Management.Automation/engine/runtime/ScriptBlockToPowerShell.cs @@ -324,11 +324,13 @@ internal static PowerShell Convert(ScriptBlockAst body, /// Scriptblock to search. /// True when input is trusted. /// Execution context. + /// List of foreach command names and aliases /// Dictionary of using variable map. internal static Dictionary GetUsingValuesForEachParallel( ScriptBlock scriptBlock, bool isTrustedInput, - ExecutionContext context) + ExecutionContext context, + string[] foreachNames) { // Using variables for Foreach-Object -Parallel use are restricted to be within the // Foreach-Object -Parallel call scope. This will filter the using variable map to variables @@ -348,7 +350,7 @@ internal static Dictionary GetUsingValuesForEachParallel( for (int i = 0; i < usingAsts.Count; ++i) { usingAst = (UsingExpressionAst)usingAsts[i]; - if (IsInForeachParallelCallingScope(usingAst)) + if (IsInForeachParallelCallingScope(usingAst, foreachNames)) { var value = Compiler.GetExpressionValue(usingAst.SubExpression, isTrustedInput, context); string usingAstKey = PsUtils.GetUsingExpressionKey(usingAst); @@ -394,8 +396,11 @@ internal static Dictionary GetUsingValuesForEachParallel( /// } /// /// Using Ast to check. + /// List of foreach-object command names. /// True if using expression is in current call scope. - private static bool IsInForeachParallelCallingScope(UsingExpressionAst usingAst) + private static bool IsInForeachParallelCallingScope( + UsingExpressionAst usingAst, + string[] foreachNames) { Diagnostics.Assert(usingAst != null, "usingAst argument cannot be null."); @@ -411,9 +416,16 @@ private static bool IsInForeachParallelCallingScope(UsingExpressionAst usingAst) { if (commandElement is StringConstantExpressionAst commandName) { - if (commandName.Value.Equals("foreach", StringComparison.OrdinalIgnoreCase) || - commandName.Value.Equals("foreach-object", StringComparison.OrdinalIgnoreCase) || - commandName.Value.Equals("%")) + bool found = false; + foreach (var foreachName in foreachNames) + { + if (commandName.Value.Equals(foreachName, StringComparison.OrdinalIgnoreCase)) + { + found = true; + break; + } + } + if (found) { // Verify this is foreach-object with parallel parameter set. var bindingResult = StaticParameterBinder.BindCommand(commandAst); diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Foreach-Object-Parallel.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Foreach-Object-Parallel.Tests.ps1 index e8d420f93fa..46f85b80bd4 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Foreach-Object-Parallel.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Foreach-Object-Parallel.Tests.ps1 @@ -50,11 +50,11 @@ Describe 'ForEach-Object -Parallel Basic Tests' -Tags 'CI' { It 'Verifies in scope using variables with different names in nested calls' { $Test1 = "TestA" $results = 1..2 | ForEach-Object -parallel { - $using:Test1 - $Test2 = "TestB" - 1..2 | ForEach-Object -parallel { - $using:Test2 - } + $using:Test1 + $Test2 = "TestB" + 1..2 | ForEach-Object -parallel { + $using:Test2 + } } $results.Count | Should -BeExactly 6 $groups = $results | Group-Object -AsHashTable From 1d3d2d5454858e70b9b222669280c424512fa5c6 Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Thu, 28 Jan 2021 08:34:30 -0800 Subject: [PATCH 4/5] Address CodFactor warnings --- .../engine/InternalCommands.cs | 5 ++-- .../engine/runtime/ScriptBlockToPowerShell.cs | 23 +++++++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/System.Management.Automation/engine/InternalCommands.cs b/src/System.Management.Automation/engine/InternalCommands.cs index a5f82dfd8c6..b7b1d87cc82 100644 --- a/src/System.Management.Automation/engine/InternalCommands.cs +++ b/src/System.Management.Automation/engine/InternalCommands.cs @@ -385,7 +385,8 @@ public void Dispose() // TODO: Look into using SessionState.Internal.GetAliasTable() to find all user created aliases. // But update Alias command logic to maintain reverse table that lists all aliases mapping // to a single command definition, for performance. - private static string[] ForEachNames = new string[] { + private static string[] forEachNames = new string[] + { "ForEach-Object", "foreach", "%" @@ -422,7 +423,7 @@ private void InitParallelParameterSet() scriptBlock: Parallel, isTrustedInput: allowUsingExpression, context: this.Context, - foreachNames: ForEachNames); + foreachNames: forEachNames); // Validate using values map, which is a map of '$using:' variables referenced in the script. // Script block variables are not allowed since their behavior is undefined outside the runspace diff --git a/src/System.Management.Automation/engine/runtime/ScriptBlockToPowerShell.cs b/src/System.Management.Automation/engine/runtime/ScriptBlockToPowerShell.cs index d205511f4d4..4531eacb07c 100644 --- a/src/System.Management.Automation/engine/runtime/ScriptBlockToPowerShell.cs +++ b/src/System.Management.Automation/engine/runtime/ScriptBlockToPowerShell.cs @@ -324,7 +324,7 @@ internal static PowerShell Convert(ScriptBlockAst body, /// Scriptblock to search. /// True when input is trusted. /// Execution context. - /// List of foreach command names and aliases + /// List of foreach command names and aliases. /// Dictionary of using variable map. internal static Dictionary GetUsingValuesForEachParallel( ScriptBlock scriptBlock, @@ -385,15 +385,6 @@ internal static Dictionary GetUsingValuesForEachParallel( /// /// Walks the using Ast to verify it is used within a foreach-object -parallel command /// and parameter set scope, and not from within a nested foreach-object -parallel call. - /// $Test1 = "Hello" - /// 1 | ForEach-Object -Parallel { - /// $using:Test1 - /// $Test2 = "Goodbye" - /// 1 | ForEach-Object -Parallel { - /// $using:Test1 # Invalid using scope - /// $using:Test2 # Valid using scope - /// } - /// } /// /// Using Ast to check. /// List of foreach-object command names. @@ -402,6 +393,17 @@ private static bool IsInForeachParallelCallingScope( UsingExpressionAst usingAst, string[] foreachNames) { + // Example: + // $Test1 = "Hello" + // 1 | ForEach-Object -Parallel { + // $using:Test1 + // $Test2 = "Goodbye" + // 1 | ForEach-Object -Parallel { + // $using:Test1 # Invalid using scope + // $using:Test2 # Valid using scope + // } + // } + Diagnostics.Assert(usingAst != null, "usingAst argument cannot be null."); // Search up the parent Ast chain for 'Foreach-Object -Parallel' commands. @@ -425,6 +427,7 @@ private static bool IsInForeachParallelCallingScope( break; } } + if (found) { // Verify this is foreach-object with parallel parameter set. From 14476e41c3bfad1cc7ba3c53f581928ed213953d Mon Sep 17 00:00:00 2001 From: Paul Higinbotham Date: Thu, 28 Jan 2021 11:14:03 -0800 Subject: [PATCH 5/5] more nonsense --- .../engine/runtime/ScriptBlockToPowerShell.cs | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/System.Management.Automation/engine/runtime/ScriptBlockToPowerShell.cs b/src/System.Management.Automation/engine/runtime/ScriptBlockToPowerShell.cs index 4531eacb07c..93394be6808 100644 --- a/src/System.Management.Automation/engine/runtime/ScriptBlockToPowerShell.cs +++ b/src/System.Management.Automation/engine/runtime/ScriptBlockToPowerShell.cs @@ -393,17 +393,18 @@ private static bool IsInForeachParallelCallingScope( UsingExpressionAst usingAst, string[] foreachNames) { - // Example: - // $Test1 = "Hello" - // 1 | ForEach-Object -Parallel { - // $using:Test1 - // $Test2 = "Goodbye" - // 1 | ForEach-Object -Parallel { - // $using:Test1 # Invalid using scope - // $using:Test2 # Valid using scope - // } - // } - + /* + Example: + $Test1 = "Hello" + 1 | ForEach-Object -Parallel { + $using:Test1 + $Test2 = "Goodbye" + 1 | ForEach-Object -Parallel { + $using:Test1 # Invalid using scope + $using:Test2 # Valid using scope + } + } + */ Diagnostics.Assert(usingAst != null, "usingAst argument cannot be null."); // Search up the parent Ast chain for 'Foreach-Object -Parallel' commands.