Skip to content

Commit b8fec67

Browse files
New rule - ReviewUnusedParameter (#1382)
* New rule - ReviewUnusedParameter * Fix test failures in AvoidAssignmentToAutomaticVariable by excluding the new rule as the parameter is not used in those tests * Fix test failures in other tests when parameters are not used by excluding rule * make linq query stop earlier for better performance, avoid redundant ToList() and minor style tweaks (Linq variable naming and trailing whitespace trim) * format test file * Style fixes for PR #1382 * Fix merge error by re-adding original change * whitespace tidy * Use OrderBy to get a dictionary of the variable Count for better performance * case insensitive dictionary and add test case * re-trigger ci * case insensitive grouping Co-authored-by: Christoph Bergmeister [MVP] <c.bergmeister@gmail.com>
1 parent b0a67c7 commit b8fec67

9 files changed

+297
-5
lines changed

RuleDocumentation/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
|[ProvideCommentHelp](./ProvideCommentHelp.md) | Information | Yes |
4444
|[ReservedCmdletChar](./ReservedCmdletChar.md) | Error | |
4545
|[ReservedParams](./ReservedParams.md) | Error | |
46+
|[ReviewUnusedParameter](./ReviewUnusedParameter.md) | Warning | |
4647
|[ShouldProcess](./ShouldProcess.md) | Error | |
4748
|[UseApprovedVerbs](./UseApprovedVerbs.md) | Warning | |
4849
|[UseBOMForUnicodeEncodedFile](./UseBOMForUnicodeEncodedFile.md) | Warning | |
+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# ReviewUnusedParameter
2+
3+
**Severity Level: Warning**
4+
5+
## Description
6+
7+
This rule identifies parameters declared in a script, scriptblock, or function scope that have not been used in that scope.
8+
9+
## How
10+
11+
Consider removing the unused parameter.
12+
13+
## Example
14+
15+
### Wrong
16+
17+
``` PowerShell
18+
function Test-Parameter
19+
{
20+
Param (
21+
$Parameter1,
22+
23+
# this parameter is never called in the function
24+
$Parameter2
25+
)
26+
27+
Get-Something $Parameter1
28+
}
29+
```
30+
31+
### Correct
32+
33+
``` PowerShell
34+
function Test-Parameter
35+
{
36+
Param (
37+
$Parameter1,
38+
39+
# now this parameter is being called in the same scope
40+
$Parameter2
41+
)
42+
43+
Get-Something $Parameter1 $Parameter2
44+
}
45+
```

Rules/ReviewUnusedParameter.cs

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Management.Automation.Language;
8+
using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic;
9+
#if !CORECLR
10+
using System.ComponentModel.Composition;
11+
#endif
12+
using System.Globalization;
13+
14+
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
15+
{
16+
/// <summary>
17+
/// ReviewUnusedParameter: Check that all declared parameters are used in the script body.
18+
/// </summary>
19+
#if !CORECLR
20+
[Export(typeof(IScriptRule))]
21+
#endif
22+
public class ReviewUnusedParameter : IScriptRule
23+
{
24+
public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
25+
{
26+
if (ast == null)
27+
{
28+
throw new ArgumentNullException(Strings.NullAstErrorMessage);
29+
}
30+
31+
IEnumerable<Ast> scriptBlockAsts = ast.FindAll(oneAst => oneAst is ScriptBlockAst, true);
32+
if (scriptBlockAsts == null)
33+
{
34+
yield break;
35+
}
36+
37+
foreach (ScriptBlockAst scriptBlockAst in scriptBlockAsts)
38+
{
39+
// find all declared parameters
40+
IEnumerable<Ast> parameterAsts = scriptBlockAst.FindAll(oneAst => oneAst is ParameterAst, false);
41+
42+
// list all variables
43+
IDictionary<string, int> variableCount = scriptBlockAst.FindAll(oneAst => oneAst is VariableExpressionAst, false)
44+
.Select(variableExpressionAst => ((VariableExpressionAst)variableExpressionAst).VariablePath.UserPath)
45+
.GroupBy(variableName => variableName, StringComparer.OrdinalIgnoreCase)
46+
.ToDictionary(variableName => variableName.Key, variableName => variableName.Count(), StringComparer.OrdinalIgnoreCase);
47+
48+
// all bets are off if the script uses PSBoundParameters
49+
if (variableCount.ContainsKey("PSBoundParameters"))
50+
{
51+
continue;
52+
}
53+
54+
foreach (ParameterAst parameterAst in parameterAsts)
55+
{
56+
// there should be at least two usages of the variable since the parameter declaration counts as one
57+
variableCount.TryGetValue(parameterAst.Name.VariablePath.UserPath, out int variableUsageCount);
58+
if (variableUsageCount >= 2)
59+
{
60+
continue;
61+
}
62+
63+
yield return new DiagnosticRecord(
64+
string.Format(CultureInfo.CurrentCulture, Strings.ReviewUnusedParameterError, parameterAst.Name.VariablePath.UserPath),
65+
parameterAst.Name.Extent,
66+
GetName(),
67+
DiagnosticSeverity.Warning,
68+
fileName,
69+
parameterAst.Name.VariablePath.UserPath
70+
);
71+
}
72+
}
73+
}
74+
75+
/// <summary>
76+
/// GetName: Retrieves the name of this rule.
77+
/// </summary>
78+
/// <returns>The name of this rule</returns>
79+
public string GetName()
80+
{
81+
return string.Format(CultureInfo.CurrentCulture, Strings.NameSpaceFormat, GetSourceName(), Strings.ReviewUnusedParameterName);
82+
}
83+
84+
/// <summary>
85+
/// GetCommonName: Retrieves the common name of this rule.
86+
/// </summary>
87+
/// <returns>The common name of this rule</returns>
88+
public string GetCommonName()
89+
{
90+
return string.Format(CultureInfo.CurrentCulture, Strings.ReviewUnusedParameterCommonName);
91+
}
92+
93+
/// <summary>
94+
/// GetDescription: Retrieves the description of this rule.
95+
/// </summary>
96+
/// <returns>The description of this rule</returns>
97+
public string GetDescription()
98+
{
99+
return string.Format(CultureInfo.CurrentCulture, Strings.ReviewUnusedParameterDescription);
100+
}
101+
102+
/// <summary>
103+
/// GetSourceType: Retrieves the type of the rule, builtin, managed or module.
104+
/// </summary>
105+
public SourceType GetSourceType()
106+
{
107+
return SourceType.Builtin;
108+
}
109+
110+
/// <summary>
111+
/// GetSeverity: Retrieves the severity of the rule: error, warning of information.
112+
/// </summary>
113+
/// <returns></returns>
114+
public RuleSeverity GetSeverity()
115+
{
116+
return RuleSeverity.Warning;
117+
}
118+
119+
/// <summary>
120+
/// GetSourceName: Retrieves the module/assembly name the rule is from.
121+
/// </summary>
122+
public string GetSourceName()
123+
{
124+
return string.Format(CultureInfo.CurrentCulture, Strings.SourceName);
125+
}
126+
}
127+
}

Rules/Strings.Designer.cs

+36
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Rules/Strings.resx

+12
Original file line numberDiff line numberDiff line change
@@ -1107,4 +1107,16 @@
11071107
<data name="UseConsistentWhitespaceErrorSpaceBetweenParameter" xml:space="preserve">
11081108
<value>Use only 1 whitespace between parameter names or values.</value>
11091109
</data>
1110+
<data name="ReviewUnusedParameterCommonName" xml:space="preserve">
1111+
<value>ReviewUnusedParameter</value>
1112+
</data>
1113+
<data name="ReviewUnusedParameterDescription" xml:space="preserve">
1114+
<value>Ensure all parameters are used within the same script, scriptblock, or function where they are declared.</value>
1115+
</data>
1116+
<data name="ReviewUnusedParameterError" xml:space="preserve">
1117+
<value>The parameter '{0}' has been declared but not used. </value>
1118+
</data>
1119+
<data name="ReviewUnusedParameterName" xml:space="preserve">
1120+
<value>ReviewUnusedParameter</value>
1121+
</data>
11101122
</root>

Tests/Engine/GetScriptAnalyzerRule.tests.ps1

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ Describe "Test Name parameters" {
5959

6060
It "get Rules with no parameters supplied" {
6161
$defaultRules = Get-ScriptAnalyzerRule
62-
$expectedNumRules = 62
62+
$expectedNumRules = 63
6363
if ((Test-PSEditionCoreClr) -or (Test-PSVersionV3) -or (Test-PSVersionV4))
6464
{
6565
# for PSv3 PSAvoidGlobalAliases is not shipped because

Tests/Rules/AvoidAssignmentToAutomaticVariable.tests.ps1

+3-3
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Describe "AvoidAssignmentToAutomaticVariables" {
4343
It "Using Variable <VariableName> as parameter name produces warning of Severity error" -TestCases $testCases_ReadOnlyVariables {
4444
param ($VariableName, $ExpectedSeverity)
4545

46-
[System.Array] $warnings = Invoke-ScriptAnalyzer -ScriptDefinition "function foo{Param(`$$VariableName)}"
46+
[System.Array] $warnings = Invoke-ScriptAnalyzer -ScriptDefinition "function foo{Param(`$$VariableName)}" -ExcludeRule PSReviewUnusedParameter
4747
$warnings.Count | Should -Be 1
4848
$warnings.Severity | Should -Be $ExpectedSeverity
4949
$warnings.RuleName | Should -Be $ruleName
@@ -59,7 +59,7 @@ Describe "AvoidAssignmentToAutomaticVariables" {
5959
}
6060

6161
It "Does not flag parameter attributes" {
62-
[System.Array] $warnings = Invoke-ScriptAnalyzer -ScriptDefinition 'function foo{Param([Parameter(Mandatory=$true)]$param1)}'
62+
[System.Array] $warnings = Invoke-ScriptAnalyzer -ScriptDefinition 'function foo{Param([Parameter(Mandatory=$true)]$param1)}' -ExcludeRule PSReviewUnusedParameter
6363
$warnings.Count | Should -Be 0
6464
}
6565

@@ -86,7 +86,7 @@ Describe "AvoidAssignmentToAutomaticVariables" {
8686
Set-Variable -Name $VariableName -Value 'foo'
8787
continue
8888
}
89-
89+
9090
# Setting the $Error variable has the side effect of the ErrorVariable to contain only the exception message string, therefore exclude this case.
9191
# For the library test in WMF 4, assigning a value $PSEdition does not seem to throw an error, therefore this special case is excluded as well.
9292
if ($VariableName -ne 'Error' -and ($VariableName -ne 'PSEdition' -and $PSVersionTable.PSVersion.Major -ne 4))

Tests/Rules/AvoidPositionalParameters.tests.ps1

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ Describe "AvoidPositionalParameters" {
4545
[Parameter(Position=3)]$C)
4646
}
4747
Foo "a" "b" "c"}
48-
$warnings = Invoke-ScriptAnalyzer -ScriptDefinition "$sb"
48+
$warnings = Invoke-ScriptAnalyzer -ScriptDefinition "$sb" -ExcludeRule PSReviewUnusedParameter
4949
$warnings.Count | Should -Be 1
5050
$warnings.RuleName | Should -BeExactly $violationName
5151
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
Describe "ReviewUnusedParameter" {
2+
BeforeAll {
3+
$RuleName = 'PSReviewUnusedParameter'
4+
$RuleSeverity = "Warning"
5+
}
6+
7+
Context "When there are violations" {
8+
It "has 1 violation - function with 1 unused parameter" {
9+
$ScriptDefinition = 'function BadFunc1 { param ($Param1, $Param2) $Param1}'
10+
$Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName
11+
$Violations.Count | Should -Be 1
12+
}
13+
14+
It "has 2 violations - function with 2 unused parameters" {
15+
$ScriptDefinition = 'function BadFunc1 { param ($Param1, $Param2) }'
16+
$Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName
17+
$Violations.Count | Should -Be 2
18+
}
19+
20+
It "has 1 violation - scriptblock with 1 unused parameter" {
21+
$ScriptDefinition = '{ param ($Param1) }'
22+
$Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName
23+
$Violations.Count | Should -Be 1
24+
}
25+
26+
It "doesn't traverse scriptblock scope" {
27+
$ScriptDefinition = '{ param ($Param1) }; { $Param1 }'
28+
$Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName
29+
$Violations.Count | Should -Be 1
30+
}
31+
32+
It "violations have correct rule and severity" {
33+
$ScriptDefinition = 'function BadFunc1 { param ($Param1, $Param2) $Param1}'
34+
$Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName
35+
$Violations.Severity | Select-Object -Unique | Should -Be $RuleSeverity
36+
$Violations.RuleName | Select-Object -Unique | Should -Be $RuleName
37+
}
38+
}
39+
40+
Context "When there are no violations" {
41+
It "has no violations - function that uses all parameters" {
42+
$ScriptDefinition = 'function GoodFunc1 { param ($Param1, $Param2) $Param1; $Param2}'
43+
$Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName
44+
$Violations.Count | Should -Be 0
45+
}
46+
47+
It "has no violations - function with splatting" {
48+
$ScriptDefinition = 'function GoodFunc1 { param ($Param1) $Splat = @{InputObject = $Param1}}'
49+
$Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName
50+
$Violations.Count | Should -Be 0
51+
}
52+
53+
It "has no violations when using PSBoundParameters" {
54+
$ScriptDefinition = 'function Bound { param ($Param1) Get-Foo @PSBoundParameters }'
55+
$Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName
56+
$Violations.Count | Should -Be 0
57+
}
58+
59+
It "has no violations when parameter is called in child scope" -skip {
60+
$ScriptDefinition = 'function foo { param ($Param1) function Child { $Param1 } }'
61+
$Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName
62+
$Violations.Count | Should -Be 0
63+
}
64+
65+
It "has no violations when case of parameter and variable usage do not match" -skip {
66+
$ScriptDefinition = 'function foo { param ($Param1, $param2) $param1; $Param2}'
67+
$Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName
68+
$Violations.Count | Should -Be 0
69+
}
70+
}
71+
}

0 commit comments

Comments
 (0)