Skip to content

Commit 6e892b9

Browse files
bergmeisterJamesWTruher
authored andcommitted
Warn when using FileRedirection operator inside if/while statements and improve new PossibleIncorrectUsageOfAssignmentOperator rule (#881)
* first working prototype that warns against usage of the redirection operator inside if statements. TODO: rename rule, adapt documentation and error message strings * adapt error message strings and add tests * remove old todo comment * remove duplicated test case * syntax fix from last cleanup commits * tweak extents in error message: for equal sign warnings, it will point to the equal sign (instead of the RHS only) and fore file redirections it will be the redirection ast (e.g. '> $b') * Enhance check for assignment by rather checking if assigned variable is used in assignment block to avoid false positives for assignments: check if variable on LHS is used in statement block update documentation and change level to warning since we are now much more certain that a violation. is happening Commits: * prototype of detecting variable usage on LHS to avoid false positives * simplify and reduce nesting * fix regression by continue statements * fix last regression * add simple test case for new improvemeent * finish it off and adapt tests * update docs * minor style update * fix typo * fix test due to warning level change * tweak messages and readme * update to pester v4 syntax * Revert to not check assigned variable usage of RHS but add optional clang suppression, split rule and enhance assignment operator rule to not warn for more special cases on the RHS * Minor fix resource variable naming * uncommented accidental comment out of ipmo pssa in tests * do not exclude BinaryExpressionAst on RHS because this case is the chance that it is by design is much smaller, therefore rather having some false positives is preferred * update test to test against clang suppression * make clang suppression work again * reword warning text to use 'equality operator' instead of 'equals operator' * Always warn when LHS is $null * Enhance PossibleIncorrectUsageOfAssignmentOperator rule to do the same analysis for while and do-while statements as well, which is the exact same use case as if statements * tweak message to be more generic in terms of the conditional statement * Address PR comments
1 parent ac707f8 commit 6e892b9

11 files changed

+421
-48
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# PossibleIncorrectUsageOfAssignmentOperator
2+
3+
**Severity Level: Information**
4+
5+
## Description
6+
7+
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.
8+
9+
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.
10+
11+
## Example
12+
13+
### Wrong
14+
15+
```` PowerShell
16+
if ($a = $b)
17+
{
18+
...
19+
}
20+
````
21+
22+
```` PowerShell
23+
if ($a == $b)
24+
{
25+
26+
}
27+
````
28+
29+
### Correct
30+
31+
```` PowerShell
32+
if ($a -eq $b) # Compare $a with $b
33+
{
34+
...
35+
}
36+
````
37+
38+
```` PowerShell
39+
if ($a = Get-Something) # Only execute action if command returns something and assign result to variable
40+
{
41+
Do-SomethingWith $a
42+
}
43+
````
44+
45+
## Implicit suppresion using Clang style
46+
47+
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.
48+
49+
```` powershell
50+
if (($shortVariableName = $SuperLongVariableName['SpecialItem']['AnotherItem']))
51+
{
52+
...
53+
}
54+
````

RuleDocumentation/PossibleIncorrectUsageOfAssignmentOperator.md

Lines changed: 0 additions & 7 deletions
This file was deleted.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# PossibleIncorrectUsageOfRedirectionOperator
2+
3+
**Severity Level: Information**
4+
5+
## Description
6+
7+
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.
8+
9+
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.
10+
11+
## Example
12+
13+
### Wrong
14+
15+
```` PowerShell
16+
if ($a > $b)
17+
{
18+
...
19+
}
20+
````
21+
22+
### Correct
23+
24+
```` PowerShell
25+
if ($a -gt $b)
26+
{
27+
...
28+
}
29+
````

Rules/ClangSuppresion.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System.Management.Automation.Language;
5+
6+
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer
7+
{
8+
/// <summary>
9+
/// 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.
10+
/// </summary>
11+
internal static class ClangSuppresion
12+
{
13+
/// <summary>
14+
/// 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.
15+
/// See here for details: https://github.com/Microsoft/clang/blob/349091162fcf2211a2e55cf81db934978e1c4f0c/test/SemaCXX/warn-assignment-condition.cpp#L15-L18
16+
/// </summary>
17+
/// <param name="scriptExtent"></param>
18+
/// <returns></returns>
19+
internal static bool ScriptExtendIsWrappedInParenthesis(IScriptExtent scriptExtent)
20+
{
21+
return scriptExtent.Text.StartsWith("(") && scriptExtent.Text.EndsWith(")");
22+
}
23+
}
24+
}

Rules/PossibleIncorrectUsageOfAssignmentOperator.cs

Lines changed: 61 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,53 +13,89 @@
1313
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
1414
{
1515
/// <summary>
16-
/// PossibleIncorrectUsageOfAssignmentOperator: Warn if someone uses the '=' or '==' by accident in an if statement because in most cases that is not the intention.
16+
/// 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.
17+
/// The origin of this rule is that people often forget that operators change when switching between different languages such as C# and PowerShell.
1718
/// </summary>
1819
#if !CORECLR
1920
[Export(typeof(IScriptRule))]
2021
#endif
2122
public class PossibleIncorrectUsageOfAssignmentOperator : AstVisitor, IScriptRule
2223
{
2324
/// <summary>
24-
/// The idea is to get all AssignmentStatementAsts and then check if the parent is an IfStatementAst, which includes if, elseif and else statements.
25+
/// The idea is to get all AssignmentStatementAsts and then check if the parent is an IfStatementAst/WhileStatementAst/DoWhileStatementAst,
26+
/// which includes if, elseif, while and do-while statements.
2527
/// </summary>
2628
public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
2729
{
2830
if (ast == null) throw new ArgumentNullException(Strings.NullAstErrorMessage);
2931

32+
var whileStatementAsts = ast.FindAll(testAst => testAst is WhileStatementAst || testAst is DoWhileStatementAst, searchNestedScriptBlocks: true);
33+
foreach (LoopStatementAst whileStatementAst in whileStatementAsts)
34+
{
35+
var diagnosticRecord = AnalyzePipelineBaseAst(whileStatementAst.Condition, fileName);
36+
if (diagnosticRecord != null)
37+
{
38+
yield return diagnosticRecord;
39+
}
40+
}
41+
3042
var ifStatementAsts = ast.FindAll(testAst => testAst is IfStatementAst, searchNestedScriptBlocks: true);
3143
foreach (IfStatementAst ifStatementAst in ifStatementAsts)
3244
{
3345
foreach (var clause in ifStatementAst.Clauses)
3446
{
35-
var assignmentStatementAst = clause.Item1.Find(testAst => testAst is AssignmentStatementAst, searchNestedScriptBlocks: false) as AssignmentStatementAst;
36-
if (assignmentStatementAst != null)
47+
var diagnosticRecord = AnalyzePipelineBaseAst(clause.Item1, fileName);
48+
if (diagnosticRecord != null)
3749
{
38-
// Check if someone used '==', which can easily happen when the person is used to coding a lot in C#.
39-
// 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
40-
if (assignmentStatementAst.Right.Extent.Text.StartsWith("="))
41-
{
42-
yield return new DiagnosticRecord(
43-
Strings.PossibleIncorrectUsageOfAssignmentOperatorError, assignmentStatementAst.Extent,
44-
GetName(), DiagnosticSeverity.Warning, fileName);
45-
}
46-
else
47-
{
48-
// If the right hand side contains a CommandAst at some point, then we do not want to warn
49-
// because this could be intentional in cases like 'if ($a = Get-ChildItem){ }'
50-
var commandAst = assignmentStatementAst.Right.Find(testAst => testAst is CommandAst, searchNestedScriptBlocks: true) as CommandAst;
51-
if (commandAst == null)
52-
{
53-
yield return new DiagnosticRecord(
54-
Strings.PossibleIncorrectUsageOfAssignmentOperatorError, assignmentStatementAst.Extent,
55-
GetName(), DiagnosticSeverity.Information, fileName);
56-
}
57-
}
50+
yield return diagnosticRecord;
5851
}
5952
}
6053
}
6154
}
6255

56+
private DiagnosticRecord AnalyzePipelineBaseAst(PipelineBaseAst pipelineBaseAst, string fileName)
57+
{
58+
var assignmentStatementAst = pipelineBaseAst.Find(testAst => testAst is AssignmentStatementAst, searchNestedScriptBlocks: false) as AssignmentStatementAst;
59+
if (assignmentStatementAst == null)
60+
{
61+
return null;
62+
}
63+
64+
// Check if someone used '==', which can easily happen when the person is used to coding a lot in C#.
65+
// 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
66+
if (assignmentStatementAst.Right.Extent.Text.StartsWith("="))
67+
{
68+
return new DiagnosticRecord(
69+
Strings.PossibleIncorrectUsageOfAssignmentOperatorError, assignmentStatementAst.ErrorPosition,
70+
GetName(), DiagnosticSeverity.Warning, fileName);
71+
}
72+
73+
// Check if LHS is $null and then always warn
74+
if (assignmentStatementAst.Left is VariableExpressionAst variableExpressionAst)
75+
{
76+
if (variableExpressionAst.VariablePath.UserPath.Equals("null", StringComparison.OrdinalIgnoreCase))
77+
{
78+
return new DiagnosticRecord(
79+
Strings.PossibleIncorrectUsageOfAssignmentOperatorError, assignmentStatementAst.ErrorPosition,
80+
GetName(), DiagnosticSeverity.Warning, fileName);
81+
}
82+
}
83+
84+
// 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){ }'
85+
var commandAst = assignmentStatementAst.Right.Find(testAst => testAst is CommandAst, searchNestedScriptBlocks: true) as CommandAst;
86+
// 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()){ }'
87+
var invokeMemberExpressionAst = assignmentStatementAst.Right.Find(testAst => testAst is ExpressionAst, searchNestedScriptBlocks: true) as InvokeMemberExpressionAst;
88+
var doNotWarnBecauseImplicitClangStyleSuppressionWasUsed = ClangSuppresion.ScriptExtendIsWrappedInParenthesis(pipelineBaseAst.Extent);
89+
if (commandAst == null && invokeMemberExpressionAst == null && !doNotWarnBecauseImplicitClangStyleSuppressionWasUsed)
90+
{
91+
return new DiagnosticRecord(
92+
Strings.PossibleIncorrectUsageOfAssignmentOperatorError, assignmentStatementAst.ErrorPosition,
93+
GetName(), DiagnosticSeverity.Information, fileName);
94+
}
95+
96+
return null;
97+
}
98+
6399
/// <summary>
64100
/// GetName: Retrieves the name of this rule.
65101
/// </summary>
@@ -84,7 +120,7 @@ public string GetCommonName()
84120
/// <returns>The description of this rule</returns>
85121
public string GetDescription()
86122
{
87-
return string.Format(CultureInfo.CurrentCulture, Strings.AvoidUsingWriteHostDescription);
123+
return string.Format(CultureInfo.CurrentCulture, Strings.PossibleIncorrectUsageOfAssignmentOperatorDescription);
88124
}
89125

90126
/// <summary>
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic;
5+
using System;
6+
using System.Collections.Generic;
7+
#if !CORECLR
8+
using System.ComponentModel.Composition;
9+
#endif
10+
using System.Management.Automation.Language;
11+
using System.Globalization;
12+
13+
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
14+
{
15+
/// <summary>
16+
/// PossibleIncorrectUsageOfRedirectionOperator: Warn if someone uses '>' or '>=' inside an if, elseif, while or do-while statement because in most cases that is not the intention.
17+
/// The origin of this rule is that people often forget that operators change when switching between different languages such as C# and PowerShell.
18+
/// </summary>
19+
#if !CORECLR
20+
[Export(typeof(IScriptRule))]
21+
#endif
22+
public class PossibleIncorrectUsageOfRedirectionOperator : AstVisitor, IScriptRule
23+
{
24+
/// <summary>
25+
/// The idea is to get all FileRedirectionAst inside all IfStatementAst clauses.
26+
/// </summary>
27+
public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
28+
{
29+
if (ast == null) throw new ArgumentNullException(Strings.NullAstErrorMessage);
30+
31+
var ifStatementAsts = ast.FindAll(testAst => testAst is IfStatementAst, searchNestedScriptBlocks: true);
32+
foreach (IfStatementAst ifStatementAst in ifStatementAsts)
33+
{
34+
foreach (var clause in ifStatementAst.Clauses)
35+
{
36+
var fileRedirectionAst = clause.Item1.Find(testAst => testAst is FileRedirectionAst, searchNestedScriptBlocks: false) as FileRedirectionAst;
37+
if (fileRedirectionAst != null)
38+
{
39+
yield return new DiagnosticRecord(
40+
Strings.PossibleIncorrectUsageOfRedirectionOperatorError, fileRedirectionAst.Extent,
41+
GetName(), DiagnosticSeverity.Warning, fileName);
42+
}
43+
}
44+
}
45+
}
46+
47+
/// <summary>
48+
/// GetName: Retrieves the name of this rule.
49+
/// </summary>
50+
/// <returns>The name of this rule</returns>
51+
public string GetName()
52+
{
53+
return string.Format(CultureInfo.CurrentCulture, Strings.NameSpaceFormat, GetSourceName(), Strings.PossibleIncorrectUsageOfRedirectionOperatorName);
54+
}
55+
56+
/// <summary>
57+
/// GetCommonName: Retrieves the common name of this rule.
58+
/// </summary>
59+
/// <returns>The common name of this rule</returns>
60+
public string GetCommonName()
61+
{
62+
return string.Format(CultureInfo.CurrentCulture, Strings.PossibleIncorrectUsageOfRedirectionOperatorCommonName);
63+
}
64+
65+
/// <summary>
66+
/// GetDescription: Retrieves the description of this rule.
67+
/// </summary>
68+
/// <returns>The description of this rule</returns>
69+
public string GetDescription()
70+
{
71+
return string.Format(CultureInfo.CurrentCulture, Strings.PossibleIncorrectUsageOfRedirectionOperatorDescription);
72+
}
73+
74+
/// <summary>
75+
/// GetSourceType: Retrieves the type of the rule: builtin, managed or module.
76+
/// </summary>
77+
public SourceType GetSourceType()
78+
{
79+
return SourceType.Builtin;
80+
}
81+
82+
/// <summary>
83+
/// GetSeverity: Retrieves the severity of the rule: error, warning of information.
84+
/// </summary>
85+
/// <returns></returns>
86+
public RuleSeverity GetSeverity()
87+
{
88+
return RuleSeverity.Warning;
89+
}
90+
91+
/// <summary>
92+
/// GetSourceName: Retrieves the module/assembly name the rule is from.
93+
/// </summary>
94+
public string GetSourceName()
95+
{
96+
return string.Format(CultureInfo.CurrentCulture, Strings.SourceName);
97+
}
98+
}
99+
}

0 commit comments

Comments
 (0)