diff --git a/Rules/ReviewUnusedParameter.cs b/Rules/ReviewUnusedParameter.cs index f13584fed..9b727e7fc 100644 --- a/Rules/ReviewUnusedParameter.cs +++ b/Rules/ReviewUnusedParameter.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Management.Automation.Language; using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic; +using Microsoft.Windows.PowerShell.ScriptAnalyzer.Extensions; #if !CORECLR using System.ComponentModel.Composition; #endif @@ -97,11 +98,40 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) // find all declared parameters IEnumerable parameterAsts = scriptBlockAst.FindAll(oneAst => oneAst is ParameterAst, false); + // does the scriptblock have a process block where either $PSItem or $_ is referenced + bool hasProcessBlockWithPSItemOrUnderscore = false; + if (scriptBlockAst.ProcessBlock != null) + { + IDictionary processBlockVariableCount = GetVariableCount(scriptBlockAst.ProcessBlock); + processBlockVariableCount.TryGetValue("_", out int underscoreVariableCount); + processBlockVariableCount.TryGetValue("psitem", out int psitemVariableCount); + if (underscoreVariableCount > 0 || psitemVariableCount > 0) + { + hasProcessBlockWithPSItemOrUnderscore = true; + } + } + // list all variables IDictionary variableCount = GetVariableCount(scriptBlockAst); foreach (ParameterAst parameterAst in parameterAsts) { + // Check if the parameter has the ValueFromPipeline attribute + NamedAttributeArgumentAst valueFromPipeline = (NamedAttributeArgumentAst)parameterAst.Find( + valFromPipelineAst => valFromPipelineAst is NamedAttributeArgumentAst namedAttrib && string.Equals( + namedAttrib.ArgumentName, "ValueFromPipeline", + StringComparison.OrdinalIgnoreCase + ), + false + ); + // If the parameter has the ValueFromPipeline attribute and the scriptblock has a process block with + // $_ or $PSItem usage, then the parameter is considered used + if (valueFromPipeline != null && valueFromPipeline.GetValue() && hasProcessBlockWithPSItemOrUnderscore) + + { + continue; + } + // there should be at least two usages of the variable since the parameter declaration counts as one variableCount.TryGetValue(parameterAst.Name.VariablePath.UserPath, out int variableUsageCount); if (variableUsageCount >= 2) @@ -220,7 +250,7 @@ public string GetSourceName() /// The scriptblock ast to scan /// Previously generated data. New findings are added to any existing dictionary if present /// a dictionary including all variables in the scriptblock and their count - IDictionary GetVariableCount(ScriptBlockAst ast, Dictionary data = null) + IDictionary GetVariableCount(Ast ast, Dictionary data = null) { Dictionary content = data; if (null == data) diff --git a/Tests/Rules/ReviewUnusedParameter.tests.ps1 b/Tests/Rules/ReviewUnusedParameter.tests.ps1 index 59d8b160d..9e4202dcf 100644 --- a/Tests/Rules/ReviewUnusedParameter.tests.ps1 +++ b/Tests/Rules/ReviewUnusedParameter.tests.ps1 @@ -20,6 +20,30 @@ Describe "ReviewUnusedParameter" { $Violations.Count | Should -Be 2 } + It "has 1 violation - function with 1 parameter with ValueFromPipeline set to false and `$_ usage inside process block" { + $ScriptDefinition = 'function BadFunc1 { param ([Parameter(ValueFromPipeline = $false)] $Param1) process {$_}}' + $Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName + $Violations.Count | Should -Be 1 + } + + It "has 1 violation - function with 1 parameter with ValueFromPipeline set to false and `$PSItem usage inside process block" { + $ScriptDefinition = 'function BadFunc1 { param ([Parameter(ValueFromPipeline = $false)] $Param1) process {$PSItem}}' + $Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName + $Violations.Count | Should -Be 1 + } + + It "has 1 violation - function with 1 parameter with ValueFromPipeline set to true and `$_ usage outside process block" { + $ScriptDefinition = 'function BadFunc1 { param ([Parameter(ValueFromPipeline = $true)] $Param1) $_}' + $Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName + $Violations.Count | Should -Be 1 + } + + It "has 1 violation - function with 1 parameter with ValueFromPipeline set to true and `$PSItem usage outside process block" { + $ScriptDefinition = 'function BadFunc1 { param ([Parameter(ValueFromPipeline = $true)] $Param1) $PSItem}' + $Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName + $Violations.Count | Should -Be 1 + } + It "has 1 violation - scriptblock with 1 unused parameter" { $ScriptDefinition = '{ param ($Param1) }' $Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName @@ -59,6 +83,30 @@ Describe "ReviewUnusedParameter" { $Violations.Count | Should -Be 0 } + It "has no violation - function with 1 parameter with ValueFromPipeline explictly set to true and `$_ usage inside process block" { + $ScriptDefinition = 'function BadFunc1 { param ([Parameter(ValueFromPipeline = $true)] $Param1) process {$_}}' + $Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName + $Violations.Count | Should -Be 0 + } + + It "has no violation - function with 1 parameter with ValueFromPipeline explictly set to true and `$PSItem usage inside process block" { + $ScriptDefinition = 'function BadFunc1 { param ([Parameter(ValueFromPipeline = $true)] $Param1) process {$PSItem}}' + $Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName + $Violations.Count | Should -Be 0 + } + + It "has no violation - function with 1 parameter with ValueFromPipeline implicitly set to true and `$_ usage inside process block" { + $ScriptDefinition = 'function BadFunc1 { param ([Parameter(ValueFromPipeline)] $Param1) process{$_}}' + $Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName + $Violations.Count | Should -Be 0 + } + + It "has no violation - function with 1 parameter with ValueFromPipeline implicitly set to true and `$PSItem usage inside process block" { + $ScriptDefinition = 'function BadFunc1 { param ([Parameter(ValueFromPipeline)] $Param1) process{$PSItem}}' + $Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName + $Violations.Count | Should -Be 0 + } + It "has no violations when using PSBoundParameters" { $ScriptDefinition = 'function Bound { param ($Param1) Get-Foo @PSBoundParameters }' $Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName