Skip to content

Warn when using FileRedirection operator inside if/while statements and improve new PossibleIncorrectUsageOfAssignmentOperator rule #881

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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4a0485c
first working prototype that warns against usage of the redirection o…
bergmeister Feb 8, 2018
2c0bc28
adapt error message strings and add tests
bergmeister Feb 8, 2018
29c7e34
remove old todo comment
bergmeister Feb 8, 2018
7310387
remove duplicated test case
bergmeister Feb 8, 2018
4078e41
syntax fix from last cleanup commits
bergmeister Feb 8, 2018
b7ad069
tweak extents in error message: for equal sign warnings, it will poin…
bergmeister Feb 12, 2018
8a48a62
Enhance check for assignment by rather checking if assigned variable …
bergmeister Feb 17, 2018
c9d510f
Merge branch 'development' of https://github.com/PowerShell/PSScriptA…
bergmeister Feb 20, 2018
fd339f6
tweak messages and readme
bergmeister Feb 20, 2018
5e8a8be
Merge branch 'WarnPipelineInsideIfStatement' of https://github.com/be…
bergmeister Feb 20, 2018
c74a746
Merge branch 'development' of https://github.com/PowerShell/PSScriptA…
bergmeister Feb 20, 2018
b467d10
update to pester v4 syntax
bergmeister Feb 20, 2018
594414f
Revert to not check assigned variable usage of RHS but add optional c…
bergmeister Feb 25, 2018
c4e242e
Minor fix resource variable naming
bergmeister Feb 25, 2018
f7ce114
uncommented accidental comment out of ipmo pssa in tests
bergmeister Feb 25, 2018
bc00213
do not exclude BinaryExpressionAst on RHS because this case is the ch…
bergmeister Feb 26, 2018
c6c4f4a
update test to test against clang suppression
bergmeister Feb 26, 2018
691f2ce
make clang suppression work again
bergmeister Feb 26, 2018
fe30f4d
reword warning text to use 'equality operator' instead of 'equals ope…
bergmeister Feb 27, 2018
78ef4bf
Always warn when LHS is $null
bergmeister Feb 28, 2018
675e3eb
Enhance PossibleIncorrectUsageOfAssignmentOperator rule to do the sam…
bergmeister Mar 15, 2018
ad008cd
tweak message to be more generic in terms of the conditional statement
bergmeister Mar 15, 2018
d042f37
Merge branch 'development' of https://github.com/PowerShell/PSScriptA…
bergmeister Mar 23, 2018
f530d75
Address PR comments
bergmeister Mar 23, 2018
ed95d84
Merge branch 'development' of https://github.com/PowerShell/PSScriptA…
bergmeister Mar 30, 2018
bdf39ac
Merge branch 'development' of https://github.com/PowerShell/PSScriptA…
bergmeister Apr 5, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions RuleDocumentation/PossibleIncorrectUsageOAssignmentOperator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# PossibleIncorrectUsageOfAssignmentOperator

**Severity Level: Information**

## Description

In many programming languages, the equality operator is denoted as `==` or `=` in many programming languages, but `PowerShell` uses `-eq`. Therefore it can easily happen that the wrong operator is used unintentionally and this rule catches a few special cases where the likelihood of that is quite high.

The rule looks for usages of `==` and `=` operators inside `if`, `else if`, `while` and `do-while` statements but it will not warn if any kind of command or expression is used at the right hand side as this is probably by design.

## Example

### Wrong

```` PowerShell
if ($a = $b)
{
...
}
````

```` PowerShell
if ($a == $b)
{

}
````

### Correct

```` PowerShell
if ($a -eq $b) # Compare $a with $b
{
...
}
````

```` PowerShell
if ($a = Get-Something) # Only execute action if command returns something and assign result to variable
Copy link
Contributor

Choose a reason for hiding this comment

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

Still even in this case we might want to only compare the variable with the function result. Shouldn't we give some warning message as well?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I know what you mean but it was a strong desire of the community to not see warnings when assignment is by design as this is a common use case. See referenced issue and there was also a discussion on the testing channel in Slack. I can sort of understand this fear/annoyance of people about false positives, especially given that one cannot suppress on a per-line basis in PSSA. I guess it is better to warn in most cases and help save the developer time rather than always warning and annoying the developer.

{
Do-SomethingWith $a
}
````

## Implicit suppresion using Clang style

There are some rare cases where assignment of variable inside an if statement is by design. Instead of suppression the rule, one can also signal that assignment was intentional by wrapping the expression in extra parenthesis. An exception for this is when `$null` is used on the LHS is used because there is no use case for this.

```` powershell
if (($shortVariableName = $SuperLongVariableName['SpecialItem']['AnotherItem']))
{
...
}
````

This file was deleted.

29 changes: 29 additions & 0 deletions RuleDocumentation/PossibleIncorrectUsageOfRedirectionOperator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# PossibleIncorrectUsageOfRedirectionOperator

**Severity Level: Information**

## Description

In many programming languages, the comparison operator for 'greater than' is `>` but `PowerShell` uses `-gt` for it and `-ge` (greater or equal) for `>=`. Therefore it can easily happen that the wrong operator is used unintentionally and this rule catches a few special cases where the likelihood of that is quite high.

The rule looks for usages of `>` or `>=` operators inside if, elseif, while and do-while statements because this is likely going to be unintentional usage.

## Example

### Wrong

```` PowerShell
if ($a > $b)
{
...
}
````

### Correct

```` PowerShell
if ($a -gt $b)
{
...
}
````
24 changes: 24 additions & 0 deletions Rules/ClangSuppresion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Management.Automation.Language;

namespace Microsoft.Windows.PowerShell.ScriptAnalyzer
{
/// <summary>
/// The idea behind clang suppresion style is to wrap a statement in extra parenthesis to implicitly suppress warnings of its content to signal that the offending operation was deliberate.
/// </summary>
internal static class ClangSuppresion
{
/// <summary>
/// The community requested an implicit suppression mechanism that follows the clang style where warnings are not issued if the expression is wrapped in extra parenthesis.
/// See here for details: https://github.com/Microsoft/clang/blob/349091162fcf2211a2e55cf81db934978e1c4f0c/test/SemaCXX/warn-assignment-condition.cpp#L15-L18
/// </summary>
/// <param name="scriptExtent"></param>
/// <returns></returns>
internal static bool ScriptExtendIsWrappedInParenthesis(IScriptExtent scriptExtent)
{
return scriptExtent.Text.StartsWith("(") && scriptExtent.Text.EndsWith(")");
}
}
}
86 changes: 61 additions & 25 deletions Rules/PossibleIncorrectUsageOfAssignmentOperator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,53 +13,89 @@
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
{
/// <summary>
/// PossibleIncorrectUsageOfAssignmentOperator: Warn if someone uses the '=' or '==' by accident in an if statement because in most cases that is not the intention.
/// PossibleIncorrectUsageOfAssignmentOperator: Warn if someone uses '>', '=' or '==' operators inside an if, elseif, while and do-while statement because in most cases that is not the intention.
/// The origin of this rule is that people often forget that operators change when switching between different languages such as C# and PowerShell.
/// </summary>
#if !CORECLR
[Export(typeof(IScriptRule))]
#endif
public class PossibleIncorrectUsageOfAssignmentOperator : AstVisitor, IScriptRule
{
/// <summary>
/// The idea is to get all AssignmentStatementAsts and then check if the parent is an IfStatementAst, which includes if, elseif and else statements.
/// The idea is to get all AssignmentStatementAsts and then check if the parent is an IfStatementAst/WhileStatementAst/DoWhileStatementAst,
/// which includes if, elseif, while and do-while statements.
/// </summary>
public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
{
if (ast == null) throw new ArgumentNullException(Strings.NullAstErrorMessage);

var whileStatementAsts = ast.FindAll(testAst => testAst is WhileStatementAst || testAst is DoWhileStatementAst, searchNestedScriptBlocks: true);
foreach (LoopStatementAst whileStatementAst in whileStatementAsts)
{
var diagnosticRecord = AnalyzePipelineBaseAst(whileStatementAst.Condition, fileName);
if (diagnosticRecord != null)
{
yield return diagnosticRecord;
}
}

var ifStatementAsts = ast.FindAll(testAst => testAst is IfStatementAst, searchNestedScriptBlocks: true);
foreach (IfStatementAst ifStatementAst in ifStatementAsts)
{
foreach (var clause in ifStatementAst.Clauses)
{
var assignmentStatementAst = clause.Item1.Find(testAst => testAst is AssignmentStatementAst, searchNestedScriptBlocks: false) as AssignmentStatementAst;
if (assignmentStatementAst != null)
var diagnosticRecord = AnalyzePipelineBaseAst(clause.Item1, fileName);
if (diagnosticRecord != null)
{
// Check if someone used '==', which can easily happen when the person is used to coding a lot in C#.
// In most cases, this will be a runtime error because PowerShell will look for a cmdlet name starting with '=', which is technically possible to define
if (assignmentStatementAst.Right.Extent.Text.StartsWith("="))
{
yield return new DiagnosticRecord(
Strings.PossibleIncorrectUsageOfAssignmentOperatorError, assignmentStatementAst.Extent,
GetName(), DiagnosticSeverity.Warning, fileName);
}
else
{
// If the right hand side contains a CommandAst at some point, then we do not want to warn
// because this could be intentional in cases like 'if ($a = Get-ChildItem){ }'
var commandAst = assignmentStatementAst.Right.Find(testAst => testAst is CommandAst, searchNestedScriptBlocks: true) as CommandAst;
if (commandAst == null)
{
yield return new DiagnosticRecord(
Strings.PossibleIncorrectUsageOfAssignmentOperatorError, assignmentStatementAst.Extent,
GetName(), DiagnosticSeverity.Information, fileName);
}
}
yield return diagnosticRecord;
}
}
}
}

private DiagnosticRecord AnalyzePipelineBaseAst(PipelineBaseAst pipelineBaseAst, string fileName)
{
var assignmentStatementAst = pipelineBaseAst.Find(testAst => testAst is AssignmentStatementAst, searchNestedScriptBlocks: false) as AssignmentStatementAst;
if (assignmentStatementAst == null)
{
return null;
}

// Check if someone used '==', which can easily happen when the person is used to coding a lot in C#.
// In most cases, this will be a runtime error because PowerShell will look for a cmdlet name starting with '=', which is technically possible to define
if (assignmentStatementAst.Right.Extent.Text.StartsWith("="))
{
return new DiagnosticRecord(
Strings.PossibleIncorrectUsageOfAssignmentOperatorError, assignmentStatementAst.ErrorPosition,
GetName(), DiagnosticSeverity.Warning, fileName);
}

// Check if LHS is $null and then always warn
if (assignmentStatementAst.Left is VariableExpressionAst variableExpressionAst)
{
if (variableExpressionAst.VariablePath.UserPath.Equals("null", StringComparison.OrdinalIgnoreCase))
{
return new DiagnosticRecord(
Strings.PossibleIncorrectUsageOfAssignmentOperatorError, assignmentStatementAst.ErrorPosition,
GetName(), DiagnosticSeverity.Warning, fileName);
}
}

// If the RHS contains a CommandAst at some point, then we do not want to warn because this could be intentional in cases like 'if ($a = Get-ChildItem){ }'
var commandAst = assignmentStatementAst.Right.Find(testAst => testAst is CommandAst, searchNestedScriptBlocks: true) as CommandAst;
// If the RHS contains an InvokeMemberExpressionAst, then we also do not want to warn because this could e.g. be 'if ($f = [System.IO.Path]::GetTempFileName()){ }'
var invokeMemberExpressionAst = assignmentStatementAst.Right.Find(testAst => testAst is ExpressionAst, searchNestedScriptBlocks: true) as InvokeMemberExpressionAst;
var doNotWarnBecauseImplicitClangStyleSuppressionWasUsed = ClangSuppresion.ScriptExtendIsWrappedInParenthesis(pipelineBaseAst.Extent);
if (commandAst == null && invokeMemberExpressionAst == null && !doNotWarnBecauseImplicitClangStyleSuppressionWasUsed)
{
return new DiagnosticRecord(
Strings.PossibleIncorrectUsageOfAssignmentOperatorError, assignmentStatementAst.ErrorPosition,
GetName(), DiagnosticSeverity.Information, fileName);
}

return null;
}

/// <summary>
/// GetName: Retrieves the name of this rule.
/// </summary>
Expand All @@ -84,7 +120,7 @@ public string GetCommonName()
/// <returns>The description of this rule</returns>
public string GetDescription()
{
return string.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingWriteHostDescription);
return string.Format(CultureInfo.CurrentCulture, Strings.PossibleIncorrectUsageOfAssignmentOperatorDescription);
}

/// <summary>
Expand Down
99 changes: 99 additions & 0 deletions Rules/PossibleIncorrectUsageOfRedirectionOperator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic;
using System;
using System.Collections.Generic;
#if !CORECLR
using System.ComponentModel.Composition;
#endif
using System.Management.Automation.Language;
using System.Globalization;

namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
{
/// <summary>
/// PossibleIncorrectUsageOfRedirectionOperator: Warn if someone uses '>' or '>=' inside an if, elseif, while or do-while statement because in most cases that is not the intention.
/// The origin of this rule is that people often forget that operators change when switching between different languages such as C# and PowerShell.
/// </summary>
#if !CORECLR
[Export(typeof(IScriptRule))]
#endif
public class PossibleIncorrectUsageOfRedirectionOperator : AstVisitor, IScriptRule
{
/// <summary>
/// The idea is to get all FileRedirectionAst inside all IfStatementAst clauses.
/// </summary>
public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
{
if (ast == null) throw new ArgumentNullException(Strings.NullAstErrorMessage);

var ifStatementAsts = ast.FindAll(testAst => testAst is IfStatementAst, searchNestedScriptBlocks: true);
foreach (IfStatementAst ifStatementAst in ifStatementAsts)
{
foreach (var clause in ifStatementAst.Clauses)
{
var fileRedirectionAst = clause.Item1.Find(testAst => testAst is FileRedirectionAst, searchNestedScriptBlocks: false) as FileRedirectionAst;
if (fileRedirectionAst != null)
{
yield return new DiagnosticRecord(
Strings.PossibleIncorrectUsageOfRedirectionOperatorError, fileRedirectionAst.Extent,
GetName(), DiagnosticSeverity.Warning, fileName);
}
}
}
}

/// <summary>
/// GetName: Retrieves the name of this rule.
/// </summary>
/// <returns>The name of this rule</returns>
public string GetName()
{
return string.Format(CultureInfo.CurrentCulture, Strings.NameSpaceFormat, GetSourceName(), Strings.PossibleIncorrectUsageOfRedirectionOperatorName);
}

/// <summary>
/// GetCommonName: Retrieves the common name of this rule.
/// </summary>
/// <returns>The common name of this rule</returns>
public string GetCommonName()
{
return string.Format(CultureInfo.CurrentCulture, Strings.PossibleIncorrectUsageOfRedirectionOperatorCommonName);
}

/// <summary>
/// GetDescription: Retrieves the description of this rule.
/// </summary>
/// <returns>The description of this rule</returns>
public string GetDescription()
{
return string.Format(CultureInfo.CurrentCulture, Strings.PossibleIncorrectUsageOfRedirectionOperatorDescription);
}

/// <summary>
/// GetSourceType: Retrieves the type of the rule: builtin, managed or module.
/// </summary>
public SourceType GetSourceType()
{
return SourceType.Builtin;
}

/// <summary>
/// GetSeverity: Retrieves the severity of the rule: error, warning of information.
/// </summary>
/// <returns></returns>
public RuleSeverity GetSeverity()
{
return RuleSeverity.Warning;
}

/// <summary>
/// GetSourceName: Retrieves the module/assembly name the rule is from.
/// </summary>
public string GetSourceName()
{
return string.Format(CultureInfo.CurrentCulture, Strings.SourceName);
}
}
}
Loading