@@ -174,11 +174,14 @@ internal static void ThrowError(ScriptBlockToPowerShellNotSupportedException ex,
174174
175175 internal class UsingExpressionAstSearcher : AstSearcher
176176 {
177- internal static IEnumerable < Ast > FindAllUsingExpressionExceptForWorkflow ( Ast ast )
177+ internal static IEnumerable < Ast > FindAllUsingExpressions ( Ast ast )
178178 {
179179 Diagnostics . Assert ( ast != null , "caller to verify arguments" ) ;
180180
181- var searcher = new UsingExpressionAstSearcher ( astParam => astParam is UsingExpressionAst , stopOnFirst : false , searchNestedScriptBlocks : true ) ;
181+ var searcher = new UsingExpressionAstSearcher (
182+ callback : astParam => astParam is UsingExpressionAst ,
183+ stopOnFirst : false ,
184+ searchNestedScriptBlocks : true ) ;
182185 ast . InternalVisit ( searcher ) ;
183186 return searcher . Results ;
184187 }
@@ -313,6 +316,145 @@ internal static PowerShell Convert(ScriptBlockAst body,
313316 }
314317 }
315318
319+ /// <summary>
320+ /// Get using values as dictionary for the Foreach-Object parallel cmdlet.
321+ /// Ignore any using expressions that are associated with inner nested Foreach-Object parallel calls,
322+ /// since they are only effective in the nested call scope and not the current outer scope.
323+ /// </summary>
324+ /// <param name = "scriptBlock">Scriptblock to search.</param>
325+ /// <param name = "isTrustedInput">True when input is trusted.</param>
326+ /// <param name = "context">Execution context.</param>
327+ /// <param name = "foreachNames">List of foreach command names and aliases.</param>
328+ /// <returns>Dictionary of using variable map.</returns>
329+ internal static Dictionary < string , object > GetUsingValuesForEachParallel (
330+ ScriptBlock scriptBlock ,
331+ bool isTrustedInput ,
332+ ExecutionContext context ,
333+ string [ ] foreachNames )
334+ {
335+ // Using variables for Foreach-Object -Parallel use are restricted to be within the
336+ // Foreach-Object -Parallel call scope. This will filter the using variable map to variables
337+ // only within the current (outer) Foreach-Object -Parallel call scope.
338+ var usingAsts = UsingExpressionAstSearcher . FindAllUsingExpressions ( scriptBlock . Ast ) . ToList ( ) ;
339+ UsingExpressionAst usingAst = null ;
340+ var usingValueMap = new Dictionary < string , object > ( usingAsts . Count ) ;
341+ Version oldStrictVersion = null ;
342+ try
343+ {
344+ if ( context != null )
345+ {
346+ oldStrictVersion = context . EngineSessionState . CurrentScope . StrictModeVersion ;
347+ context . EngineSessionState . CurrentScope . StrictModeVersion = PSVersionInfo . PSVersion ;
348+ }
349+
350+ for ( int i = 0 ; i < usingAsts . Count ; ++ i )
351+ {
352+ usingAst = ( UsingExpressionAst ) usingAsts [ i ] ;
353+ if ( IsInForeachParallelCallingScope ( usingAst , foreachNames ) )
354+ {
355+ var value = Compiler . GetExpressionValue ( usingAst . SubExpression , isTrustedInput , context ) ;
356+ string usingAstKey = PsUtils . GetUsingExpressionKey ( usingAst ) ;
357+ usingValueMap . TryAdd ( usingAstKey , value ) ;
358+ }
359+ }
360+ }
361+ catch ( RuntimeException rte )
362+ {
363+ if ( rte . ErrorRecord . FullyQualifiedErrorId . Equals ( "VariableIsUndefined" , StringComparison . Ordinal ) )
364+ {
365+ throw InterpreterError . NewInterpreterException (
366+ targetObject : null ,
367+ exceptionType : typeof ( RuntimeException ) ,
368+ errorPosition : usingAst . Extent ,
369+ resourceIdAndErrorId : "UsingVariableIsUndefined" ,
370+ resourceString : AutomationExceptions . UsingVariableIsUndefined ,
371+ args : rte . ErrorRecord . TargetObject ) ;
372+ }
373+ }
374+ finally
375+ {
376+ if ( context != null )
377+ {
378+ context . EngineSessionState . CurrentScope . StrictModeVersion = oldStrictVersion ;
379+ }
380+ }
381+
382+ return usingValueMap ;
383+ }
384+
385+ /// <summary>
386+ /// Walks the using Ast to verify it is used within a foreach-object -parallel command
387+ /// and parameter set scope, and not from within a nested foreach-object -parallel call.
388+ /// </summary>
389+ /// <param name="usingAst">Using Ast to check.</param>
390+ /// <param name-"foreachNames">List of foreach-object command names.</param>
391+ /// <returns>True if using expression is in current call scope.</returns>
392+ private static bool IsInForeachParallelCallingScope (
393+ UsingExpressionAst usingAst ,
394+ string [ ] foreachNames )
395+ {
396+ /*
397+ Example:
398+ $Test1 = "Hello"
399+ 1 | ForEach-Object -Parallel {
400+ $using:Test1
401+ $Test2 = "Goodbye"
402+ 1 | ForEach-Object -Parallel {
403+ $using:Test1 # Invalid using scope
404+ $using:Test2 # Valid using scope
405+ }
406+ }
407+ */
408+ Diagnostics . Assert ( usingAst != null , "usingAst argument cannot be null." ) ;
409+
410+ // Search up the parent Ast chain for 'Foreach-Object -Parallel' commands.
411+ Ast currentParent = usingAst . Parent ;
412+ int foreachNestedCount = 0 ;
413+ while ( currentParent != null )
414+ {
415+ // Look for Foreach-Object outer commands
416+ if ( currentParent is CommandAst commandAst )
417+ {
418+ foreach ( var commandElement in commandAst . CommandElements )
419+ {
420+ if ( commandElement is StringConstantExpressionAst commandName )
421+ {
422+ bool found = false ;
423+ foreach ( var foreachName in foreachNames )
424+ {
425+ if ( commandName . Value . Equals ( foreachName , StringComparison . OrdinalIgnoreCase ) )
426+ {
427+ found = true ;
428+ break ;
429+ }
430+ }
431+
432+ if ( found )
433+ {
434+ // Verify this is foreach-object with parallel parameter set.
435+ var bindingResult = StaticParameterBinder . BindCommand ( commandAst ) ;
436+ if ( bindingResult . BoundParameters . ContainsKey ( "Parallel" ) )
437+ {
438+ foreachNestedCount ++ ;
439+ break ;
440+ }
441+ }
442+ }
443+ }
444+ }
445+
446+ if ( foreachNestedCount > 1 )
447+ {
448+ // This using expression Ast is outside the original calling scope.
449+ return false ;
450+ }
451+
452+ currentParent = currentParent . Parent ;
453+ }
454+
455+ return foreachNestedCount == 1 ;
456+ }
457+
316458 /// <summary>
317459 /// Get using values in the dictionary form.
318460 /// </summary>
@@ -343,11 +485,16 @@ internal static object[] GetUsingValuesAsArray(ScriptBlock scriptBlock, bool isT
343485 /// A tuple of the dictionary-form and the array-form using values.
344486 /// If the array-form using value is null, then there are UsingExpressions used in different scopes.
345487 /// </returns>
346- private static Tuple < Dictionary < string , object > , object [ ] > GetUsingValues ( Ast body , bool isTrustedInput , ExecutionContext context , Dictionary < string , object > variables , bool filterNonUsingVariables )
488+ private static Tuple < Dictionary < string , object > , object [ ] > GetUsingValues (
489+ Ast body ,
490+ bool isTrustedInput ,
491+ ExecutionContext context ,
492+ Dictionary < string , object > variables ,
493+ bool filterNonUsingVariables )
347494 {
348495 Diagnostics . Assert ( context != null || variables != null , "can't retrieve variables with no context and no variables" ) ;
349496
350- var usingAsts = UsingExpressionAstSearcher . FindAllUsingExpressionExceptForWorkflow ( body ) . ToList ( ) ;
497+ var usingAsts = UsingExpressionAstSearcher . FindAllUsingExpressions ( body ) . ToList ( ) ;
351498 var usingValueArray = new object [ usingAsts . Count ] ;
352499 var usingValueMap = new Dictionary < string , object > ( usingAsts . Count ) ;
353500 HashSet < string > usingVarNames = ( variables != null && filterNonUsingVariables ) ? new HashSet < string > ( ) : null ;
@@ -456,7 +603,7 @@ private static Tuple<Dictionary<string, object>, object[]> GetUsingValues(Ast bo
456603 /// Check if the given UsingExpression is in a different scope from the previous UsingExpression that we analyzed.
457604 /// </summary>
458605 /// <remarks>
459- /// Note that the value of <paramref name="usingExpr"/> is retrieved by calling 'UsingExpressionAstSearcher.FindAllUsingExpressionExceptForWorkflow '.
606+ /// Note that the value of <paramref name="usingExpr"/> is retrieved by calling 'UsingExpressionAstSearcher.FindAllUsingExpressions '.
460607 /// So <paramref name="usingExpr"/> is guaranteed not inside a workflow.
461608 /// </remarks>
462609 /// <param name="usingExpr">The UsingExpression to analyze.</param>
0 commit comments