diff --git a/Rules/ReviewUnusedParameter.cs b/Rules/ReviewUnusedParameter.cs index 5a06500d8..ffaaa1334 100644 --- a/Rules/ReviewUnusedParameter.cs +++ b/Rules/ReviewUnusedParameter.cs @@ -36,6 +36,12 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) foreach (ScriptBlockAst scriptBlockAst in scriptBlockAsts) { + // bail out if PS bound parameter used. + if (scriptBlockAst.Find(IsBoundParametersReference, searchNestedScriptBlocks: false) != null) + { + continue; + } + // find all declared parameters IEnumerable parameterAsts = scriptBlockAst.FindAll(oneAst => oneAst is ParameterAst, false); @@ -45,12 +51,6 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) .GroupBy(variableName => variableName, StringComparer.OrdinalIgnoreCase) .ToDictionary(variableName => variableName.Key, variableName => variableName.Count(), StringComparer.OrdinalIgnoreCase); - // all bets are off if the script uses PSBoundParameters - if (variableCount.ContainsKey("PSBoundParameters")) - { - continue; - } - foreach (ParameterAst parameterAst in parameterAsts) { // there should be at least two usages of the variable since the parameter declaration counts as one @@ -72,6 +72,47 @@ public IEnumerable AnalyzeScript(Ast ast, string fileName) } } + /// + /// Checks for PS bound parameter reference. + /// + /// AST to be analyzed. This should be non-null + /// Boolean true indicating that given AST has PS bound parameter reference, otherwise false + private static bool IsBoundParametersReference(Ast ast) + { + // $PSBoundParameters + if (ast is VariableExpressionAst variableAst + && variableAst.VariablePath.UserPath.Equals("PSBoundParameters", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + if (ast is MemberExpressionAst memberAst + && memberAst.Member is StringConstantExpressionAst memberStringAst + && memberStringAst.Value.Equals("BoundParameters", StringComparison.OrdinalIgnoreCase)) + { + // $MyInvocation.BoundParameters + if (memberAst.Expression is VariableExpressionAst veAst + && veAst.VariablePath.UserPath.Equals("MyInvocation", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + // $PSCmdlet.MyInvocation.BoundParameters + if (memberAst.Expression is MemberExpressionAst meAstNested) + { + if (meAstNested.Expression is VariableExpressionAst veAstNested + && veAstNested.VariablePath.UserPath.Equals("PSCmdlet", StringComparison.OrdinalIgnoreCase) + && meAstNested.Member is StringConstantExpressionAst sceAstNested + && sceAstNested.Value.Equals("MyInvocation", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + } + + return false; + } + /// /// GetName: Retrieves the name of this rule. /// diff --git a/Tests/Rules/ReviewUnusedParameter.tests.ps1 b/Tests/Rules/ReviewUnusedParameter.tests.ps1 index f5d6004cc..85d27d731 100644 --- a/Tests/Rules/ReviewUnusedParameter.tests.ps1 +++ b/Tests/Rules/ReviewUnusedParameter.tests.ps1 @@ -56,6 +56,18 @@ Describe "ReviewUnusedParameter" { $Violations.Count | Should -Be 0 } + It "has no violations when using MyInvocation.BoundParameters" { + $ScriptDefinition = 'function Bound { param ($Param1) $splat = $MyInvocation.BoundParameters; Get-Foo @splat }' + $Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName + $Violations.Count | Should -Be 0 + } + + It "has no violations when using PSCmdlet.MyInvocation.BoundParameters" { + $ScriptDefinition = 'function Bound { param ($Param1) $splat = $PSCmdlet.MyInvocation.BoundParameters; Get-Foo @splat }' + $Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName + $Violations.Count | Should -Be 0 + } + It "has no violations when parameter is called in child scope" -skip { $ScriptDefinition = 'function foo { param ($Param1) function Child { $Param1 } }' $Violations = Invoke-ScriptAnalyzer -ScriptDefinition $ScriptDefinition -IncludeRule $RuleName