Skip to content

Commit e88b4c4

Browse files
authored
PSAvoidUsingPositionalParameters: Do not warn on AZ CLI (#1846)
* PSAvoidUsingPositionalParameters: Do not warn on AZ CLI * Add CommandAllowList configuration and make it case insensitive * fix failing test on Linux as az is not a script on linux and make test not depend on AZ CLI
1 parent e923e16 commit e88b4c4

File tree

3 files changed

+66
-13
lines changed

3 files changed

+66
-13
lines changed

Rules/AvoidPositionalParameters.cs

+26-13
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.Management.Automation.Language;
77
using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic;
8+
using System.Linq;
89
#if !CORECLR
910
using System.ComponentModel.Composition;
1011
#endif
@@ -18,12 +19,20 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
1819
#if !CORECLR
1920
[Export(typeof(IScriptRule))]
2021
#endif
21-
public class AvoidPositionalParameters : IScriptRule
22+
public class AvoidPositionalParameters : ConfigurableRule
2223
{
24+
[ConfigurableRuleProperty(defaultValue: new string[] { "az" })]
25+
public string[] CommandAllowList { get; set; }
26+
27+
public AvoidPositionalParameters()
28+
{
29+
Enable = true; // keep it enabled by default, user can still override this with settings
30+
}
31+
2332
/// <summary>
2433
/// AnalyzeScript: Analyze the ast to check that positional parameters are not used.
2534
/// </summary>
26-
public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
35+
public override IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
2736
{
2837
if (ast == null) throw new ArgumentNullException(Strings.NullAstErrorMessage);
2938

@@ -57,20 +66,24 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
5766
{
5867
PipelineAst parent = cmdAst.Parent as PipelineAst;
5968

69+
string commandName = cmdAst.GetCommandName();
6070
if (parent != null && parent.PipelineElements.Count > 1)
6171
{
6272
// raise if it's the first element in pipeline. otherwise no.
63-
if (parent.PipelineElements[0] == cmdAst)
73+
if (parent.PipelineElements[0] == cmdAst && !CommandAllowList.Contains(commandName, StringComparer.OrdinalIgnoreCase))
6474
{
65-
yield return new DiagnosticRecord(string.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingPositionalParametersError, cmdAst.GetCommandName()),
66-
cmdAst.Extent, GetName(), DiagnosticSeverity.Information, fileName, cmdAst.GetCommandName());
75+
yield return new DiagnosticRecord(string.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingPositionalParametersError, commandName),
76+
cmdAst.Extent, GetName(), DiagnosticSeverity.Information, fileName, commandName);
6777
}
6878
}
6979
// not in pipeline so just raise it normally
7080
else
7181
{
72-
yield return new DiagnosticRecord(string.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingPositionalParametersError, cmdAst.GetCommandName()),
73-
cmdAst.Extent, GetName(), DiagnosticSeverity.Information, fileName, cmdAst.GetCommandName());
82+
if (!CommandAllowList.Contains(commandName, StringComparer.OrdinalIgnoreCase))
83+
{
84+
yield return new DiagnosticRecord(string.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingPositionalParametersError, commandName),
85+
cmdAst.Extent, GetName(), DiagnosticSeverity.Information, fileName, commandName);
86+
}
7487
}
7588
}
7689
}
@@ -80,7 +93,7 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
8093
/// GetName: Retrieves the name of this rule.
8194
/// </summary>
8295
/// <returns>The name of this rule</returns>
83-
public string GetName()
96+
public override string GetName()
8497
{
8598
return string.Format(CultureInfo.CurrentCulture, Strings.NameSpaceFormat, GetSourceName(), Strings.AvoidUsingPositionalParametersName);
8699
}
@@ -89,7 +102,7 @@ public string GetName()
89102
/// GetCommonName: Retrieves the common name of this rule.
90103
/// </summary>
91104
/// <returns>The common name of this rule</returns>
92-
public string GetCommonName()
105+
public override string GetCommonName()
93106
{
94107
return string.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingPositionalParametersCommonName);
95108
}
@@ -98,15 +111,15 @@ public string GetCommonName()
98111
/// GetDescription: Retrieves the description of this rule.
99112
/// </summary>
100113
/// <returns>The description of this rule</returns>
101-
public string GetDescription()
114+
public override string GetDescription()
102115
{
103116
return string.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingPositionalParametersDescription);
104117
}
105118

106119
/// <summary>
107120
/// Method: Retrieves the type of the rule: builtin, managed or module.
108121
/// </summary>
109-
public SourceType GetSourceType()
122+
public override SourceType GetSourceType()
110123
{
111124
return SourceType.Builtin;
112125
}
@@ -115,15 +128,15 @@ public SourceType GetSourceType()
115128
/// GetSeverity: Retrieves the severity of the rule: error, warning of information.
116129
/// </summary>
117130
/// <returns></returns>
118-
public RuleSeverity GetSeverity()
131+
public override RuleSeverity GetSeverity()
119132
{
120133
return RuleSeverity.Information;
121134
}
122135

123136
/// <summary>
124137
/// Method: Retrieves the module/assembly name the rule is from.
125138
/// </summary>
126-
public string GetSourceName()
139+
public override string GetSourceName()
127140
{
128141
return string.Format(CultureInfo.CurrentCulture, Strings.SourceName);
129142
}

Tests/Rules/AvoidPositionalParameters.tests.ps1

+19
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ Describe "AvoidPositionalParameters" {
2626
$violations.RuleName | Should -Contain 'PSAvoidUsingCmdletAliases'
2727
}
2828

29+
It "returns violations for command that is not in allow list of settings" {
30+
$violations = Invoke-ScriptAnalyzer -ScriptDefinition 'Join-Path a b c d' -Settings @{
31+
IncludeRules = @('PSAvoidUsingPositionalParameters')
32+
Rules = @{ PSAvoidUsingPositionalParameters = @{ CommandAllowList = 'Test-Path' } }
33+
}
34+
$violations.Count | Should -Be 1
35+
$violations.RuleName | Should -Be 'PSAvoidUsingPositionalParameters'
36+
}
2937
}
3038

3139
Context "When there are no violations" {
@@ -36,6 +44,17 @@ Describe "AvoidPositionalParameters" {
3644
It "returns no violations for DSC configuration" {
3745
$noViolationsDSC.Count | Should -Be 0
3846
}
47+
48+
It "returns no violations for AZ CLI by default" {
49+
Invoke-ScriptAnalyzer -ScriptDefinition 'az group deployment list' | Should -BeNullOrEmpty
50+
}
51+
52+
It "returns no violations for command from allow list defined in settings and is case invariant" {
53+
Invoke-ScriptAnalyzer -ScriptDefinition 'join-patH a b c' -Settings @{
54+
IncludeRules = @('PSAvoidUsingPositionalParameters')
55+
Rules = @{ PSAvoidUsingPositionalParameters = @{ CommandAllowList = 'az', 'Join-Path' } }
56+
} | Should -BeNullOrEmpty
57+
}
3958
}
4059

4160
Context "Function defined and called in script, which has 3 or more positional parameters triggers rule." {

docs/Rules/AvoidUsingPositionalParameters.md

+21
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,27 @@ rule from being too noisy, this rule gets only triggered when there are 3 or mor
2020
supplied. A simple example where the risk of using positional parameters is negligible, is
2121
`Test-Path $Path`.
2222

23+
## Configuration
24+
25+
```powershell
26+
Rules = @{
27+
AvoidUsingPositionalParameters = @{
28+
CommandAllowList = 'az', 'Join-Path'
29+
Enable = $true
30+
}
31+
}
32+
```
33+
34+
### Parameters
35+
36+
#### AvoidUsingPositionalParameters: string[] (Default value is 'az')
37+
38+
Commands to be excluded from this rule. `az` is excluded by default because starting with version 2.40.0 the entrypoint of the AZ CLI became an `az.ps1` script but this script does not have any named parameters and just passes them on using `$args` as is to the Python process that it starts, therefore it is still a CLI and not a PowerShell command.
39+
40+
#### Enable: bool (Default value is `$true`)
41+
42+
Enable or disable the rule during ScriptAnalyzer invocation.
43+
2344
## How
2445

2546
Use full parameter names when calling commands.

0 commit comments

Comments
 (0)