Skip to content

Commit 1c4b704

Browse files
kalgizbergmeister
authored andcommitted
Trigger AvoidPositionalParameters rule for function defined and called inside a script. (#963)
* Trigger AvoidPositionalParameters rule for function defined and called inside a sript given as argument to Script Analyzer. * Get rid of debug print. * Tests fpr PSAvoidPOsitionalParameters rule. * Tests for AvoidPositionalParameters rule fixes. * Counting positional parameters fix. * Code cleanup for AvoidPositionalParameter rule fix. * Trigger AvoidPositionalParameters rule for function defined and called inside a sript given as argument to Script Analyzer. * Tests fpr PSAvoidPOsitionalParameters rule. * Counting positional parameters fix. * Tests fixes for AvoidPositionalParameters rule. * Code cleanup
1 parent 7218b2d commit 1c4b704

File tree

4 files changed

+54
-63
lines changed

4 files changed

+54
-63
lines changed

Engine/Helper.cs

+20-59
Original file line numberDiff line numberDiff line change
@@ -602,91 +602,52 @@ public bool HasSplattedVariable(CommandAst cmdAst)
602602
}
603603

604604
/// <summary>
605-
/// Given a commandast, checks whether positional parameters are used or not.
605+
/// Given a commandast, checks if the command is a Cmdlet.
606606
/// </summary>
607607
/// <param name="cmdAst"></param>
608-
/// <param name="moreThanThreePositional">only return true if more than three positional parameters are used</param>
609608
/// <returns></returns>
610-
public bool PositionalParameterUsed(CommandAst cmdAst, bool moreThanThreePositional = false)
611-
{
609+
public bool IsCmdlet(CommandAst cmdAst) {
612610
if (cmdAst == null)
613611
{
614612
return false;
615613
}
616614

617-
var commandInfo = GetCommandInfoLegacy(cmdAst.GetCommandName());
618-
if (commandInfo == null || (commandInfo.CommandType != System.Management.Automation.CommandTypes.Cmdlet))
619-
{
620-
return false;
621-
}
622-
623-
IEnumerable<ParameterMetadata> switchParams = null;
615+
var commandInfo = GetCommandInfo(cmdAst.GetCommandName());
616+
return (commandInfo != null && commandInfo.CommandType == System.Management.Automation.CommandTypes.Cmdlet);
617+
}
624618

619+
/// <summary>
620+
/// Given a commandast, checks whether positional parameters are used or not.
621+
/// </summary>
622+
/// <param name="cmdAst"></param>
623+
/// <param name="moreThanTwoPositional">only return true if more than two positional parameters are used</param>
624+
/// <returns></returns>
625+
public bool PositionalParameterUsed(CommandAst cmdAst, bool moreThanTwoPositional = false)
626+
{
625627
if (HasSplattedVariable(cmdAst))
626628
{
627629
return false;
628630
}
629631

630-
if (commandInfo != null && commandInfo.CommandType == System.Management.Automation.CommandTypes.Cmdlet)
631-
{
632-
try
633-
{
634-
switchParams = commandInfo.Parameters.Values.Where<ParameterMetadata>(pm => pm.SwitchParameter);
635-
}
636-
catch (Exception)
637-
{
638-
switchParams = null;
639-
}
640-
}
641-
642-
int parameters = 0;
643632
// Because of the way we count, we will also count the cmdlet as an argument so we have to -1
644-
int arguments = -1;
645-
646-
foreach (CommandElementAst ceAst in cmdAst.CommandElements)
647-
{
648-
var cmdParamAst = ceAst as CommandParameterAst;
649-
if (cmdParamAst != null)
650-
{
651-
// Skip if it's a switch parameter
652-
if (switchParams != null &&
653-
switchParams.Any(
654-
pm => String.Equals(
655-
pm.Name,
656-
cmdParamAst.ParameterName, StringComparison.OrdinalIgnoreCase)))
657-
{
658-
continue;
659-
}
660-
661-
parameters += 1;
662-
663-
if (cmdParamAst.Argument != null)
664-
{
665-
arguments += 1;
666-
}
633+
int argumentsWithoutProcedingParameters = 0;
667634

668-
}
669-
else
635+
var commandElementCollection = cmdAst.CommandElements;
636+
for (int i = 1; i < commandElementCollection.Count(); i++) {
637+
if (!(commandElementCollection[i] is CommandParameterAst) && !(commandElementCollection[i-1] is CommandParameterAst))
670638
{
671-
arguments += 1;
639+
argumentsWithoutProcedingParameters++;
672640
}
673641
}
674642

675643
// if not the first element in a pipeline, increase the number of arguments by 1
676644
PipelineAst parent = cmdAst.Parent as PipelineAst;
677-
678645
if (parent != null && parent.PipelineElements.Count > 1 && parent.PipelineElements[0] != cmdAst)
679646
{
680-
arguments += 1;
681-
}
682-
683-
// if we are only checking for 3 or more positional parameters, check that arguments < parameters + 3
684-
if (moreThanThreePositional && (arguments - parameters) < 3)
685-
{
686-
return false;
647+
argumentsWithoutProcedingParameters++;
687648
}
688649

689-
return arguments > parameters;
650+
return moreThanTwoPositional ? argumentsWithoutProcedingParameters > 2 : argumentsWithoutProcedingParameters > 0;
690651
}
691652

692653

Rules/AvoidPositionalParameters.cs

+15-2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,19 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
2727
{
2828
if (ast == null) throw new ArgumentNullException(Strings.NullAstErrorMessage);
2929

30+
// Find all function definitions in the script and add them to the set.
31+
IEnumerable<Ast> functionDefinitionAsts = ast.FindAll(testAst => testAst is FunctionDefinitionAst, true);
32+
HashSet<String> declaredFunctionNames = new HashSet<String>();
33+
34+
foreach (FunctionDefinitionAst functionDefinitionAst in functionDefinitionAsts)
35+
{
36+
if (string.IsNullOrEmpty(functionDefinitionAst.Name))
37+
{
38+
continue;
39+
}
40+
declaredFunctionNames.Add(functionDefinitionAst.Name);
41+
}
42+
3043
// Finds all CommandAsts.
3144
IEnumerable<Ast> foundAsts = ast.FindAll(testAst => testAst is CommandAst, true);
3245

@@ -39,8 +52,8 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
3952
// MSDN: CommandAst.GetCommandName Method
4053
if (cmdAst.GetCommandName() == null) continue;
4154

42-
if (Helper.Instance.GetCommandInfoLegacy(cmdAst.GetCommandName()) != null
43-
&& Helper.Instance.PositionalParameterUsed(cmdAst, true))
55+
if ((Helper.Instance.IsCmdlet(cmdAst) || declaredFunctionNames.Contains(cmdAst.GetCommandName())) &&
56+
(Helper.Instance.PositionalParameterUsed(cmdAst, true)))
4457
{
4558
PipelineAst parent = cmdAst.Parent as PipelineAst;
4659

Rules/UseCmdletCorrectly.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ private bool MandatoryParameterExists(CommandAst cmdAst)
129129
return true;
130130
}
131131

132-
if (mandParams.Count() == 0 || Helper.Instance.PositionalParameterUsed(cmdAst))
132+
if (mandParams.Count == 0 || (Helper.Instance.IsCmdlet(cmdAst) && Helper.Instance.PositionalParameterUsed(cmdAst)))
133133
{
134134
returnValue = true;
135135
}

Tests/Rules/AvoidPositionalParameters.tests.ps1

+18-1
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,26 @@ Describe "AvoidPositionalParameters" {
2121
It "returns no violations" {
2222
$noViolations.Count | Should -Be 0
2323
}
24-
24+
2525
It "returns no violations for DSC configuration" {
2626
$noViolationsDSC.Count | Should -Be 0
2727
}
2828
}
29+
30+
Context "Function defined and called in script, which has 3 or more positional parameters triggers rule." {
31+
It "returns avoid positional parameters violation" {
32+
$sb=
33+
{
34+
Function Foo {
35+
param(
36+
[Parameter(Mandatory=$true,Position=1)] $A,
37+
[Parameter(Position=2)]$B,
38+
[Parameter(Position=3)]$C)
39+
}
40+
Foo "a" "b" "c"}
41+
$warnings = Invoke-ScriptAnalyzer -ScriptDefinition "$sb"
42+
$warnings.Count | Should -Be 1
43+
$warnings.RuleName | Should -BeExactly $violationName
44+
}
45+
}
2946
}

0 commit comments

Comments
 (0)