Skip to content

Commit 55d98f2

Browse files
authored
Add 'UseCorrectCasing' formatting rule for cmdlet/function name (#1117)
* first working prototype: Invoke-Formatter get-childitem * tweak for fully qualified cmdlets and add tests * add rule documentation and fix 3 tests as part of that * add resource strings * fix 1 test (severity) and docs test due to rule name * fix failing test using a workaround * fix tests * remove workaround in test that has been fixed upstream * cleanup and use helper method for getting cached cmdlet data
1 parent 012fd5e commit 55d98f2

12 files changed

+286
-7
lines changed

Engine/Formatter.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ public static string Format<TCmdlet>(
4040
"PSPlaceOpenBrace",
4141
"PSUseConsistentWhitespace",
4242
"PSUseConsistentIndentation",
43-
"PSAlignAssignmentStatement"
43+
"PSAlignAssignmentStatement",
44+
"PSUseCorrectCasing"
4445
};
4546

4647
var text = new EditableText(scriptDefinition);

Engine/Settings/CodeFormatting.psd1

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
'PSPlaceCloseBrace',
55
'PSUseConsistentWhitespace',
66
'PSUseConsistentIndentation',
7-
'PSAlignAssignmentStatement'
7+
'PSAlignAssignmentStatement',
8+
'PSUseCorrectCasing'
89
)
910

1011
Rules = @{
@@ -43,5 +44,9 @@
4344
Enable = $true
4445
CheckHashtable = $true
4546
}
47+
48+
PSUseCorrectCasing = @{
49+
Enable = $true
50+
}
4651
}
4752
}

Engine/Settings/CodeFormattingAllman.psd1

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
'PSPlaceCloseBrace',
55
'PSUseConsistentWhitespace',
66
'PSUseConsistentIndentation',
7-
'PSAlignAssignmentStatement'
7+
'PSAlignAssignmentStatement',
8+
'PSUseCorrectCasing'
89
)
910

1011
Rules = @{
@@ -43,5 +44,9 @@
4344
Enable = $true
4445
CheckHashtable = $true
4546
}
47+
48+
PSUseCorrectCasing = @{
49+
Enable = $true
50+
}
4651
}
4752
}

Engine/Settings/CodeFormattingOTBS.psd1

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
'PSPlaceCloseBrace',
55
'PSUseConsistentWhitespace',
66
'PSUseConsistentIndentation',
7-
'PSAlignAssignmentStatement'
7+
'PSAlignAssignmentStatement',
8+
'PSUseCorrectCasing'
89
)
910

1011
Rules = @{
@@ -43,5 +44,9 @@
4344
Enable = $true
4445
CheckHashtable = $true
4546
}
47+
48+
PSUseCorrectCasing = @{
49+
Enable = $true
50+
}
4651
}
4752
}

Engine/Settings/CodeFormattingStroustrup.psd1

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
'PSPlaceCloseBrace',
66
'PSUseConsistentWhitespace',
77
'PSUseConsistentIndentation',
8-
'PSAlignAssignmentStatement'
8+
'PSAlignAssignmentStatement',
9+
'PSUseCorrectCasing'
910
)
1011

1112
Rules = @{
@@ -44,5 +45,9 @@
4445
Enable = $true
4546
CheckHashtable = $true
4647
}
48+
49+
PSUseCorrectCasing = @{
50+
Enable = $true
51+
}
4752
}
4853
}

RuleDocumentation/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
|[UseApprovedVerbs](./UseApprovedVerbs.md) | Warning | |
4646
|[UseBOMForUnicodeEncodedFile](./UseBOMForUnicodeEncodedFile.md) | Warning | |
4747
|[UseCmdletCorrectly](./UseCmdletCorrectly.md) | Warning | |
48+
|[UseCorrectCasing](./UseCorrectCasing.md) | Information | |
4849
|[UseDeclaredVarsMoreThanAssignments](./UseDeclaredVarsMoreThanAssignments.md) | Warning | |
4950
|[UseLiteralInitializerForHashtable](./UseLiteralInitializerForHashtable.md) | Warning | |
5051
|[UseOutputTypeCorrectly](./UseOutputTypeCorrectly.md) | Information | |

RuleDocumentation/UseCorrectCasing.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# UseCorrectCasing
2+
3+
**Severity Level: Information**
4+
5+
## Description
6+
7+
This is a style/formatting rule. PowerShell is case insensitive where applicable. The casing of cmdlet names does not matter but this rule ensures that the casing matches for consistency and also because most cmdlets start with an upper case and using that improves readability to the human eye.
8+
9+
## How
10+
11+
Use exact casing of the cmdlet, e.g. `Invoke-Command`.
12+
13+
## Example
14+
15+
### Wrong
16+
17+
``` PowerShell
18+
invoke-command { 'foo' }
19+
}
20+
```
21+
22+
### Correct
23+
24+
``` PowerShell
25+
Invoke-Command { 'foo' }
26+
}
27+
```

Rules/Strings.Designer.cs

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Rules/Strings.resx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,4 +1056,16 @@
10561056
<data name="UseConsistentWhitespaceErrorSpaceBeforePipe" xml:space="preserve">
10571057
<value>Use space before pipe.</value>
10581058
</data>
1059+
<data name="UseCorrectCasingCommonName" xml:space="preserve">
1060+
<value>Use exact casing of cmdlet/function name.</value>
1061+
</data>
1062+
<data name="UseCorrectCasingDescription" xml:space="preserve">
1063+
<value>For better readability and consistency, use the exact casing of the cmdlet/function.</value>
1064+
</data>
1065+
<data name="UseCorrectCasingError" xml:space="preserve">
1066+
<value>Cmdlet/Function does not match its exact casing '{0}'.</value>
1067+
</data>
1068+
<data name="UseCorrectCasingName" xml:space="preserve">
1069+
<value>UseCorrectCasing</value>
1070+
</data>
10591071
</root>

Rules/UseCorrectCasing.cs

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Management.Automation.Language;
7+
using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic;
8+
#if !CORECLR
9+
using System.ComponentModel.Composition;
10+
#endif
11+
using System.Globalization;
12+
13+
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
14+
{
15+
/// <summary>
16+
/// UseCorrectCasing: Check if cmdlet is cased correctly.
17+
/// </summary>
18+
#if !CORECLR
19+
[Export(typeof(IScriptRule))]
20+
#endif
21+
public class UseCorrectCasing : ConfigurableRule
22+
{
23+
/// <summary>
24+
/// AnalyzeScript: Analyze the script to check if cmdlet alias is used.
25+
/// </summary>
26+
public override IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
27+
{
28+
if (ast == null) throw new ArgumentNullException(Strings.NullAstErrorMessage);
29+
30+
IEnumerable<Ast> commandAsts = ast.FindAll(testAst => testAst is CommandAst, true);
31+
32+
// Iterates all CommandAsts and check the command name.
33+
foreach (CommandAst commandAst in commandAsts)
34+
{
35+
string commandName = commandAst.GetCommandName();
36+
37+
// Handles the exception caused by commands like, {& $PLINK $args 2> $TempErrorFile}.
38+
// You can also review the remark section in following document,
39+
// MSDN: CommandAst.GetCommandName Method
40+
if (commandName == null)
41+
{
42+
continue;
43+
}
44+
45+
var commandInfo = Helper.Instance.GetCommandInfo(commandName);
46+
if (commandInfo == null)
47+
{
48+
continue;
49+
}
50+
51+
var shortName = commandInfo.Name;
52+
var fullyqualifiedName = $"{commandInfo.ModuleName}\\{shortName}";
53+
var isFullyQualified = commandName.Equals(fullyqualifiedName, StringComparison.OrdinalIgnoreCase);
54+
var correctlyCasedCommandName = isFullyQualified ? fullyqualifiedName : shortName;
55+
56+
if (!commandName.Equals(correctlyCasedCommandName, StringComparison.Ordinal))
57+
{
58+
yield return new DiagnosticRecord(
59+
string.Format(CultureInfo.CurrentCulture, Strings.UseCorrectCasingError, commandName, shortName),
60+
GetCommandExtent(commandAst),
61+
GetName(),
62+
DiagnosticSeverity.Warning,
63+
fileName,
64+
commandName,
65+
suggestedCorrections: GetCorrectionExtent(commandAst, correctlyCasedCommandName));
66+
}
67+
}
68+
}
69+
70+
/// <summary>
71+
/// For a command like "gci -path c:", returns the extent of "gci" in the command
72+
/// </summary>
73+
private IScriptExtent GetCommandExtent(CommandAst commandAst)
74+
{
75+
var cmdName = commandAst.GetCommandName();
76+
foreach (var cmdElement in commandAst.CommandElements)
77+
{
78+
var stringConstExpressinAst = cmdElement as StringConstantExpressionAst;
79+
if (stringConstExpressinAst != null)
80+
{
81+
if (stringConstExpressinAst.Value.Equals(cmdName))
82+
{
83+
return stringConstExpressinAst.Extent;
84+
}
85+
}
86+
}
87+
return commandAst.Extent;
88+
}
89+
90+
private IEnumerable<CorrectionExtent> GetCorrectionExtent(CommandAst commandAst, string correctlyCaseName)
91+
{
92+
var description = string.Format(
93+
CultureInfo.CurrentCulture,
94+
Strings.UseCorrectCasingDescription,
95+
correctlyCaseName,
96+
correctlyCaseName);
97+
var cmdExtent = GetCommandExtent(commandAst);
98+
var correction = new CorrectionExtent(
99+
cmdExtent.StartLineNumber,
100+
cmdExtent.EndLineNumber,
101+
cmdExtent.StartColumnNumber,
102+
cmdExtent.EndColumnNumber,
103+
correctlyCaseName,
104+
commandAst.Extent.File,
105+
description);
106+
yield return correction;
107+
}
108+
109+
/// <summary>
110+
/// GetName: Retrieves the name of this rule.
111+
/// </summary>
112+
/// <returns>The name of this rule</returns>
113+
public override string GetName()
114+
{
115+
return string.Format(CultureInfo.CurrentCulture, Strings.NameSpaceFormat, GetSourceName(), Strings.UseCorrectCasingName);
116+
}
117+
118+
/// <summary>
119+
/// GetCommonName: Retrieves the common name of this rule.
120+
/// </summary>
121+
/// <returns>The common name of this rule</returns>
122+
public override string GetCommonName()
123+
{
124+
return string.Format(CultureInfo.CurrentCulture, Strings.UseCorrectCasingCommonName);
125+
}
126+
127+
/// <summary>
128+
/// GetDescription: Retrieves the description of this rule.
129+
/// </summary>
130+
/// <returns>The description of this rule</returns>
131+
public override string GetDescription()
132+
{
133+
return string.Format(CultureInfo.CurrentCulture, Strings.UseCorrectCasingDescription);
134+
}
135+
136+
/// <summary>
137+
/// GetSourceType: Retrieves the type of the rule, Builtin, Managed or Module.
138+
/// </summary>
139+
public override SourceType GetSourceType()
140+
{
141+
return SourceType.Builtin;
142+
}
143+
144+
/// <summary>
145+
/// GetSeverity: Retrieves the severity of the rule: error, warning of information.
146+
/// </summary>
147+
/// <returns></returns>
148+
public override RuleSeverity GetSeverity()
149+
{
150+
return RuleSeverity.Information;
151+
}
152+
153+
/// <summary>
154+
/// GetSourceName: Retrieves the name of the module/assembly the rule is from.
155+
/// </summary>
156+
public override string GetSourceName()
157+
{
158+
return string.Format(CultureInfo.CurrentCulture, Strings.SourceName);
159+
}
160+
}
161+
}

Tests/Engine/GetScriptAnalyzerRule.tests.ps1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ Describe "Test Name parameters" {
5959

6060
It "get Rules with no parameters supplied" {
6161
$defaultRules = Get-ScriptAnalyzerRule
62-
$expectedNumRules = 58
62+
$expectedNumRules = 59
6363
if ((Test-PSEditionCoreClr) -or (Test-PSVersionV3) -or (Test-PSVersionV4))
6464
{
6565
# for PSv3 PSAvoidGlobalAliases is not shipped because
@@ -157,7 +157,7 @@ Describe "TestSeverity" {
157157

158158
It "filters rules based on multiple severity inputs"{
159159
$rules = Get-ScriptAnalyzerRule -Severity Error,Information
160-
$rules.Count | Should -Be 15
160+
$rules.Count | Should -Be 16
161161
}
162162

163163
It "takes lower case inputs" {
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
Describe "UseCorrectCasing" {
2+
It "corrects case of simple cmdlet" {
3+
Invoke-Formatter 'get-childitem' | Should -Be 'Get-ChildItem'
4+
}
5+
6+
It "corrects case of fully qualified cmdlet" {
7+
Invoke-Formatter 'Microsoft.PowerShell.management\get-childitem' | Should -Be 'Microsoft.PowerShell.Management\Get-ChildItem'
8+
}
9+
10+
It "corrects case of of cmdlet inside interpolated string" {
11+
Invoke-Formatter '"$(get-childitem)"' | Should -Be '"$(get-childitem)"'
12+
}
13+
14+
It "corrects case of script function" {
15+
function Invoke-DummyFunction
16+
{
17+
18+
}
19+
Invoke-Formatter 'invoke-dummyFunction' | Should -Be 'Invoke-DummyFunction'
20+
}
21+
}

0 commit comments

Comments
 (0)