Skip to content

Commit 784620a

Browse files
bergmeisterCopilot
andcommitted
Distinguish script-defined functions from DSC resources
Resolve visible function declarations from the AST when classifying DSC resource syntax and checking implicit Get-prefix aliases. Preserve analysis of executable function arguments for both brace layouts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 9914f20 commit 784620a

3 files changed

Lines changed: 110 additions & 9 deletions

File tree

‎Rules/AvoidAlias.cs‎

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
140140
}
141141

142142
// If we find match of any kind, do not continue with the Get-{commandname} check
143-
if ( Helper.Instance.GetCommandInfo(commandName) != null ) {
143+
if (Helper.Instance.GetCommandInfo(commandName) != null || IsScriptDefinedFunction(cmdAst)) {
144144
continue;
145145
}
146146

@@ -214,7 +214,8 @@ private bool IsDscResourceDeclaration(CommandAst cmdAst)
214214
}
215215
}
216216

217-
if (!hasConfigurationAncestor || GetDscResourceBody(cmdAst) == null)
217+
if (!hasConfigurationAncestor || GetDscResourceBody(cmdAst) == null
218+
|| !IsDscResourceCommand(cmdAst))
218219
{
219220
return false;
220221
}
@@ -249,9 +250,7 @@ private bool IsDscResourceDeclaration(CommandAst cmdAst)
249250
if (scriptBlock.Parent is CommandAst enclosingCommand)
250251
{
251252
if (GetDscResourceBody(enclosingCommand) != scriptBlock
252-
|| !String.IsNullOrEmpty(Helper.Instance.GetCmdletNameFromAlias(enclosingCommand.GetCommandName()))
253-
|| (!String.Equals(enclosingCommand.GetCommandName(), "Node", StringComparison.OrdinalIgnoreCase)
254-
&& Helper.Instance.GetCommandInfo(enclosingCommand.GetCommandName()) != null))
253+
|| !IsDscResourceCommand(enclosingCommand))
255254
{
256255
return false;
257256
}
@@ -297,11 +296,35 @@ private bool IsFollowingLineDscResourceBody(ScriptBlockExpressionAst body)
297296
return false;
298297
}
299298

300-
string declarationCommandName = declaration.GetCommandName();
301299
return GetDscResourceBody(declaration) == body
302-
&& String.IsNullOrEmpty(Helper.Instance.GetCmdletNameFromAlias(declarationCommandName))
303-
&& (String.Equals(declarationCommandName, "Node", StringComparison.OrdinalIgnoreCase)
304-
|| Helper.Instance.GetCommandInfo(declarationCommandName) == null);
300+
&& IsDscResourceCommand(declaration);
301+
}
302+
303+
return false;
304+
}
305+
306+
private bool IsDscResourceCommand(CommandAst cmdAst)
307+
{
308+
string commandName = cmdAst.GetCommandName();
309+
return !IsScriptDefinedFunction(cmdAst)
310+
&& String.IsNullOrEmpty(Helper.Instance.GetCmdletNameFromAlias(commandName))
311+
&& (String.Equals(commandName, "Node", StringComparison.OrdinalIgnoreCase)
312+
|| Helper.Instance.GetCommandInfo(commandName) == null);
313+
}
314+
315+
private bool IsScriptDefinedFunction(CommandAst cmdAst)
316+
{
317+
string commandName = cmdAst.GetCommandName();
318+
// Command discovery runs in a separate runspace and cannot see script-local functions.
319+
for (Ast parent = cmdAst.Parent; parent != null; parent = parent.Parent)
320+
{
321+
if (parent is ScriptBlockAst scope
322+
&& scope.Find(candidate => candidate is FunctionDefinitionAst function
323+
&& String.Equals(function.Name, commandName, StringComparison.OrdinalIgnoreCase),
324+
searchNestedScriptBlocks: false) != null)
325+
{
326+
return true;
327+
}
305328
}
306329

307330
return false;

‎Tests/Rules/AvoidUsingAlias.tests.ps1‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,17 @@ gci -Path C:\
6363
$noViolations.Count | Should -Be 0
6464
}
6565

66+
It "does not add the Get prefix to script-defined functions or skip their script block arguments" {
67+
$target = 'function vErB {}; Verb Instance { ChildItem . {} }'
68+
$violations = @(Invoke-ScriptAnalyzer -ScriptDefinition $target -IncludeRule $violationName)
69+
70+
$violations.Count | Should -Be 1
71+
$violations[0].Extent.Text | Should -Be 'ChildItem'
72+
$settings = @{ Rules = @{ PSAvoidUsingCmdletAliases = @{ Enable = $true } } }
73+
Invoke-Formatter -ScriptDefinition $target -Settings $settings |
74+
Should -Be ($target.Replace('ChildItem', 'Get-ChildItem'))
75+
}
76+
6677
It "should return no violation for assignment statement-like command in dsc configuration" -skip:($IsLinux -or $IsMacOS) {
6778
$target = @'
6879
Configuration MyDscConfiguration {
@@ -97,6 +108,69 @@ Configuration PackageTest {
97108
}
98109

99110
Context "DSC resource ASTs" {
111+
It "recognizes functions declared in the <Scope> scope as executable commands" -TestCases @(
112+
@{ Scope = 'script' }
113+
@{ Scope = 'configuration' }
114+
) {
115+
param($Scope)
116+
117+
$scriptFunction = if ($Scope -eq 'script') { 'function vErB {}' }
118+
$configurationFunction = if ($Scope -eq 'configuration') { 'function vErB {}' }
119+
$target = @"
120+
$scriptFunction
121+
{
122+
$configurationFunction
123+
Verb SameLine { ChildItem . {} }
124+
Verb FollowingLine
125+
{
126+
ChildItem . {}
127+
}
128+
Package PackageExample {}
129+
}
130+
"@
131+
$ast = [System.Management.Automation.Language.Parser]::ParseInput($target, [ref]$null, [ref]$null)
132+
$body = $ast.EndBlock.Statements[-1].PipelineElements[0].Expression.Copy()
133+
$configuration = [System.Management.Automation.Language.ConfigurationDefinitionAst]::new(
134+
$ast.Extent, $body, 'Resource',
135+
[System.Management.Automation.Language.StringConstantExpressionAst]::new($ast.Extent, 'Test', 'BareWord'))
136+
[System.Management.Automation.Language.StatementAst[]]$statements = if ($Scope -eq 'script') {
137+
@($ast.EndBlock.Statements[0].Copy(), $configuration)
138+
} else {
139+
@($configuration)
140+
}
141+
$scriptAst = [System.Management.Automation.Language.ScriptBlockAst]::new(
142+
$ast.Extent, $null,
143+
[System.Management.Automation.Language.StatementBlockAst]::new($ast.Extent, $statements, $null), $false)
144+
$rule = [Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules.AvoidAlias]::new()
145+
146+
$violations = @($rule.AnalyzeScript($scriptAst, $null))
147+
148+
$violations.Count | Should -Be 2
149+
$violations | ForEach-Object {
150+
$_.Extent.Text | Should -Be 'ChildItem'
151+
$_.SuggestedCorrections[0].Text | Should -Be 'Get-ChildItem'
152+
}
153+
}
154+
155+
It "does not treat functions declared in unrelated scopes as executable commands" {
156+
$target = @'
157+
{
158+
function Get-Example {
159+
function Verb {}
160+
}
161+
Verb ResourceExample { ChildItem NestedResource {} }
162+
}
163+
'@
164+
$ast = [System.Management.Automation.Language.Parser]::ParseInput($target, [ref]$null, [ref]$null)
165+
$body = $ast.EndBlock.Statements[0].PipelineElements[0].Expression.Copy()
166+
$configuration = [System.Management.Automation.Language.ConfigurationDefinitionAst]::new(
167+
$ast.Extent, $body, 'Resource',
168+
[System.Management.Automation.Language.StringConstantExpressionAst]::new($ast.Extent, 'Test', 'BareWord'))
169+
$rule = [Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules.AvoidAlias]::new()
170+
171+
@($rule.AnalyzeScript($configuration, $null)).Count | Should -Be 0
172+
}
173+
100174
It "does not expand nested resource names in a same-line Node declaration when node is discoverable" -skip:(!(Get-Command node -CommandType Application -ErrorAction Ignore)) {
101175
$target = @'
102176
{

‎docs/Rules/AvoidUsingCmdletAliases.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ name, doing so reduces code readability and maintainability.
2020
PowerShell also supports implicit aliases. When a cmdlet name isn't found, PowerShell appends
2121
`Get-` to the command as a fallback. For example, typing `verb` executes `Get-Verb`.
2222

23+
Functions declared in the script are not implicit aliases. In DSC configurations, resource
24+
declarations are also excluded from implicit alias correction, while executable script blocks
25+
passed to script-defined functions are still analyzed.
26+
2327
Aliases create inconsistency across codebases because different authors learn and use different
2428
aliases. Aliases can potentially make code difficult to read and understand in collaborative
2529
environments. It can also cause issues with script portability and availability.

0 commit comments

Comments
 (0)