Skip to content

Trigger AvoidPositionalParameters rule for function defined and called inside a script. #963

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
79 changes: 20 additions & 59 deletions Engine/Helper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -602,91 +602,52 @@ public bool HasSplattedVariable(CommandAst cmdAst)
}

/// <summary>
/// Given a commandast, checks whether positional parameters are used or not.
/// Given a commandast, checks if the command is a Cmdlet.
/// </summary>
/// <param name="cmdAst"></param>
/// <param name="moreThanThreePositional">only return true if more than three positional parameters are used</param>
/// <returns></returns>
public bool PositionalParameterUsed(CommandAst cmdAst, bool moreThanThreePositional = false)
{
public bool IsCmdlet(CommandAst cmdAst) {
if (cmdAst == null)
{
return false;
}

var commandInfo = GetCommandInfoLegacy(cmdAst.GetCommandName());
if (commandInfo == null || (commandInfo.CommandType != System.Management.Automation.CommandTypes.Cmdlet))
{
return false;
}

IEnumerable<ParameterMetadata> switchParams = null;
var commandInfo = GetCommandInfo(cmdAst.GetCommandName());
return (commandInfo != null && commandInfo.CommandType == System.Management.Automation.CommandTypes.Cmdlet);
}

/// <summary>
/// Given a commandast, checks whether positional parameters are used or not.
/// </summary>
/// <param name="cmdAst"></param>
/// <param name="moreThanTwoPositional">only return true if more than two positional parameters are used</param>
/// <returns></returns>
public bool PositionalParameterUsed(CommandAst cmdAst, bool moreThanTwoPositional = false)
{
if (HasSplattedVariable(cmdAst))
{
return false;
}

if (commandInfo != null && commandInfo.CommandType == System.Management.Automation.CommandTypes.Cmdlet)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in this function we probably should care only about the number of arguments which have no corresponding parameters, analyzing switch parameters seems unnecessary in here...

{
try
{
switchParams = commandInfo.Parameters.Values.Where<ParameterMetadata>(pm => pm.SwitchParameter);
}
catch (Exception)
{
switchParams = null;
}
}

int parameters = 0;
// Because of the way we count, we will also count the cmdlet as an argument so we have to -1
int arguments = -1;

foreach (CommandElementAst ceAst in cmdAst.CommandElements)
{
var cmdParamAst = ceAst as CommandParameterAst;
if (cmdParamAst != null)
{
// Skip if it's a switch parameter
if (switchParams != null &&
switchParams.Any(
pm => String.Equals(
pm.Name,
cmdParamAst.ParameterName, StringComparison.OrdinalIgnoreCase)))
{
continue;
}

parameters += 1;

if (cmdParamAst.Argument != null)
{
arguments += 1;
}
int argumentsWithoutProcedingParameters = 0;

}
else
var commandElementCollection = cmdAst.CommandElements;
for (int i = 1; i < commandElementCollection.Count(); i++) {
if (!(commandElementCollection[i] is CommandParameterAst) && !(commandElementCollection[i-1] is CommandParameterAst))
{
arguments += 1;
argumentsWithoutProcedingParameters++;
}
}

// if not the first element in a pipeline, increase the number of arguments by 1
PipelineAst parent = cmdAst.Parent as PipelineAst;

if (parent != null && parent.PipelineElements.Count > 1 && parent.PipelineElements[0] != cmdAst)
{
arguments += 1;
}

// if we are only checking for 3 or more positional parameters, check that arguments < parameters + 3
if (moreThanThreePositional && (arguments - parameters) < 3)
{
return false;
argumentsWithoutProcedingParameters++;
}

return arguments > parameters;
return moreThanTwoPositional ? argumentsWithoutProcedingParameters > 2 : argumentsWithoutProcedingParameters > 0;
}


Expand Down
17 changes: 15 additions & 2 deletions Rules/AvoidPositionalParameters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
{
if (ast == null) throw new ArgumentNullException(Strings.NullAstErrorMessage);

// Find all function definitions in the script and add them to the set.
IEnumerable<Ast> functionDefinitionAsts = ast.FindAll(testAst => testAst is FunctionDefinitionAst, true);
HashSet<String> declaredFunctionNames = new HashSet<String>();

foreach (FunctionDefinitionAst functionDefinitionAst in functionDefinitionAsts)
{
if (string.IsNullOrEmpty(functionDefinitionAst.Name))
{
continue;
}
declaredFunctionNames.Add(functionDefinitionAst.Name);
}

// Finds all CommandAsts.
IEnumerable<Ast> foundAsts = ast.FindAll(testAst => testAst is CommandAst, true);

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

if (Helper.Instance.GetCommandInfoLegacy(cmdAst.GetCommandName()) != null
&& Helper.Instance.PositionalParameterUsed(cmdAst, true))
if ((Helper.Instance.IsCmdlet(cmdAst) || declaredFunctionNames.Contains(cmdAst.GetCommandName())) &&
(Helper.Instance.PositionalParameterUsed(cmdAst, true)))
{
PipelineAst parent = cmdAst.Parent as PipelineAst;

Expand Down
2 changes: 1 addition & 1 deletion Rules/UseCmdletCorrectly.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ private bool MandatoryParameterExists(CommandAst cmdAst)
return true;
}

if (mandParams.Count() == 0 || Helper.Instance.PositionalParameterUsed(cmdAst))
if (mandParams.Count == 0 || (Helper.Instance.IsCmdlet(cmdAst) && Helper.Instance.PositionalParameterUsed(cmdAst)))
{
returnValue = true;
}
Expand Down
19 changes: 18 additions & 1 deletion Tests/Rules/AvoidPositionalParameters.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,26 @@ Describe "AvoidPositionalParameters" {
It "returns no violations" {
$noViolations.Count | Should -Be 0
}

It "returns no violations for DSC configuration" {
$noViolationsDSC.Count | Should -Be 0
}
}

Context "Function defined and called in script, which has 3 or more positional parameters triggers rule." {
It "returns avoid positional parameters violation" {
$sb=
{
Function Foo {
param(
[Parameter(Mandatory=$true,Position=1)] $A,
[Parameter(Position=2)]$B,
[Parameter(Position=3)]$C)
}
Foo "a" "b" "c"}
$warnings = Invoke-ScriptAnalyzer -ScriptDefinition "$sb"
$warnings.Count | Should -Be 1
$warnings.RuleName | Should -BeExactly $violationName
}
}
}