Skip to content

Commit c550c39

Browse files
Copilotbergmeister
andauthored
Fix AvoidAlias execution-order and positional handling
Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com>
1 parent 00f6322 commit c550c39

2 files changed

Lines changed: 84 additions & 5 deletions

File tree

‎Rules/AvoidAlias.cs‎

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -398,9 +398,63 @@ private bool IsFunctionDeclaredAfterCommand(
398398
ScriptBlockAst commandScope,
399399
FunctionAnalysisContext functionAnalysis)
400400
{
401-
return commandScope != null
402-
&& commandScope == GetFunctionScope(function, functionAnalysis)
403-
&& function.Extent.StartOffset > cmdAst.Extent.StartOffset;
401+
if (commandScope == null || commandScope != GetFunctionScope(function, functionAnalysis))
402+
{
403+
return false;
404+
}
405+
406+
int functionPhase = GetForEachObjectExecutionPhase(function, out CommandAst functionCallback);
407+
int commandPhase = GetForEachObjectExecutionPhase(cmdAst, out CommandAst commandCallback);
408+
if (functionCallback == commandCallback
409+
&& functionPhase >= 0
410+
&& commandPhase >= 0
411+
&& functionPhase != commandPhase)
412+
{
413+
return functionPhase > commandPhase;
414+
}
415+
416+
return function.Extent.StartOffset > cmdAst.Extent.StartOffset;
417+
}
418+
419+
private int GetForEachObjectExecutionPhase(Ast ast, out CommandAst callbackCommand)
420+
{
421+
callbackCommand = null;
422+
for (Ast current = ast; current != null; current = current.Parent)
423+
{
424+
if (!(current is ScriptBlockAst scope)
425+
|| !(scope.Parent is ScriptBlockExpressionAst scriptBlock))
426+
{
427+
continue;
428+
}
429+
430+
CommandAst commandAst = GetScriptBlockCommand(scriptBlock);
431+
if (commandAst == null
432+
|| !IsForEachObjectCommand(GetCallbackCommandName(commandAst)))
433+
{
434+
continue;
435+
}
436+
437+
string parameterName = GetScriptBlockParameterName(commandAst, scriptBlock);
438+
if (IsParameter(parameterName, "Begin"))
439+
{
440+
callbackCommand = commandAst;
441+
return 0;
442+
}
443+
444+
if (IsParameter(parameterName, "Process"))
445+
{
446+
callbackCommand = commandAst;
447+
return 1;
448+
}
449+
450+
if (IsParameter(parameterName, "End"))
451+
{
452+
callbackCommand = commandAst;
453+
return 2;
454+
}
455+
}
456+
457+
return -1;
404458
}
405459

406460
private FunctionAnalysisContext GetFunctionNameIndex(Ast ast)
@@ -1017,8 +1071,10 @@ private bool IsRemoteInvokeCommand(CommandAst commandAst)
10171071
CommandElementAst element = commandAst.CommandElements[i];
10181072
if (element is CommandParameterAst parameter)
10191073
{
1020-
if (parameter.Argument != null
1021-
|| (i + 1 < commandAst.CommandElements.Count
1074+
ParameterMetadata metadata = ResolveParameter(parameters, parameter.ParameterName);
1075+
if (parameter.Argument == null
1076+
&& metadata?.ParameterType != typeof(SwitchParameter)
1077+
&& (i + 1 < commandAst.CommandElements.Count
10221078
&& !(commandAst.CommandElements[i + 1] is CommandParameterAst)))
10231079
{
10241080
i++;

‎Tests/Rules/AvoidUsingAlias.tests.ps1‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,27 @@ gci -Path C:\
108108
$violations[0].SuggestedCorrections[0].Text | Should -Be 'Get-Verb'
109109
}
110110

111+
It "respects execution order across ForEach-Object callback phases" -TestCases @(
112+
@{
113+
Target = '1 | ForEach-Object -Process { Verb } -Begin { function Verb {} }'
114+
ExpectedViolations = 0
115+
ExpectedText = '1 | ForEach-Object -Process { Verb } -Begin { function Verb {} }'
116+
}
117+
@{
118+
Target = '1 | ForEach-Object -Begin { Verb } -Process { function Verb {} }'
119+
ExpectedViolations = 1
120+
ExpectedText = '1 | ForEach-Object -Begin { Get-Verb } -Process { function Verb {} }'
121+
}
122+
) {
123+
param($Target, $ExpectedViolations, $ExpectedText)
124+
125+
$violations = @(Invoke-ScriptAnalyzer -ScriptDefinition $Target -IncludeRule $violationName)
126+
127+
$violations.Count | Should -Be $ExpectedViolations
128+
$settings = @{ Rules = @{ PSAvoidUsingCmdletAliases = @{ Enable = $true } } }
129+
Invoke-Formatter -ScriptDefinition $Target -Settings $settings | Should -Be $ExpectedText
130+
}
131+
111132
It "does not apply caller scope to script-block arguments of dot-invoked commands" {
112133
$target = 'function private:Verb {}; . Invoke-Command -ScriptBlock { Verb }'
113134
$violations = @(Invoke-ScriptAnalyzer -ScriptDefinition $target -IncludeRule $violationName)
@@ -241,6 +262,8 @@ Script Test {
241262
@{ Boundary = 'Invoke-Command -ComputerName localhost -ScriptBlock { Verb }' }
242263
@{ Boundary = 'Invoke-Command -Session $session -ScriptBlock { Verb }' }
243264
@{ Boundary = 'Invoke-Command localhost { Verb }' }
265+
@{ Boundary = 'Invoke-Command -Verbose localhost -ScriptBlock { Verb }' }
266+
@{ Boundary = 'Invoke-Command -Verbose:$true localhost -ScriptBlock { Verb }' }
244267
@{ Boundary = 'Invoke-Command -ConnectionUri http://server:5985/wsman -ScriptBlock { Verb }' }
245268
@{ Boundary = 'InlineScript { Verb }' }
246269
@{ Boundary = 'Script Test { GetScript = { Verb } }' }

0 commit comments

Comments
 (0)