Skip to content

Commit fd926e3

Browse files
committed
Added the AvoidExclamationPointOperator rule to warn about the use of the negation operator !. Fixes PowerShell#1826
1 parent 5ba330a commit fd926e3

8 files changed

+314
-3
lines changed

Engine/Formatter.cs

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public static string Format<TCmdlet>(
4646
"PSAvoidUsingCmdletAliases",
4747
"PSAvoidUsingDoubleQuotesForConstantString",
4848
"PSAvoidSemicolonsAsLineTerminators",
49+
"PSAvoidExclamationPointOperator",
4950
};
5051

5152
var text = new EditableText(scriptDefinition);
+151
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Copyright (c) Microsoft Corporation.
2+
//
3+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
4+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
5+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
6+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
7+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
8+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
9+
// THE SOFTWARE.
10+
11+
using System;
12+
using System.Collections.Generic;
13+
#if !CORECLR
14+
using System.ComponentModel.Composition;
15+
#endif
16+
using System.Globalization;
17+
using System.Linq;
18+
using System.Management.Automation;
19+
using System.Management.Automation.Language;
20+
using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic;
21+
22+
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
23+
{
24+
/// <summary>
25+
/// AvoidExclamationPointOperator: Checks for use of the exclamation point operator
26+
/// </summary>
27+
#if !CORECLR
28+
[Export(typeof(IScriptRule))]
29+
#endif
30+
public class AvoidExclamationPointOperator : ConfigurableRule
31+
{
32+
33+
/// <summary>
34+
/// Construct an object of AvoidExclamationPointOperator type.
35+
/// </summary>
36+
public AvoidExclamationPointOperator() {
37+
Enable = false;
38+
}
39+
40+
/// <summary>
41+
/// Analyzes the given ast to find the [violation]
42+
/// </summary>
43+
/// <param name="ast">AST to be analyzed. This should be non-null</param>
44+
/// <param name="fileName">Name of file that corresponds to the input AST.</param>
45+
/// <returns>A an enumerable type containing the violations</returns>
46+
public override IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
47+
{
48+
if (ast == null) throw new ArgumentNullException(Strings.NullAstErrorMessage);
49+
50+
var diagnosticRecords = new List<DiagnosticRecord>();
51+
52+
IEnumerable<Ast> foundAsts = ast.FindAll(testAst => testAst is UnaryExpressionAst, true);
53+
if (foundAsts != null) {
54+
var CorrectionDescription = Strings.AvoidExclamationPointOperatorCorrectionDescription;
55+
foreach (UnaryExpressionAst foundAst in foundAsts) {
56+
if (foundAst.TokenKind == TokenKind.Exclaim) {
57+
// If the exclaim is not followed by a space, add one
58+
var replaceWith = "-not";
59+
if (foundAst.Child != null && foundAst.Child.Extent.StartColumnNumber == foundAst.Extent.StartColumnNumber + 1) {
60+
replaceWith = "-not ";
61+
}
62+
var corrections = new List<CorrectionExtent> {
63+
new CorrectionExtent(
64+
foundAst.Extent.StartLineNumber,
65+
foundAst.Extent.EndLineNumber,
66+
foundAst.Extent.StartColumnNumber,
67+
foundAst.Extent.StartColumnNumber + 1,
68+
replaceWith,
69+
fileName,
70+
CorrectionDescription
71+
)
72+
};
73+
diagnosticRecords.Add(new DiagnosticRecord(
74+
string.Format(
75+
CultureInfo.CurrentCulture,
76+
Strings.AvoidExclamationPointOperatorError
77+
),
78+
foundAst.Extent,
79+
GetName(),
80+
GetDiagnosticSeverity(),
81+
fileName,
82+
suggestedCorrections: corrections
83+
));
84+
}
85+
}
86+
}
87+
return diagnosticRecords;
88+
}
89+
90+
/// <summary>
91+
/// Retrieves the common name of this rule.
92+
/// </summary>
93+
public override string GetCommonName()
94+
{
95+
return string.Format(CultureInfo.CurrentCulture, Strings.AvoidExclamationPointOperatorCommonName);
96+
}
97+
98+
/// <summary>
99+
/// Retrieves the description of this rule.
100+
/// </summary>
101+
public override string GetDescription()
102+
{
103+
return string.Format(CultureInfo.CurrentCulture, Strings.AvoidExclamationPointOperatorDescription);
104+
}
105+
106+
/// <summary>
107+
/// Retrieves the name of this rule.
108+
/// </summary>
109+
public override string GetName()
110+
{
111+
return string.Format(
112+
CultureInfo.CurrentCulture,
113+
Strings.NameSpaceFormat,
114+
GetSourceName(),
115+
Strings.AvoidExclamationPointOperatorName);
116+
}
117+
118+
/// <summary>
119+
/// Retrieves the severity of the rule: error, warning or information.
120+
/// </summary>
121+
public override RuleSeverity GetSeverity()
122+
{
123+
return RuleSeverity.Warning;
124+
}
125+
126+
/// <summary>
127+
/// Gets the severity of the returned diagnostic record: error, warning, or information.
128+
/// </summary>
129+
/// <returns></returns>
130+
public DiagnosticSeverity GetDiagnosticSeverity()
131+
{
132+
return DiagnosticSeverity.Warning;
133+
}
134+
135+
/// <summary>
136+
/// Retrieves the name of the module/assembly the rule is from.
137+
/// </summary>
138+
public override string GetSourceName()
139+
{
140+
return string.Format(CultureInfo.CurrentCulture, Strings.SourceName);
141+
}
142+
143+
/// <summary>
144+
/// Retrieves the type of the rule, Builtin, Managed or Module.
145+
/// </summary>
146+
public override SourceType GetSourceType()
147+
{
148+
return SourceType.Builtin;
149+
}
150+
}
151+
}

Rules/Strings.Designer.cs

+46-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Rules/Strings.resx

+16-1
Original file line numberDiff line numberDiff line change
@@ -1191,4 +1191,19 @@
11911191
<data name="UseCorrectCasingParameterError" xml:space="preserve">
11921192
<value>Parameter '{0}' of function/cmdlet '{1}' does not match its exact casing '{2}'.</value>
11931193
</data>
1194-
</root>
1194+
<data name="AvoidExclamationPointOperatorName" xml:space="preserve">
1195+
<value>AvoidExclamationPointOperator</value>
1196+
</data>
1197+
<data name="AvoidExclamationPointOperatorCommonName" xml:space="preserve">
1198+
<value>Avoid exclamation point operator</value>
1199+
</data>
1200+
<data name="AvoidExclamationPointOperatorDescription" xml:space="preserve">
1201+
<value>The negation operator ! should not be used. Use -not instead.</value>
1202+
</data>
1203+
<data name="AvoidExclamationPointOperatorError" xml:space="preserve">
1204+
<value>Avoid using the ! operator</value>
1205+
</data>
1206+
<data name="AvoidExclamationPointOperatorCorrectionDescription" xml:space="preserve">
1207+
<value>Replace ! with -not</value>
1208+
</data>
1209+
</root>

Tests/Engine/GetScriptAnalyzerRule.tests.ps1

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ Describe "Test Name parameters" {
6363

6464
It "get Rules with no parameters supplied" {
6565
$defaultRules = Get-ScriptAnalyzerRule
66-
$expectedNumRules = 68
66+
$expectedNumRules = 69
6767
if ($PSVersionTable.PSVersion.Major -le 4)
6868
{
6969
# for PSv3 PSAvoidGlobalAliases is not shipped because
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
BeforeAll {
5+
$ruleName = "PSAvoidExclamationPointOperator"
6+
7+
$ruleSettings = @{
8+
Enable = $true
9+
}
10+
$settings = @{
11+
IncludeRules = @($ruleName)
12+
Rules = @{ $ruleName = $ruleSettings }
13+
}
14+
}
15+
16+
Describe "AvoidExclamationPointOperator" {
17+
Context "When the rule is not enabled explicitly" {
18+
It "Should not find violations" {
19+
$def = '!$true'
20+
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $def
21+
$violations.Count | Should -Be 0
22+
}
23+
}
24+
25+
Context "Given a line with the exclamation point operator" {
26+
It "Should find one violation" {
27+
$def = '!$true'
28+
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings
29+
$violations.Count | Should -Be 1
30+
}
31+
}
32+
33+
Context "Given a line with the exclamation point operator" {
34+
It "Should replace the exclamation point operator with the -not operator" {
35+
$def = '!$true'
36+
$expected = '-not $true'
37+
Invoke-Formatter -ScriptDefinition $def -Settings $settings | Should -Be $expected
38+
}
39+
}
40+
Context "Given a line with the exclamation point operator followed by a space" {
41+
It "Should replace the exclamation point operator without adding an additional space" {
42+
$def = '! $true'
43+
$expected = '-not $true'
44+
Invoke-Formatter -ScriptDefinition $def -Settings $settings | Should -Be $expected
45+
}
46+
}
47+
Context "Given a line with a string containing an exclamation point" {
48+
It "Should not replace it" {
49+
$def = '$MyVar = "Should not replace!"'
50+
$expected = '$MyVar = "Should not replace!"'
51+
Invoke-Formatter -ScriptDefinition $def -Settings $settings | Should -Be $expected
52+
}
53+
}
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
description: Avoid exclamation point operator
3+
ms.custom: PSSA v1.21.0
4+
ms.date: 06/14/2023
5+
ms.topic: reference
6+
title: AvoidExclamationPointOperator
7+
---
8+
# AvoidExclamationPointOperator
9+
**Severity Level: Warning**
10+
11+
## Description
12+
13+
The negation operator ! should not be used. Use -not instead.
14+
15+
**Note**: This rule is not enabled by default. The user needs to enable it through settings.
16+
17+
## How to Fix
18+
19+
## Example
20+
### Wrong:
21+
```PowerShell
22+
$MyVar = !$true
23+
```
24+
25+
### Correct:
26+
```PowerShell
27+
$MyVar = -not $true
28+
```
29+
30+
## Configuration
31+
32+
```powershell
33+
Rules = @{
34+
PSAvoidExclamationPointOperator = @{
35+
Enable = $true
36+
}
37+
}
38+
```
39+
40+
### Parameters
41+
42+
#### Enable: bool (Default value is `$false`)
43+
44+
Enable or disable the rule during ScriptAnalyzer invocation.

docs/Rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ The PSScriptAnalyzer contains the following rule definitions.
1515
| [AvoidAssignmentToAutomaticVariable](./AvoidAssignmentToAutomaticVariable.md) | Warning | Yes | |
1616
| [AvoidDefaultValueForMandatoryParameter](./AvoidDefaultValueForMandatoryParameter.md) | Warning | Yes | |
1717
| [AvoidDefaultValueSwitchParameter](./AvoidDefaultValueSwitchParameter.md) | Warning | Yes | |
18+
| [AvoidExclamationPointOperator](./AvoidExclamationPointOperator.md) | Warning | No | |
1819
| [AvoidGlobalAliases<sup>1</sup>](./AvoidGlobalAliases.md) | Warning | Yes | |
1920
| [AvoidGlobalFunctions](./AvoidGlobalFunctions.md) | Warning | Yes | |
2021
| [AvoidGlobalVars](./AvoidGlobalVars.md) | Warning | Yes | |

0 commit comments

Comments
 (0)