From cafb813c8aac531f2c9fa6c43906651ab51c21d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:40:58 +0000 Subject: [PATCH 01/14] Initial plan From be98895b08ce6c6b8fafdc69e8eac6cb3aa98e0d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:52:07 +0000 Subject: [PATCH 02/14] Avoid implicit Get-prefix corrections for DSC resource declarations Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --- Rules/AvoidAlias.cs | 81 +++++++++++++++++++++++++++ Tests/Rules/AvoidUsingAlias.tests.ps1 | 76 +++++++++++++++++++++++++ 2 files changed, 157 insertions(+) diff --git a/Rules/AvoidAlias.cs b/Rules/AvoidAlias.cs index 9efe0e916..67011ece0 100644 --- a/Rules/AvoidAlias.cs +++ b/Rules/AvoidAlias.cs @@ -138,6 +138,11 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) continue; } + if (IsDscResourceDeclaration(cmdAst)) + { + continue; + } + var commdNameWithGetPrefix = $"Get-{commandName}"; var cmdletNameIfCommandWasMissingGetPrefix = Helper.Instance.GetCommandInfo(name: commdNameWithGetPrefix, commandType: CommandTypes.Cmdlet | CommandTypes.Function | CommandTypes.Script); @@ -186,6 +191,82 @@ private bool IgnoreCommandast(CommandAst cmdAst) return false; } + private bool IsDscResourceDeclaration(CommandAst cmdAst) + { + if (GetDscResourceBody(cmdAst) == null) + { + return false; + } + + for (Ast parent = cmdAst.Parent; parent != null; parent = parent.Parent) + { + if (parent is ConfigurationDefinitionAst) + { + return true; + } + + if (parent is FunctionDefinitionAst || parent is HashtableAst) + { + return false; + } + + // Script resource properties contain executable code, not resource declarations. + if (parent is ScriptBlockExpressionAst scriptBlock) + { + if (scriptBlock.Parent is CommandAst enclosingCommand) + { + if (GetDscResourceBody(enclosingCommand) != scriptBlock + || !String.IsNullOrEmpty(Helper.Instance.GetCmdletNameFromAlias(enclosingCommand.GetCommandName())) + || Helper.Instance.GetCommandInfo(enclosingCommand.GetCommandName()) != null) + { + return false; + } + } + else if (!(scriptBlock.Parent is ConfigurationDefinitionAst) + && !(scriptBlock.Parent is DynamicKeywordStatementAst) + && !(scriptBlock.Parent is CommandExpressionAst)) + { + return false; + } + } + } + + return false; + } + + private ScriptBlockExpressionAst GetDscResourceBody(CommandAst cmdAst) + { + if (cmdAst.InvocationOperator != TokenKind.Unknown + || cmdAst.CommandElements.Count < 2 + || !(cmdAst.CommandElements[1] is ExpressionAst) + || cmdAst.CommandElements[1].Extent.Text == "=") + { + return null; + } + + if (cmdAst.CommandElements.Count == 3) + { + return cmdAst.CommandElements[2] as ScriptBlockExpressionAst; + } + + // A resource's opening brace can be on the following line. + if (cmdAst.CommandElements.Count == 2 && cmdAst.Parent is PipelineAst pipeline) + { + var statements = (pipeline.Parent as NamedBlockAst)?.Statements + ?? (pipeline.Parent as StatementBlockAst)?.Statements; + int index = statements == null ? -1 : statements.IndexOf(pipeline); + if (index >= 0 && index + 1 < statements.Count + && statements[index + 1] is PipelineAst bodyPipeline + && bodyPipeline.PipelineElements.Count == 1 + && bodyPipeline.PipelineElements[0] is CommandExpressionAst bodyExpression) + { + return bodyExpression.Expression as ScriptBlockExpressionAst; + } + } + + return null; + } + /// /// For a command like "gci -path c:", returns the extent of "gci" in the command /// diff --git a/Tests/Rules/AvoidUsingAlias.tests.ps1 b/Tests/Rules/AvoidUsingAlias.tests.ps1 index 00db269a0..96e31df5b 100644 --- a/Tests/Rules/AvoidUsingAlias.tests.ps1 +++ b/Tests/Rules/AvoidUsingAlias.tests.ps1 @@ -33,6 +33,19 @@ Describe "AvoidUsingAlias" { $violations = Invoke-ScriptAnalyzer -ScriptDefinition 'verb' | Where-Object { $_.RuleName -eq $violationName } $violations.Count | Should -Be 1 } + + It "still expands aliases with resource-shaped arguments" { + $target = 'icm Server01 { Get-Process }' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $target -IncludeRule $violationName + $violations.Count | Should -Be 1 + $violations[0].SuggestedCorrections[0].Text | Should -Be 'Invoke-Command' + } + + It "still adds the Get prefix to ordinary commands with resource-shaped arguments" { + $violations = Invoke-ScriptAnalyzer -ScriptDefinition 'Verb Argument {}' -IncludeRule $violationName + $violations.Count | Should -Be 1 + $violations[0].SuggestedCorrections[0].Text | Should -Be 'Get-Verb' + } } Context "Violation Extent" { @@ -65,6 +78,69 @@ Configuration MyDscConfiguration { Get-Count | ` Should -Be 0 } + + It "does not expand DSC resource names during formatting" -skip:($IsLinux -or $IsMacOS) { + $target = @' +Configuration PackageTest { + Package PackageExample + { + Name = "TestPackage" + } +} +'@ + $settings = @{ Rules = @{ PSAvoidUsingCmdletAliases = @{ Enable = $true } } } + Invoke-ScriptAnalyzer -ScriptDefinition $target -IncludeRule $violationName | + Get-Count | Should -Be 0 + Invoke-Formatter -ScriptDefinition $target -Settings $settings | Should -Be $target + } + } + + Context "DSC resource ASTs" { + It "ignores resource declarations but not executable commands" { + $target = @' +{ + Verb VerbExample {} + Verb NextLineExample + { + } + icm Server01 { Verb Argument {} } + & Verb Argument {} + function Get-Example { + Verb Argument {} + } + Node localhost + { + Verb NestedExample {} + Script ScriptExample { + SetScript = { + icm Server01 {} + Verb Argument {} + } + } + } + $scriptProperties = @{ + SetScript = { Verb Argument {} } + } +} +'@ + $ast = [System.Management.Automation.Language.Parser]::ParseInput($target, [ref]$null, [ref]$null) + $body = $ast.EndBlock.Statements[0].PipelineElements[0].Expression.Copy() + $configuration = [System.Management.Automation.Language.ConfigurationDefinitionAst]::new( + $ast.Extent, $body, 'Resource', + [System.Management.Automation.Language.StringConstantExpressionAst]::new($ast.Extent, 'Test', 'BareWord')) + $rule = [Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules.AvoidAlias]::new() + + $violations = @($rule.AnalyzeScript($configuration, $null)) + + $violations.Count | Should -Be 7 + $violations[0].Extent.Text | Should -Be 'icm' + $violations[1].Extent.Text | Should -Be 'Verb' + $violations[2].Extent.Text | Should -Be 'Verb' + $violations[3].Extent.Text | Should -Be 'Verb' + $violations[4].Extent.Text | Should -Be 'icm' + $violations[5].Extent.Text | Should -Be 'Verb' + $violations[6].Extent.Text | Should -Be 'Verb' + } } Context "Settings file provides allowlist" { From 4b4bb198d6d657292558d9ddce6abd860efd0d56 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Oct 2026 08:51:47 +0000 Subject: [PATCH 03/14] Handle wrapped DSC resource bodies Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --- Rules/AvoidAlias.cs | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/Rules/AvoidAlias.cs b/Rules/AvoidAlias.cs index 67011ece0..18944eef2 100644 --- a/Rules/AvoidAlias.cs +++ b/Rules/AvoidAlias.cs @@ -252,15 +252,27 @@ private ScriptBlockExpressionAst GetDscResourceBody(CommandAst cmdAst) // A resource's opening brace can be on the following line. if (cmdAst.CommandElements.Count == 2 && cmdAst.Parent is PipelineAst pipeline) { - var statements = (pipeline.Parent as NamedBlockAst)?.Statements - ?? (pipeline.Parent as StatementBlockAst)?.Statements; - int index = statements == null ? -1 : statements.IndexOf(pipeline); - if (index >= 0 && index + 1 < statements.Count - && statements[index + 1] is PipelineAst bodyPipeline - && bodyPipeline.PipelineElements.Count == 1 - && bodyPipeline.PipelineElements[0] is CommandExpressionAst bodyExpression) + Ast statement = pipeline; + for (Ast parent = pipeline.Parent; parent != null; parent = parent.Parent) { - return bodyExpression.Expression as ScriptBlockExpressionAst; + var statements = (parent as NamedBlockAst)?.Statements + ?? (parent as StatementBlockAst)?.Statements; + if (statements == null) + { + statement = parent; + continue; + } + + int index = statements.IndexOf(statement as StatementAst); + if (index >= 0 && index + 1 < statements.Count + && statements[index + 1] is PipelineAst bodyPipeline + && bodyPipeline.PipelineElements.Count == 1 + && bodyPipeline.PipelineElements[0] is CommandExpressionAst bodyExpression) + { + return bodyExpression.Expression as ScriptBlockExpressionAst; + } + + break; } } From b64eda73de9d3d0b09920dd249681d34b6e08389 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Oct 2026 09:35:02 +0000 Subject: [PATCH 04/14] Handle DSC resources before command matching Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --- Rules/AvoidAlias.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Rules/AvoidAlias.cs b/Rules/AvoidAlias.cs index 18944eef2..417ac344e 100644 --- a/Rules/AvoidAlias.cs +++ b/Rules/AvoidAlias.cs @@ -133,13 +133,13 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) continue; } - // If we find match of any kind, do not continue with the Get-{commandname} check - if ( Helper.Instance.GetCommandInfo(commandName) != null ) { + if (IsDscResourceDeclaration(cmdAst)) + { continue; } - if (IsDscResourceDeclaration(cmdAst)) - { + // If we find match of any kind, do not continue with the Get-{commandname} check + if ( Helper.Instance.GetCommandInfo(commandName) != null ) { continue; } From cabfc1741c2dc3edaad548205a8a5e8b24b3eab0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Oct 2026 11:42:10 +0000 Subject: [PATCH 05/14] Fix DSC resource alias classification Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --- Rules/AvoidAlias.cs | 12 +++++++----- Tests/Rules/AvoidUsingAlias.tests.ps1 | 4 ++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Rules/AvoidAlias.cs b/Rules/AvoidAlias.cs index 417ac344e..3fb623f29 100644 --- a/Rules/AvoidAlias.cs +++ b/Rules/AvoidAlias.cs @@ -119,6 +119,13 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) } string cmdletNameIfCommandNameWasAlias = Helper.Instance.GetCmdletNameFromAlias(commandName); + bool isDscResourceDeclaration = IsDscResourceDeclaration(cmdAst); + if (isDscResourceDeclaration + && (String.IsNullOrEmpty(cmdletNameIfCommandNameWasAlias) || cmdAst.CommandElements.Count == 2)) + { + continue; + } + if (!String.IsNullOrEmpty(cmdletNameIfCommandNameWasAlias)) { yield return new DiagnosticRecord( @@ -133,11 +140,6 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) continue; } - if (IsDscResourceDeclaration(cmdAst)) - { - continue; - } - // If we find match of any kind, do not continue with the Get-{commandname} check if ( Helper.Instance.GetCommandInfo(commandName) != null ) { continue; diff --git a/Tests/Rules/AvoidUsingAlias.tests.ps1 b/Tests/Rules/AvoidUsingAlias.tests.ps1 index 96e31df5b..1f4a15877 100644 --- a/Tests/Rules/AvoidUsingAlias.tests.ps1 +++ b/Tests/Rules/AvoidUsingAlias.tests.ps1 @@ -103,6 +103,10 @@ Configuration PackageTest { Verb NextLineExample { } + gci ResourceAliasExample + { + Name = "Example" + } icm Server01 { Verb Argument {} } & Verb Argument {} function Get-Example { From 5c3cbec0f30e1e6528c0b1cc68c1ae58ba570b78 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:16:38 +0000 Subject: [PATCH 06/14] Ignore parser diagnostics in DSC alias test Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --- Tests/Rules/AvoidUsingAlias.tests.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/Rules/AvoidUsingAlias.tests.ps1 b/Tests/Rules/AvoidUsingAlias.tests.ps1 index 1f4a15877..e9b89a02f 100644 --- a/Tests/Rules/AvoidUsingAlias.tests.ps1 +++ b/Tests/Rules/AvoidUsingAlias.tests.ps1 @@ -90,6 +90,7 @@ Configuration PackageTest { '@ $settings = @{ Rules = @{ PSAvoidUsingCmdletAliases = @{ Enable = $true } } } Invoke-ScriptAnalyzer -ScriptDefinition $target -IncludeRule $violationName | + Where-Object { $_.RuleName -eq $violationName } | Get-Count | Should -Be 0 Invoke-Formatter -ScriptDefinition $target -Settings $settings | Should -Be $target } From ee28f1077811e739f3dc1277c55f80d053aad76a Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:18:02 +0000 Subject: [PATCH 07/14] Tighten DSC resource detection in AvoidAlias (#2280) * Initial plan * Fix AvoidAlias DSC review feedback Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --- Rules/AvoidAlias.cs | 74 ++++++++++++++++++++++++++- Tests/Rules/AvoidUsingAlias.tests.ps1 | 39 ++++++++++++++ 2 files changed, 111 insertions(+), 2 deletions(-) diff --git a/Rules/AvoidAlias.cs b/Rules/AvoidAlias.cs index 3fb623f29..d59920db4 100644 --- a/Rules/AvoidAlias.cs +++ b/Rules/AvoidAlias.cs @@ -195,7 +195,25 @@ private bool IgnoreCommandast(CommandAst cmdAst) private bool IsDscResourceDeclaration(CommandAst cmdAst) { - if (GetDscResourceBody(cmdAst) == null) + bool hasConfigurationAncestor = false; + for (Ast parent = cmdAst.Parent; parent != null; parent = parent.Parent) + { + if (parent is ConfigurationDefinitionAst) + { + hasConfigurationAncestor = true; + break; + } + + if (parent is FunctionDefinitionAst + || parent is HashtableAst + || parent is AssignmentStatementAst + || parent is ParenExpressionAst) + { + return false; + } + } + + if (!hasConfigurationAncestor || GetDscResourceBody(cmdAst) == null) { return false; } @@ -207,7 +225,17 @@ private bool IsDscResourceDeclaration(CommandAst cmdAst) return true; } - if (parent is FunctionDefinitionAst || parent is HashtableAst) + if (parent is FunctionDefinitionAst + || parent is HashtableAst + || parent is AssignmentStatementAst + || parent is ParenExpressionAst) + { + return false; + } + + if (parent is CommandExpressionAst commandExpression + && (!(commandExpression.Expression is ScriptBlockExpressionAst body) + || !IsFollowingLineDscResourceBody(body))) { return false; } @@ -236,6 +264,41 @@ private bool IsDscResourceDeclaration(CommandAst cmdAst) return false; } + private bool IsFollowingLineDscResourceBody(ScriptBlockExpressionAst body) + { + if (!(body.Parent is CommandExpressionAst bodyExpression) + || !(bodyExpression.Parent is PipelineAst bodyPipeline) + || bodyPipeline.PipelineElements.Count != 1 + || bodyPipeline.PipelineElements[0] != bodyExpression) + { + return false; + } + + Ast statement = bodyPipeline; + for (Ast parent = bodyPipeline.Parent; parent != null; parent = parent.Parent) + { + var statements = (parent as NamedBlockAst)?.Statements + ?? (parent as StatementBlockAst)?.Statements; + if (statements == null) + { + statement = parent; + continue; + } + + int index = statements.IndexOf(statement as StatementAst); + if (index <= 0 || !(statements[index - 1] is PipelineAst declarationPipeline) + || declarationPipeline.PipelineElements.Count != 1 + || !(declarationPipeline.PipelineElements[0] is CommandAst declaration)) + { + return false; + } + + return GetDscResourceBody(declaration) == body; + } + + return false; + } + private ScriptBlockExpressionAst GetDscResourceBody(CommandAst cmdAst) { if (cmdAst.InvocationOperator != TokenKind.Unknown @@ -246,6 +309,13 @@ private ScriptBlockExpressionAst GetDscResourceBody(CommandAst cmdAst) return null; } + if (!(cmdAst.Parent is PipelineAst commandPipeline) + || commandPipeline.PipelineElements.Count != 1 + || commandPipeline.PipelineElements[0] != cmdAst) + { + return null; + } + if (cmdAst.CommandElements.Count == 3) { return cmdAst.CommandElements[2] as ScriptBlockExpressionAst; diff --git a/Tests/Rules/AvoidUsingAlias.tests.ps1 b/Tests/Rules/AvoidUsingAlias.tests.ps1 index e9b89a02f..bae7310a7 100644 --- a/Tests/Rules/AvoidUsingAlias.tests.ps1 +++ b/Tests/Rules/AvoidUsingAlias.tests.ps1 @@ -146,6 +146,45 @@ Configuration PackageTest { $violations[5].Extent.Text | Should -Be 'Verb' $violations[6].Extent.Text | Should -Be 'Verb' } + + It "does not ignore executable commands in assignment and expression contexts or pipelines" { + $target = @' +{ + $items = ChildItem . {} + $result = Verb Argument {} + $action = { Verb Argument {} } + Name = (Verb Argument {}) + Script ScriptExample { + SetScript = + { + Verb Argument {} + } + } + Verb Argument {} | Out-Null + gci . | Out-Null + { + } +} +'@ + $ast = [System.Management.Automation.Language.Parser]::ParseInput($target, [ref]$null, [ref]$null) + $body = $ast.EndBlock.Statements[0].PipelineElements[0].Expression.Copy() + $configuration = [System.Management.Automation.Language.ConfigurationDefinitionAst]::new( + $ast.Extent, $body, 'Resource', + [System.Management.Automation.Language.StringConstantExpressionAst]::new($ast.Extent, 'Test', 'BareWord')) + $rule = [Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules.AvoidAlias]::new() + + $violations = @($rule.AnalyzeScript($configuration, $null)) + + $violations.Count | Should -Be 7 + $violations[0].Extent.Text | Should -Be 'ChildItem' + $violations[0].SuggestedCorrections[0].Text | Should -Be 'Get-ChildItem' + $violations[1].Extent.Text | Should -Be 'Verb' + $violations[2].Extent.Text | Should -Be 'Verb' + $violations[3].Extent.Text | Should -Be 'Verb' + $violations[4].Extent.Text | Should -Be 'Verb' + $violations[5].Extent.Text | Should -Be 'Verb' + $violations[6].Extent.Text | Should -Be 'gci' + } } Context "Settings file provides allowlist" { From ab700bf7b1ea450eaff7172eb1f9fe7a093808a1 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:34:56 +0000 Subject: [PATCH 08/14] Preserve nested DSC resources in same-line Node blocks (#2282) * Initial plan * Preserve DSC Node resource declarations Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --- Rules/AvoidAlias.cs | 3 ++- Tests/Rules/AvoidUsingAlias.tests.ps1 | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Rules/AvoidAlias.cs b/Rules/AvoidAlias.cs index d59920db4..087e6bfc8 100644 --- a/Rules/AvoidAlias.cs +++ b/Rules/AvoidAlias.cs @@ -247,7 +247,8 @@ private bool IsDscResourceDeclaration(CommandAst cmdAst) { if (GetDscResourceBody(enclosingCommand) != scriptBlock || !String.IsNullOrEmpty(Helper.Instance.GetCmdletNameFromAlias(enclosingCommand.GetCommandName())) - || Helper.Instance.GetCommandInfo(enclosingCommand.GetCommandName()) != null) + || (!String.Equals(enclosingCommand.GetCommandName(), "Node", StringComparison.OrdinalIgnoreCase) + && Helper.Instance.GetCommandInfo(enclosingCommand.GetCommandName()) != null)) { return false; } diff --git a/Tests/Rules/AvoidUsingAlias.tests.ps1 b/Tests/Rules/AvoidUsingAlias.tests.ps1 index bae7310a7..2f2614a40 100644 --- a/Tests/Rules/AvoidUsingAlias.tests.ps1 +++ b/Tests/Rules/AvoidUsingAlias.tests.ps1 @@ -97,6 +97,22 @@ Configuration PackageTest { } Context "DSC resource ASTs" { + 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)) { + $target = @' +{ + Node localhost { Package PackageExample {} } +} +'@ + $ast = [System.Management.Automation.Language.Parser]::ParseInput($target, [ref]$null, [ref]$null) + $body = $ast.EndBlock.Statements[0].PipelineElements[0].Expression.Copy() + $configuration = [System.Management.Automation.Language.ConfigurationDefinitionAst]::new( + $ast.Extent, $body, 'Resource', + [System.Management.Automation.Language.StringConstantExpressionAst]::new($ast.Extent, 'Test', 'BareWord')) + $rule = [Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules.AvoidAlias]::new() + + @($rule.AnalyzeScript($configuration, $null)).Count | Should -Be 0 + } + It "ignores resource declarations but not executable commands" { $target = @' { From 26f025c579b2747feb36259db407c3aafc674939 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Oct 2026 18:19:59 +0000 Subject: [PATCH 09/14] Preserve alias corrections for executable commands in fallback DSC ASTs (#2284) * Initial plan * Fix fallback DSC resource detection Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --- Rules/AvoidAlias.cs | 14 ++++++++---- Tests/Rules/AvoidUsingAlias.tests.ps1 | 31 +++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/Rules/AvoidAlias.cs b/Rules/AvoidAlias.cs index 087e6bfc8..ba069453b 100644 --- a/Rules/AvoidAlias.cs +++ b/Rules/AvoidAlias.cs @@ -207,7 +207,9 @@ private bool IsDscResourceDeclaration(CommandAst cmdAst) if (parent is FunctionDefinitionAst || parent is HashtableAst || parent is AssignmentStatementAst - || parent is ParenExpressionAst) + || parent is ParenExpressionAst + || parent is SubExpressionAst + || parent is ArrayExpressionAst) { return false; } @@ -228,7 +230,9 @@ private bool IsDscResourceDeclaration(CommandAst cmdAst) if (parent is FunctionDefinitionAst || parent is HashtableAst || parent is AssignmentStatementAst - || parent is ParenExpressionAst) + || parent is ParenExpressionAst + || parent is SubExpressionAst + || parent is ArrayExpressionAst) { return false; } @@ -305,14 +309,16 @@ private ScriptBlockExpressionAst GetDscResourceBody(CommandAst cmdAst) if (cmdAst.InvocationOperator != TokenKind.Unknown || cmdAst.CommandElements.Count < 2 || !(cmdAst.CommandElements[1] is ExpressionAst) - || cmdAst.CommandElements[1].Extent.Text == "=") + || cmdAst.CommandElements[1].Extent.Text == "=" + || cmdAst.Redirections.Count != 0) { return null; } if (!(cmdAst.Parent is PipelineAst commandPipeline) || commandPipeline.PipelineElements.Count != 1 - || commandPipeline.PipelineElements[0] != cmdAst) + || commandPipeline.PipelineElements[0] != cmdAst + || !(commandPipeline.Parent is NamedBlockAst || commandPipeline.Parent is StatementBlockAst)) { return null; } diff --git a/Tests/Rules/AvoidUsingAlias.tests.ps1 b/Tests/Rules/AvoidUsingAlias.tests.ps1 index 2f2614a40..2bdd55b33 100644 --- a/Tests/Rules/AvoidUsingAlias.tests.ps1 +++ b/Tests/Rules/AvoidUsingAlias.tests.ps1 @@ -201,6 +201,37 @@ Configuration PackageTest { $violations[5].Extent.Text | Should -Be 'Verb' $violations[6].Extent.Text | Should -Be 'gci' } + + It "does not mistake executable pipelines for DSC resources in fallback configuration ASTs" { + $target = @' +{ + Write-Output $(Verb Argument {}) + @(Verb Argument {}) + Verb Argument {} > $null + if (Verb Argument {}) {} + return Verb Argument {} + if ($true) { + Verb SameLineResource {} + Verb NextLineResource + { + } + } +} +'@ + $ast = [System.Management.Automation.Language.Parser]::ParseInput($target, [ref]$null, [ref]$null) + $body = $ast.EndBlock.Statements[0].PipelineElements[0].Expression.Copy() + $configuration = [System.Management.Automation.Language.ConfigurationDefinitionAst]::new( + $ast.Extent, $body, 'Resource', + [System.Management.Automation.Language.StringConstantExpressionAst]::new($ast.Extent, 'Test', 'BareWord')) + $rule = [Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules.AvoidAlias]::new() + + $violations = @($rule.AnalyzeScript($configuration, $null)) + + $violations.Count | Should -Be 5 + $violations | ForEach-Object { + $_.SuggestedCorrections[0].Text | Should -Be 'Get-Verb' + } + } } Context "Settings file provides allowlist" { From aeccd615fd176baf3c4b3846e96b900cda5d6523 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:35:18 +0000 Subject: [PATCH 10/14] Reject invalid DSC resource-body pairings (#2286) * Initial plan * Fix DSC resource body detection edge cases Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> * Optimize DSC separator detection Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --- Rules/AvoidAlias.cs | 26 ++++++++++++++++++++++++++ Tests/Rules/AvoidUsingAlias.tests.ps1 | 24 ++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/Rules/AvoidAlias.cs b/Rules/AvoidAlias.cs index ba069453b..7b8a46c2f 100644 --- a/Rules/AvoidAlias.cs +++ b/Rules/AvoidAlias.cs @@ -348,6 +348,12 @@ private ScriptBlockExpressionAst GetDscResourceBody(CommandAst cmdAst) && bodyPipeline.PipelineElements.Count == 1 && bodyPipeline.PipelineElements[0] is CommandExpressionAst bodyExpression) { + if (HasSemicolonSeparator(pipeline, bodyPipeline) + || bodyExpression.Redirections.Count != 0) + { + return null; + } + return bodyExpression.Expression as ScriptBlockExpressionAst; } @@ -358,6 +364,26 @@ private ScriptBlockExpressionAst GetDscResourceBody(CommandAst cmdAst) return null; } + private bool HasSemicolonSeparator(Ast statement, Ast nextStatement) + { + int separatorStart = statement.Extent.EndOffset; + int separatorLength = nextStatement.Extent.StartOffset - separatorStart; + if (separatorLength <= 0) + { + return false; + } + + string script = statement.Extent.EndScriptPosition.GetFullScript(); + string separator = script.Substring(separatorStart, separatorLength); + if (separator.IndexOf(';') < 0) + { + return false; + } + + Parser.ParseInput(separator, out Token[] tokens, out ParseError[] errors); + return tokens.Any(token => token.Kind == TokenKind.Semi); + } + /// /// For a command like "gci -path c:", returns the extent of "gci" in the command /// diff --git a/Tests/Rules/AvoidUsingAlias.tests.ps1 b/Tests/Rules/AvoidUsingAlias.tests.ps1 index 2bdd55b33..731dc4137 100644 --- a/Tests/Rules/AvoidUsingAlias.tests.ps1 +++ b/Tests/Rules/AvoidUsingAlias.tests.ps1 @@ -232,6 +232,30 @@ Configuration PackageTest { $_.SuggestedCorrections[0].Text | Should -Be 'Get-Verb' } } + + It "does not pair following-line blocks with commands when the block has redirections or a statement terminator intervenes" { + $target = @' +{ + gci . + {} > $null + gci .; {} +} +'@ + $ast = [System.Management.Automation.Language.Parser]::ParseInput($target, [ref]$null, [ref]$null) + $body = $ast.EndBlock.Statements[0].PipelineElements[0].Expression.Copy() + $configuration = [System.Management.Automation.Language.ConfigurationDefinitionAst]::new( + $ast.Extent, $body, 'Resource', + [System.Management.Automation.Language.StringConstantExpressionAst]::new($ast.Extent, 'Test', 'BareWord')) + $rule = [Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules.AvoidAlias]::new() + + $violations = @($rule.AnalyzeScript($configuration, $null)) + + $violations.Count | Should -Be 2 + $violations | ForEach-Object { + $_.Extent.Text | Should -Be 'gci' + $_.SuggestedCorrections[0].Text | Should -Be 'Get-ChildItem' + } + } } Context "Settings file provides allowlist" { From 6dcd4a4bd309e59208958be4d7312467f954c1e9 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:15:17 +0000 Subject: [PATCH 11/14] Reject ampersand-separated DSC resource bodies (#2288) * Initial plan * Reject ampersand-separated DSC body pairing Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> * Fix DSC separator detection across PowerShell versions Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --- Rules/AvoidAlias.cs | 10 +++++----- Tests/Rules/AvoidUsingAlias.tests.ps1 | 6 ++++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Rules/AvoidAlias.cs b/Rules/AvoidAlias.cs index 7b8a46c2f..f40d961f1 100644 --- a/Rules/AvoidAlias.cs +++ b/Rules/AvoidAlias.cs @@ -348,7 +348,7 @@ private ScriptBlockExpressionAst GetDscResourceBody(CommandAst cmdAst) && bodyPipeline.PipelineElements.Count == 1 && bodyPipeline.PipelineElements[0] is CommandExpressionAst bodyExpression) { - if (HasSemicolonSeparator(pipeline, bodyPipeline) + if (HasStatementSeparator(cmdAst, bodyPipeline) || bodyExpression.Redirections.Count != 0) { return null; @@ -364,9 +364,9 @@ private ScriptBlockExpressionAst GetDscResourceBody(CommandAst cmdAst) return null; } - private bool HasSemicolonSeparator(Ast statement, Ast nextStatement) + private bool HasStatementSeparator(CommandAst statement, Ast nextStatement) { - int separatorStart = statement.Extent.EndOffset; + int separatorStart = statement.CommandElements[statement.CommandElements.Count - 1].Extent.EndOffset; int separatorLength = nextStatement.Extent.StartOffset - separatorStart; if (separatorLength <= 0) { @@ -375,13 +375,13 @@ private bool HasSemicolonSeparator(Ast statement, Ast nextStatement) string script = statement.Extent.EndScriptPosition.GetFullScript(); string separator = script.Substring(separatorStart, separatorLength); - if (separator.IndexOf(';') < 0) + if (separator.IndexOf(';') < 0 && separator.IndexOf('&') < 0) { return false; } Parser.ParseInput(separator, out Token[] tokens, out ParseError[] errors); - return tokens.Any(token => token.Kind == TokenKind.Semi); + return tokens.Any(token => token.Kind == TokenKind.Semi || token.Kind == TokenKind.Ampersand); } /// diff --git a/Tests/Rules/AvoidUsingAlias.tests.ps1 b/Tests/Rules/AvoidUsingAlias.tests.ps1 index 731dc4137..1c2470447 100644 --- a/Tests/Rules/AvoidUsingAlias.tests.ps1 +++ b/Tests/Rules/AvoidUsingAlias.tests.ps1 @@ -233,12 +233,14 @@ Configuration PackageTest { } } - It "does not pair following-line blocks with commands when the block has redirections or a statement terminator intervenes" { + It "does not pair following-line blocks with commands when the block has redirections or a statement separator intervenes" { $target = @' { gci . {} > $null gci .; {} + gci . & + {} } '@ $ast = [System.Management.Automation.Language.Parser]::ParseInput($target, [ref]$null, [ref]$null) @@ -250,7 +252,7 @@ Configuration PackageTest { $violations = @($rule.AnalyzeScript($configuration, $null)) - $violations.Count | Should -Be 2 + $violations.Count | Should -Be 3 $violations | ForEach-Object { $_.Extent.Text | Should -Be 'gci' $_.SuggestedCorrections[0].Text | Should -Be 'Get-ChildItem' From 3b14fd8d3be02f85dd6c10212c4c95de6a35234b Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Oct 2026 09:10:45 +0000 Subject: [PATCH 12/14] Reject background pipelines as DSC resources (#2290) * Initial plan * Reject background pipelines as DSC resources Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> * Guard background AST API for CoreCLR Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> * Make background pipeline test expectations version-aware Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --- Rules/AvoidAlias.cs | 6 ++++++ Tests/Rules/AvoidUsingAlias.tests.ps1 | 9 +++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Rules/AvoidAlias.cs b/Rules/AvoidAlias.cs index f40d961f1..fa6f94efa 100644 --- a/Rules/AvoidAlias.cs +++ b/Rules/AvoidAlias.cs @@ -318,6 +318,9 @@ private ScriptBlockExpressionAst GetDscResourceBody(CommandAst cmdAst) if (!(cmdAst.Parent is PipelineAst commandPipeline) || commandPipeline.PipelineElements.Count != 1 || commandPipeline.PipelineElements[0] != cmdAst +#if CORECLR + || commandPipeline.Background +#endif || !(commandPipeline.Parent is NamedBlockAst || commandPipeline.Parent is StatementBlockAst)) { return null; @@ -346,6 +349,9 @@ private ScriptBlockExpressionAst GetDscResourceBody(CommandAst cmdAst) if (index >= 0 && index + 1 < statements.Count && statements[index + 1] is PipelineAst bodyPipeline && bodyPipeline.PipelineElements.Count == 1 +#if CORECLR + && !bodyPipeline.Background +#endif && bodyPipeline.PipelineElements[0] is CommandExpressionAst bodyExpression) { if (HasStatementSeparator(cmdAst, bodyPipeline) diff --git a/Tests/Rules/AvoidUsingAlias.tests.ps1 b/Tests/Rules/AvoidUsingAlias.tests.ps1 index 1c2470447..7dc377774 100644 --- a/Tests/Rules/AvoidUsingAlias.tests.ps1 +++ b/Tests/Rules/AvoidUsingAlias.tests.ps1 @@ -208,6 +208,7 @@ Configuration PackageTest { Write-Output $(Verb Argument {}) @(Verb Argument {}) Verb Argument {} > $null + Verb Argument {} & if (Verb Argument {}) {} return Verb Argument {} if ($true) { @@ -227,7 +228,8 @@ Configuration PackageTest { $violations = @($rule.AnalyzeScript($configuration, $null)) - $violations.Count | Should -Be 5 + $expectedViolations = if ($PSVersionTable.PSVersion.Major -ge 7) { 6 } else { 5 } + $violations.Count | Should -Be $expectedViolations $violations | ForEach-Object { $_.SuggestedCorrections[0].Text | Should -Be 'Get-Verb' } @@ -241,6 +243,8 @@ Configuration PackageTest { gci .; {} gci . & {} + gci . + {} & } '@ $ast = [System.Management.Automation.Language.Parser]::ParseInput($target, [ref]$null, [ref]$null) @@ -252,7 +256,8 @@ Configuration PackageTest { $violations = @($rule.AnalyzeScript($configuration, $null)) - $violations.Count | Should -Be 3 + $expectedViolations = if ($PSVersionTable.PSVersion.Major -ge 7) { 4 } else { 3 } + $violations.Count | Should -Be $expectedViolations $violations | ForEach-Object { $_.Extent.Text | Should -Be 'gci' $_.SuggestedCorrections[0].Text | Should -Be 'Get-ChildItem' From e51412ddd3d59a6e2e1236f1efa1dd06c38343a9 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Oct 2026 11:23:32 +0000 Subject: [PATCH 13/14] Preserve alias diagnostics after executable commands in DSC ASTs (#2293) * Initial plan * Guard following-line DSC resource detection Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --- Rules/AvoidAlias.cs | 6 +++++- Tests/Rules/AvoidUsingAlias.tests.ps1 | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/Rules/AvoidAlias.cs b/Rules/AvoidAlias.cs index fa6f94efa..d1ab8b780 100644 --- a/Rules/AvoidAlias.cs +++ b/Rules/AvoidAlias.cs @@ -298,7 +298,11 @@ private bool IsFollowingLineDscResourceBody(ScriptBlockExpressionAst body) return false; } - return GetDscResourceBody(declaration) == body; + string declarationCommandName = declaration.GetCommandName(); + return GetDscResourceBody(declaration) == body + && String.IsNullOrEmpty(Helper.Instance.GetCmdletNameFromAlias(declarationCommandName)) + && (String.Equals(declarationCommandName, "Node", StringComparison.OrdinalIgnoreCase) + || Helper.Instance.GetCommandInfo(declarationCommandName) == null); } return false; diff --git a/Tests/Rules/AvoidUsingAlias.tests.ps1 b/Tests/Rules/AvoidUsingAlias.tests.ps1 index 7dc377774..60b4c7a6f 100644 --- a/Tests/Rules/AvoidUsingAlias.tests.ps1 +++ b/Tests/Rules/AvoidUsingAlias.tests.ps1 @@ -235,6 +235,29 @@ Configuration PackageTest { } } + It "does not mistake following-line blocks after executable commands for DSC resources" { + $target = @' +{ + Write-Output value + { + Verb Argument {} + } +} +'@ + $ast = [System.Management.Automation.Language.Parser]::ParseInput($target, [ref]$null, [ref]$null) + $body = $ast.EndBlock.Statements[0].PipelineElements[0].Expression.Copy() + $configuration = [System.Management.Automation.Language.ConfigurationDefinitionAst]::new( + $ast.Extent, $body, 'Resource', + [System.Management.Automation.Language.StringConstantExpressionAst]::new($ast.Extent, 'Test', 'BareWord')) + $rule = [Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules.AvoidAlias]::new() + + $violations = @($rule.AnalyzeScript($configuration, $null)) + + $violations.Count | Should -Be 1 + $violations[0].Extent.Text | Should -Be 'Verb' + $violations[0].SuggestedCorrections[0].Text | Should -Be 'Get-Verb' + } + It "does not pair following-line blocks with commands when the block has redirections or a statement separator intervenes" { $target = @' { From 9914f203914af7b1b62fc4f69b343de2c220d8f5 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Oct 2026 14:54:02 +0000 Subject: [PATCH 14/14] Preserve alias diagnostics for two-element DSC-shaped commands (#2295) * Initial plan * Fix AvoidAlias DSC regression Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> * Fix Windows Pester expected alias count Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bergmeister <9250262+bergmeister@users.noreply.github.com> --- Rules/AvoidAlias.cs | 3 +-- Tests/Rules/AvoidUsingAlias.tests.ps1 | 15 ++++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Rules/AvoidAlias.cs b/Rules/AvoidAlias.cs index d1ab8b780..c6435833d 100644 --- a/Rules/AvoidAlias.cs +++ b/Rules/AvoidAlias.cs @@ -120,8 +120,7 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) string cmdletNameIfCommandNameWasAlias = Helper.Instance.GetCmdletNameFromAlias(commandName); bool isDscResourceDeclaration = IsDscResourceDeclaration(cmdAst); - if (isDscResourceDeclaration - && (String.IsNullOrEmpty(cmdletNameIfCommandNameWasAlias) || cmdAst.CommandElements.Count == 2)) + if (isDscResourceDeclaration && String.IsNullOrEmpty(cmdletNameIfCommandNameWasAlias)) { continue; } diff --git a/Tests/Rules/AvoidUsingAlias.tests.ps1 b/Tests/Rules/AvoidUsingAlias.tests.ps1 index 60b4c7a6f..814574525 100644 --- a/Tests/Rules/AvoidUsingAlias.tests.ps1 +++ b/Tests/Rules/AvoidUsingAlias.tests.ps1 @@ -153,14 +153,16 @@ Configuration PackageTest { $violations = @($rule.AnalyzeScript($configuration, $null)) - $violations.Count | Should -Be 7 - $violations[0].Extent.Text | Should -Be 'icm' - $violations[1].Extent.Text | Should -Be 'Verb' + $violations.Count | Should -Be 8 + $violations[0].Extent.Text | Should -Be 'gci' + $violations[0].SuggestedCorrections[0].Text | Should -Be 'Get-ChildItem' + $violations[1].Extent.Text | Should -Be 'icm' $violations[2].Extent.Text | Should -Be 'Verb' $violations[3].Extent.Text | Should -Be 'Verb' - $violations[4].Extent.Text | Should -Be 'icm' - $violations[5].Extent.Text | Should -Be 'Verb' + $violations[4].Extent.Text | Should -Be 'Verb' + $violations[5].Extent.Text | Should -Be 'icm' $violations[6].Extent.Text | Should -Be 'Verb' + $violations[7].Extent.Text | Should -Be 'Verb' } It "does not ignore executable commands in assignment and expression contexts or pipelines" { @@ -279,8 +281,7 @@ Configuration PackageTest { $violations = @($rule.AnalyzeScript($configuration, $null)) - $expectedViolations = if ($PSVersionTable.PSVersion.Major -ge 7) { 4 } else { 3 } - $violations.Count | Should -Be $expectedViolations + $violations.Count | Should -Be 4 $violations | ForEach-Object { $_.Extent.Text | Should -Be 'gci' $_.SuggestedCorrections[0].Text | Should -Be 'Get-ChildItem'