Skip to content

Commit 651ed5d

Browse files
committed
Added tests, fixed typos, changed default PowerShellVersion behavior
1 parent 8aa60a9 commit 651ed5d

5 files changed

+172
-49
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# AvoidOverwritingBuiltInCmdlets
2+
3+
**Severity Level: Warning**
4+
5+
## Description
6+
7+
This rule flags cmdlets that are available in a given Edition/Version of PowerShell on a given Operating System which are overwritten by a function declaration. It works by comparing function declarations against a set of whitelists which ship with PSScriptAnalyzer. They can be found at `/path/to/PSScriptAnalyzerModule/Settings`. These files are of the form, `PSEDITION-PSVERSION-OS.json` where `PSEDITION` can be either `Core` or `Desktop`, `OS` can be either `Windows`, `Linux` or `MacOS`, and `Version` is the PowerShell version.
8+
9+
## Configuration
10+
11+
To enable the rule to check if your script is compatible on PowerShell Core on Windows, put the following your settings file.
12+
13+
14+
```PowerShell
15+
@{
16+
'Rules' = @{
17+
'PSAvoidOverwritingBuiltInCmdlets' = @{
18+
'PowerShellVersion' = @("core-6.1.0-windows")
19+
}
20+
}
21+
}
22+
```
23+
24+
### Parameters
25+
26+
#### PowerShellVersion
27+
28+
The parameter `PowerShellVersion` is a list that contain any of the following
29+
30+
- desktop-2.0-windows
31+
- desktop-3.0-windows
32+
- desktop-4.0-windows (taken from Windows Server 2012R2)
33+
- desktop-5.1.14393.206-windows
34+
- core-6.1.0-windows (taken from Windows 10 - 1803)
35+
- core-6.1.0-linux (taken from Ubuntu 18.04)
36+
- core-6.1.0-linux-arm (taken from Raspbian)
37+
- core-6.1.0-macos
38+
39+
**Note**: The default value for `PowerShellVersion` is `"core-6.1.0-windows"` if PowerShell 6 or later is installed, and `"desktop-5.1.14393.206-windows"` if it is not.
40+
41+
Usually, patched versions of PowerShell have the same cmdlet data, therefore only settings of major and minor versions of PowerShell are supplied. One can also create a custom settings file as well with the [New-CommandDataFile.ps1](https://github.com/PowerShell/PSScriptAnalyzer/blob/development/Utils/New-CommandDataFile.ps1) script and use it by placing the created `JSON` into the `Settings` folder of the `PSScriptAnalyzer` module installation folder, then the `PowerShellVersion` parameter is just its file name (that can also be changed if desired).
42+
Note that the `core-6.0.2-*` files were removed in PSScriptAnalyzer 1.18 since PowerShell 6.0 reached it's end of life.

RuleDocumentation/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
|[AvoidGlobalFunctions](./AvoidGlobalFunctions.md) | Warning | |
1313
|[AvoidGlobalVars](./AvoidGlobalVars.md) | Warning | |
1414
|[AvoidInvokingEmptyMembers](./AvoidInvokingEmptyMembers.md) | Warning | |
15+
|[AvoidOverwritingBuiltInCmdlets](./AvoidOverwritingBuiltInCmdlets.md) | Warning | |
1516
|[AvoidNullOrEmptyHelpMessageAttribute](./AvoidNullOrEmptyHelpMessageAttribute.md) | Warning | |
1617
|[AvoidShouldContinueWithoutForce](./AvoidShouldContinueWithoutForce.md) | Warning | |
1718
|[AvoidUsingCmdletAliases](./AvoidUsingCmdletAliases.md) | Warning | Yes |

Rules/AvoidOverwritingBuiltInCmdlets.cs

Lines changed: 42 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Diagnostics;
67
#if !CORECLR
78
using System.ComponentModel.Composition;
89
#endif
@@ -18,7 +19,7 @@
1819
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
1920
{
2021
/// <summary>
21-
/// AvoidLongLines: Checks if a script overwrites a cmdlet that comes with PowerShell
22+
/// AvoidOverwritingBuiltInCmdlets: Checks if a script overwrites a cmdlet that comes with PowerShell
2223
/// </summary>
2324
#if !CORECLR
2425
[Export(typeof(IScriptRule))]
@@ -28,6 +29,12 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
2829
/// </summary>
2930
public class AvoidOverwritingBuiltInCmdlets : ConfigurableRule
3031
{
32+
[ConfigurableRuleProperty(defaultValue: "")]
33+
public string[] PowerShellVersion { get; set; }
34+
private Dictionary<string, HashSet<string>> cmdletMap;
35+
private bool initialized;
36+
37+
3138
/// <summary>
3239
/// Construct an object of AvoidOverwritingBuiltInCmdlets type.
3340
/// </summary>
@@ -36,14 +43,31 @@ public AvoidOverwritingBuiltInCmdlets() : base()
3643
initialized = false;
3744
cmdletMap = new Dictionary<string, HashSet<string>>();
3845
Enable = true; // Enable rule by default
39-
}
40-
41-
42-
[ConfigurableRuleProperty(defaultValue: "core-6.1.0-windows")]
43-
public string PowerShellVersion { get; set; }
46+
47+
string versionTest = string.Join("", PowerShellVersion);
4448

45-
private Dictionary<string, HashSet<string>> cmdletMap;
46-
private bool initialized;
49+
if (versionTest != "core-6.1.0-windows" && versionTest != "desktop-5.1.14393.206-windows")
50+
{
51+
// PowerShellVersion is not already set to one of the acceptable defaults
52+
// Try launching `pwsh -v` to see if PowerShell 6+ is installed, and use those cmdlets
53+
// as a default. If 6+ is not installed this will throw an error, which when caught will
54+
// allow us to use the PowerShell 5 cmdlets as a default.
55+
try
56+
{
57+
var testProcess = new Process();
58+
testProcess.StartInfo.FileName = "pwsh";
59+
testProcess.StartInfo.Arguments = "-v";
60+
testProcess.StartInfo.CreateNoWindow = true;
61+
testProcess.StartInfo.UseShellExecute = false;
62+
testProcess.Start();
63+
PowerShellVersion = new[] {"core-6.1.0-windows"};
64+
}
65+
catch
66+
{
67+
PowerShellVersion = new[] {"desktop-5.1.14393.206-windows"};
68+
}
69+
}
70+
}
4771

4872

4973
/// <summary>
@@ -118,29 +142,32 @@ private DiagnosticRecord CreateDiagnosticRecord(string FunctionName, string PSVe
118142

119143
private void Initialize()
120144
{
121-
var psVerList = PowerShellVersion.Split(',').ToList();
145+
var psVerList = PowerShellVersion;
122146

123147
string settingsPath = Settings.GetShippedSettingsDirectory();
124148

125-
if (settingsPath == null || !ContainsReferenceFile(settingsPath))
149+
foreach (string reference in psVerList)
126150
{
127-
return;
151+
if (settingsPath == null || !ContainsReferenceFile(settingsPath, reference))
152+
{
153+
return;
154+
}
128155
}
129-
156+
130157
ProcessDirectory(settingsPath, psVerList);
131158

132159
if (cmdletMap.Keys.Count != psVerList.Count())
133160
{
134161
return;
135162
}
136-
163+
137164
initialized = true;
138165
}
139166

140167

141-
private bool ContainsReferenceFile(string directory)
168+
private bool ContainsReferenceFile(string directory, string reference)
142169
{
143-
return File.Exists(Path.Combine(directory, PowerShellVersion + ".json"));
170+
return File.Exists(Path.Combine(directory, reference + ".json"));
144171
}
145172

146173

@@ -193,39 +220,6 @@ private HashSet<string> GetCmdletsFromData(dynamic deserializedObject)
193220
}
194221

195222

196-
private bool IsValidPlatformString(string fileNameWithoutExt)
197-
{
198-
string psedition, psversion, os;
199-
return GetVersionInfoFromPlatformString(
200-
fileNameWithoutExt,
201-
out psedition,
202-
out psversion,
203-
out os);
204-
}
205-
206-
207-
private bool GetVersionInfoFromPlatformString(
208-
string fileName,
209-
out string psedition,
210-
out string psversion,
211-
out string os)
212-
{
213-
psedition = null;
214-
psversion = null;
215-
os = null;
216-
const string pattern = @"^(?<psedition>core|desktop)-(?<psversion>[\S]+)-(?<os>windows|linux|macos)$";
217-
var match = Regex.Match(fileName, pattern, RegexOptions.IgnoreCase);
218-
if (match == Match.Empty)
219-
{
220-
return false;
221-
}
222-
psedition = match.Groups["psedition"].Value;
223-
psversion = match.Groups["psversion"].Value;
224-
os = match.Groups["os"].Value;
225-
return true;
226-
}
227-
228-
229223
/// <summary>
230224
/// Retrieves the common name of this rule.
231225
/// </summary>

Tests/Engine/GetScriptAnalyzerRule.tests.ps1

Lines changed: 1 addition & 1 deletion
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 = 59
62+
$expectedNumRules = 60
6363
if ((Test-PSEditionCoreClr) -or (Test-PSVersionV3) -or (Test-PSVersionV4))
6464
{
6565
# for PSv3 PSAvoidGlobalAliases is not shipped because
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
$ruleName = "PSAvoidOverwritingBuiltInCmdlets"
2+
3+
$ruleSettingsWindows = @{$ruleName = @{PowerShellVersion = @('desktop-5.1.14393.206-windows')}}
4+
$ruleSettingsCore = @{$ruleName = @{PowerShellVersion = @('core-6.1.0-windows')}}
5+
$ruleSettingsBoth = @{$ruleName = @{PowerShellVersion = @('core-6.1.0-windows', 'desktop-5.1.14393.206-windows')}}
6+
7+
$settings = @{
8+
IncludeRules = @($ruleName)
9+
}
10+
11+
# Get-Something is not a built in cmdlet on any platform and should never be flagged
12+
# Get-ChildItem is available on all versions of PowerShell and should always be flagged
13+
# Get-Clipboard is available on PowerShell 5 but not 6 and should be flagged conditionally
14+
$scriptDefinition = @"
15+
function Get-Something {
16+
Write-Output "Get-Something"
17+
}
18+
19+
function Get-ChildItem {
20+
Write-Output "Get-ChildItem"
21+
}
22+
23+
function Get-Clipboard {
24+
Write-Output "Get-Clipboard"
25+
}
26+
"@
27+
28+
describe 'AvoidOverwritingBuiltInCmdlets' {
29+
context 'No settings specified' {
30+
it 'should default to core-6.1.0-windows' {
31+
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings
32+
$violations.Count | Should -Be 1
33+
$violations.Extent.StartLineNumber | Should -Be 5
34+
}
35+
}
36+
37+
context 'PowerShellVersion explicitly set to Windows PowerShell' {
38+
$settings['Rules'] = $ruleSettingsWindows
39+
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings
40+
41+
it 'should find two violations' {
42+
$violations.Count | Should -Be 2
43+
}
44+
it 'should find the violations on the correct line' {
45+
$violations[0].Extent.StartLineNumber | Should -Be 5
46+
$violations[0].Extent.EndLineNumber | Should -Be 7
47+
48+
$violations[1].Extent.StartLineNumber | Should -Be 9
49+
$violations[1].Extent.EndLineNumber | Should -Be 11
50+
}
51+
}
52+
53+
context 'PowerShellVersion explicitly set to PowerShell 6' {
54+
$settings['Rules'] = $ruleSettingsCore
55+
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings
56+
57+
it 'should find one violation' {
58+
$violations.Count | Should -Be 1
59+
}
60+
it 'should find the correct violating function' {
61+
$violations.Extent.StartLineNumber | Should -Be 5
62+
$violations.Extent.EndLineNumber | Should -Be 7
63+
}
64+
}
65+
66+
context 'PowerShellVersion explicitly set to both Windows PowerShell and PowerShell 6' {
67+
$settings['Rules'] = $ruleSettingsBoth
68+
$violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings
69+
70+
it 'should find three violations' {
71+
$violations.Count | Should -Be 3
72+
}
73+
it 'should find the correct violating functions' {
74+
$violations[0].Extent.StartLineNumber | Should -Be 5
75+
$violations[0].Extent.EndLineNumber | Should -Be 7
76+
77+
$violations[1].Extent.StartLineNumber | Should -Be 5
78+
$violations[1].Extent.EndLineNumber | Should -Be 7
79+
80+
$violations[2].Extent.StartLineNumber | Should -Be 9
81+
$violations[2].Extent.EndLineNumber | Should -Be 11
82+
83+
}
84+
}
85+
86+
}

0 commit comments

Comments
 (0)