Skip to content

Commit ab69069

Browse files
thomasraynerbergmeister
authored andcommitted
New rule: AvoidOverwritingBuiltInCmdlets (#1348)
* avoidoverwritingbuiltincmdlets first draft * rough draft avoidoverwritingcmdlets working * Added tests, fixed typos, changed default PowerShellVersion behavior * updates a/p rjmholt * remove unneeded else * avoidoverwritingbuiltincmdlets first draft * rough draft avoidoverwritingcmdlets working * Added tests, fixed typos, changed default PowerShellVersion behavior * updates a/p rjmholt * remove unneeded else * updated readme - want tests to run in CI again * prevent adding duplicate keys * return an empty list instead of null * update rule count * fixing pwsh not present issue in test * fixing a ps 4 test broke a linux test * better PS core detection * Add reference to UseCompatibleCmdlets doc * changes a/p Chris * Update RuleDocumentation/AvoidOverwritingBuiltInCmdlets.md Co-Authored-By: Christoph Bergmeister [MVP] <[email protected]> * trimmed doc and changed functiondefinitions detection to be more performant * retrigger-ci after fix was made in master * retrigger-ci due to sporadic test failure * Update number of expected rules due to recent merge of PR #1373
1 parent 4bc3911 commit ab69069

7 files changed

+448
-2
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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. These whitelist files are used by other PSScriptAnalyzer rules. More information can be found in the documentation for the [UseCompatibleCmdlets](./UseCompatibleCmdlets.md) rule.
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 of whitelists that ship with PSScriptAnalyzer.
29+
30+
**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.
31+
32+
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).
33+
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
@@ -13,6 +13,7 @@
1313
|[AvoidGlobalVars](./AvoidGlobalVars.md) | Warning | |
1414
|[AvoidInvokingEmptyMembers](./AvoidInvokingEmptyMembers.md) | Warning | |
1515
|[AvoidLongLines](./AvoidLongLines.md) | Warning | |
16+
|[AvoidOverwritingBuiltInCmdlets](./AvoidOverwritingBuiltInCmdlets.md) | Warning | |
1617
|[AvoidNullOrEmptyHelpMessageAttribute](./AvoidNullOrEmptyHelpMessageAttribute.md) | Warning | |
1718
|[AvoidShouldContinueWithoutForce](./AvoidShouldContinueWithoutForce.md) | Warning | |
1819
|[AvoidUsingCmdletAliases](./AvoidUsingCmdletAliases.md) | Warning | Yes |
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
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.Diagnostics;
7+
#if !CORECLR
8+
using System.ComponentModel.Composition;
9+
#endif
10+
using System.Globalization;
11+
using System.IO;
12+
using System.Linq;
13+
using System.Management.Automation.Language;
14+
using System.Text.RegularExpressions;
15+
using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic;
16+
17+
using Newtonsoft.Json.Linq;
18+
19+
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
20+
{
21+
/// <summary>
22+
/// AvoidOverwritingBuiltInCmdlets: Checks if a script overwrites a cmdlet that comes with PowerShell
23+
/// </summary>
24+
#if !CORECLR
25+
[Export(typeof(IScriptRule))]
26+
#endif
27+
/// <summary>
28+
/// A class to check if a script overwrites a cmdlet that comes with PowerShell
29+
/// </summary>
30+
public class AvoidOverwritingBuiltInCmdlets : ConfigurableRule
31+
{
32+
/// <summary>
33+
/// Specify the version of PowerShell to compare against since different versions of PowerShell
34+
/// ship with different sets of built in cmdlets. The default value for PowerShellVersion is
35+
/// "core-6.1.0-windows" if PowerShell 6 or later is installed, and "desktop-5.1.14393.206-windows"
36+
/// if it is not. The version specified aligns with a JSON file in `/path/to/PSScriptAnalyzerModule/Settings`.
37+
/// These files are of the form, `PSEDITION-PSVERSION-OS.json` where `PSEDITION` can be either `Core` or
38+
/// `Desktop`, `OS` can be either `Windows`, `Linux` or `MacOS`, and `Version` is the PowerShell version.
39+
/// </summary>
40+
[ConfigurableRuleProperty(defaultValue: "")]
41+
public string[] PowerShellVersion { get; set; }
42+
private readonly Dictionary<string, HashSet<string>> _cmdletMap;
43+
44+
45+
/// <summary>
46+
/// Construct an object of AvoidOverwritingBuiltInCmdlets type.
47+
/// </summary>
48+
public AvoidOverwritingBuiltInCmdlets()
49+
{
50+
_cmdletMap = new Dictionary<string, HashSet<string>>();
51+
Enable = true; // Enable rule by default
52+
}
53+
54+
55+
/// <summary>
56+
/// Analyzes the given ast to find the [violation]
57+
/// </summary>
58+
/// <param name="ast">AST to be analyzed. This should be non-null</param>
59+
/// <param name="fileName">Name of file that corresponds to the input AST.</param>
60+
/// <returns>A an enumerable type containing the violations</returns>
61+
public override IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
62+
{
63+
if (ast == null)
64+
{
65+
throw new ArgumentNullException(nameof(ast));
66+
}
67+
68+
var diagnosticRecords = new List<DiagnosticRecord>();
69+
70+
IEnumerable<FunctionDefinitionAst> functionDefinitions = ast.FindAll(testAst => testAst is FunctionDefinitionAst, true).OfType<FunctionDefinitionAst>();
71+
if (!functionDefinitions.Any())
72+
{
73+
// There are no function definitions in this AST and so it's not worth checking the rest of this rule
74+
return diagnosticRecords;
75+
}
76+
77+
78+
if (PowerShellVersion.Length == 0 || string.IsNullOrEmpty(PowerShellVersion[0]))
79+
{
80+
// PowerShellVersion is not already set to one of the acceptable defaults
81+
// Try launching `pwsh -v` to see if PowerShell 6+ is installed, and use those cmdlets
82+
// as a default. If 6+ is not installed this will throw an error, which when caught will
83+
// allow us to use the PowerShell 5 cmdlets as a default.
84+
85+
PowerShellVersion = new[] { "desktop-5.1.14393.206-windows" };
86+
#if CORECLR
87+
PowerShellVersion = new[] { "core-6.1.0-windows" };
88+
#endif
89+
90+
}
91+
92+
var psVerList = PowerShellVersion;
93+
string settingsPath = Settings.GetShippedSettingsDirectory();
94+
95+
foreach (string reference in psVerList)
96+
{
97+
if (settingsPath == null || !ContainsReferenceFile(settingsPath, reference))
98+
{
99+
throw new ArgumentException(nameof(PowerShellVersion));
100+
}
101+
}
102+
103+
ProcessDirectory(settingsPath, psVerList);
104+
105+
if (_cmdletMap.Keys.Count != psVerList.Count())
106+
{
107+
throw new ArgumentException(nameof(PowerShellVersion));
108+
}
109+
110+
foreach (FunctionDefinitionAst functionDef in functionDefinitions)
111+
{
112+
string functionName = functionDef.Name;
113+
foreach (KeyValuePair<string, HashSet<string>> cmdletSet in _cmdletMap)
114+
{
115+
if (cmdletSet.Value.Contains(functionName))
116+
{
117+
diagnosticRecords.Add(CreateDiagnosticRecord(functionName, cmdletSet.Key, functionDef.Extent));
118+
}
119+
}
120+
}
121+
122+
return diagnosticRecords;
123+
}
124+
125+
126+
private DiagnosticRecord CreateDiagnosticRecord(string FunctionName, string PSVer, IScriptExtent ViolationExtent)
127+
{
128+
var record = new DiagnosticRecord(
129+
string.Format(CultureInfo.CurrentCulture,
130+
string.Format(Strings.AvoidOverwritingBuiltInCmdletsError, FunctionName, PSVer)),
131+
ViolationExtent,
132+
GetName(),
133+
GetDiagnosticSeverity(),
134+
ViolationExtent.File,
135+
null
136+
);
137+
return record;
138+
}
139+
140+
141+
private bool ContainsReferenceFile(string directory, string reference)
142+
{
143+
return File.Exists(Path.Combine(directory, reference + ".json"));
144+
}
145+
146+
147+
private void ProcessDirectory(string path, IEnumerable<string> acceptablePlatformSpecs)
148+
{
149+
foreach (var filePath in Directory.EnumerateFiles(path))
150+
{
151+
var extension = Path.GetExtension(filePath);
152+
if (String.IsNullOrWhiteSpace(extension)
153+
|| !extension.Equals(".json", StringComparison.OrdinalIgnoreCase))
154+
{
155+
continue;
156+
}
157+
158+
var fileNameWithoutExt = Path.GetFileNameWithoutExtension(filePath);
159+
if (acceptablePlatformSpecs != null
160+
&& !acceptablePlatformSpecs.Contains(fileNameWithoutExt, StringComparer.OrdinalIgnoreCase))
161+
{
162+
continue;
163+
}
164+
165+
if (_cmdletMap.Keys.Contains(fileNameWithoutExt))
166+
{
167+
continue;
168+
}
169+
170+
_cmdletMap.Add(fileNameWithoutExt, GetCmdletsFromData(JObject.Parse(File.ReadAllText(filePath))));
171+
}
172+
}
173+
174+
175+
private HashSet<string> GetCmdletsFromData(dynamic deserializedObject)
176+
{
177+
var cmdlets = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
178+
dynamic modules = deserializedObject.Modules;
179+
foreach (dynamic module in modules)
180+
{
181+
if (module.ExportedCommands == null)
182+
{
183+
continue;
184+
}
185+
186+
foreach (dynamic cmdlet in module.ExportedCommands)
187+
{
188+
var name = cmdlet.Name as string;
189+
if (name == null)
190+
{
191+
name = cmdlet.Name.ToString();
192+
}
193+
cmdlets.Add(name);
194+
}
195+
}
196+
197+
return cmdlets;
198+
}
199+
200+
201+
/// <summary>
202+
/// Retrieves the common name of this rule.
203+
/// </summary>
204+
public override string GetCommonName()
205+
{
206+
return string.Format(CultureInfo.CurrentCulture, Strings.AvoidOverwritingBuiltInCmdletsCommonName);
207+
}
208+
209+
/// <summary>
210+
/// Retrieves the description of this rule.
211+
/// </summary>
212+
public override string GetDescription()
213+
{
214+
return string.Format(CultureInfo.CurrentCulture, Strings.AvoidOverwritingBuiltInCmdletsDescription);
215+
}
216+
217+
/// <summary>
218+
/// Retrieves the name of this rule.
219+
/// </summary>
220+
public override string GetName()
221+
{
222+
return string.Format(
223+
CultureInfo.CurrentCulture,
224+
Strings.NameSpaceFormat,
225+
GetSourceName(),
226+
Strings.AvoidOverwritingBuiltInCmdletsName);
227+
}
228+
229+
/// <summary>
230+
/// Retrieves the severity of the rule: error, warning or information.
231+
/// </summary>
232+
public override RuleSeverity GetSeverity()
233+
{
234+
return RuleSeverity.Warning;
235+
}
236+
237+
/// <summary>
238+
/// Gets the severity of the returned diagnostic record: error, warning, or information.
239+
/// </summary>
240+
/// <returns></returns>
241+
public DiagnosticSeverity GetDiagnosticSeverity()
242+
{
243+
return DiagnosticSeverity.Warning;
244+
}
245+
246+
/// <summary>
247+
/// Retrieves the name of the module/assembly the rule is from.
248+
/// </summary>
249+
public override string GetSourceName()
250+
{
251+
return string.Format(CultureInfo.CurrentCulture, Strings.SourceName);
252+
}
253+
254+
/// <summary>
255+
/// Retrieves the type of the rule, Builtin, Managed or Module.
256+
/// </summary>
257+
public override SourceType GetSourceType()
258+
{
259+
return SourceType.Builtin;
260+
}
261+
}
262+
}

Rules/Strings.Designer.cs

Lines changed: 44 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
@@ -810,6 +810,18 @@
810810
<data name="UseCompatibleCmdletsError" xml:space="preserve">
811811
<value>'{0}' is not compatible with PowerShell edition '{1}', version '{2}' and OS '{3}'</value>
812812
</data>
813+
<data name="AvoidOverwritingBuiltInCmdletsName" xml:space="preserve">
814+
<value>AvoidOverwritingBuiltInCmdlets</value>
815+
</data>
816+
<data name="AvoidOverwritingBuiltInCmdletsCommonName" xml:space="preserve">
817+
<value>Avoid overwriting built in cmdlets</value>
818+
</data>
819+
<data name="AvoidOverwritingBuiltInCmdletsDescription" xml:space="preserve">
820+
<value>Do not overwrite the definition of a cmdlet that is included with PowerShell</value>
821+
</data>
822+
<data name="AvoidOverwritingBuiltInCmdletsError" xml:space="preserve">
823+
<value>'{0}' is a cmdlet that is included with PowerShell (version {1}) whose definition should not be overridden</value>
824+
</data>
813825
<data name="UseCompatibleCommandsName" xml:space="preserve">
814826
<value>UseCompatibleCommands</value>
815827
</data>

Tests/Engine/GetScriptAnalyzerRule.tests.ps1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ Describe "Test Name parameters" {
5858
}
5959

6060
It "get Rules with no parameters supplied" {
61-
$defaultRules = Get-ScriptAnalyzerRule
62-
$expectedNumRules = 61
61+
$defaultRules = Get-ScriptAnalyzerRule
62+
$expectedNumRules = 62
6363
if ((Test-PSEditionCoreClr) -or (Test-PSVersionV3) -or (Test-PSVersionV4))
6464
{
6565
# for PSv3 PSAvoidGlobalAliases is not shipped because

0 commit comments

Comments
 (0)