Skip to content

Commit 5d0ff03

Browse files
author
Kapil Borle
authored
Merge pull request #647 from PowerShell/kapilmb/FixUseCompatibleCmdlets
Fix UseCompatibleCmdlets rule
2 parents bd7c1b1 + 945d773 commit 5d0ff03

9 files changed

+11584
-132
lines changed

CHANGELOG.MD

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
## [unreleased](https://github.com/PowerShell/PSScriptAnalyzer/tree/development)
22
### Fixed
33
- Regular expression in `target` parameter in SuppressMessageAttribute (#638)
4+
- Filtering on severity level for DSC rules (#642)
5+
- PSUseCompatibleCmdlets rule
6+
+ to check for commands in Microsoft.PowerShell.Core snapin
7+
+ to ignore aliases
8+
+ to ignore custom defind commands
9+
410

511
## [1.8.1](https://github.com/PowerShell/PSScriptAnalyzer/tree/1.8.1) - 2016-10-13
612
### Added

Engine/Settings/core-6.0.0-alpha-linux.json

Lines changed: 378 additions & 22 deletions
Large diffs are not rendered by default.

Engine/Settings/core-6.0.0-alpha-osx.json

Lines changed: 370 additions & 14 deletions
Large diffs are not rendered by default.

Engine/Settings/core-6.0.0-alpha-windows.json

Lines changed: 423 additions & 25 deletions
Large diffs are not rendered by default.

Engine/Settings/desktop-5.1.14393.206-windows.json

Lines changed: 10207 additions & 0 deletions
Large diffs are not rendered by default.

Rules/UseCompatibleCmdlets.cs

Lines changed: 119 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
3333
#endif
3434
public class UseCompatibleCmdlets : AstVisitor, IScriptRule
3535
{
36+
private struct RuleParameters
37+
{
38+
public string mode;
39+
public string[] compatibility;
40+
public string reference;
41+
}
42+
3643
private List<DiagnosticRecord> diagnosticRecords;
3744
private Dictionary<string, HashSet<string>> psCmdletMap;
3845
private readonly List<string> validParameters;
@@ -41,10 +48,14 @@ public class UseCompatibleCmdlets : AstVisitor, IScriptRule
4148
private Dictionary<string, dynamic> platformSpecMap;
4249
private string scriptPath;
4350
private bool IsInitialized;
51+
private bool hasInitializationError;
52+
private string reference;
53+
private readonly string defaultReference = "desktop-5.1.14393.206-windows";
54+
private RuleParameters ruleParameters;
4455

4556
public UseCompatibleCmdlets()
4657
{
47-
validParameters = new List<string> { "mode", "uri", "compatibility" };
58+
validParameters = new List<string> { "mode", "uri", "compatibility", "reference" };
4859
IsInitialized = false;
4960
}
5061

@@ -124,6 +135,11 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
124135
Initialize();
125136
}
126137

138+
if (hasInitializationError)
139+
{
140+
yield break;
141+
}
142+
127143
if (ast == null)
128144
{
129145
throw new ArgumentNullException("ast");
@@ -168,12 +184,22 @@ public override AstVisitAction VisitCommand(CommandAst commandAst)
168184
/// </summary>
169185
private void GenerateDiagnosticRecords()
170186
{
171-
foreach (var curCmdletCompat in curCmdletCompatibilityMap)
187+
bool referenceCompatibility = curCmdletCompatibilityMap[reference];
188+
189+
// If the command is present in reference platform but not in any of the target platforms.
190+
// Or if the command is not present in reference platform but present in any of the target platforms
191+
// then declare it as an incompatible cmdlet.
192+
// If it is present neither in reference platform nor any target platforms, then it is probably a
193+
// non-builtin command and hence do not declare it as an incompatible cmdlet.
194+
// Since we do not check for aliases, the XOR-ing will also make sure that aliases are not flagged
195+
// as they will be found neither in reference platform nor in target platforms
196+
foreach (var platform in ruleParameters.compatibility)
172197
{
173-
if (!curCmdletCompat.Value)
198+
var curCmdletCompat = curCmdletCompatibilityMap[platform];
199+
if (!curCmdletCompat && referenceCompatibility)
174200
{
175201
var cmdletName = curCmdletAst.GetCommandName();
176-
var platformInfo = platformSpecMap[curCmdletCompat.Key];
202+
var platformInfo = platformSpecMap[platform];
177203
var funcNameTokens = Helper.Instance.Tokens.Where(
178204
token =>
179205
Helper.ContainsExtent(curCmdletAst.Extent, token.Extent)
@@ -215,6 +241,9 @@ private void Initialize()
215241
/// </summary>
216242
private void SetupCmdletsDictionary()
217243
{
244+
// If the method encounters any error, it returns early
245+
// which implies there is an initialization error
246+
hasInitializationError = true;
218247
Dictionary<string, object> ruleArgs = Helper.Instance.GetRuleArguments(GetName());
219248
if (ruleArgs == null)
220249
{
@@ -251,45 +280,93 @@ private void SetupCmdletsDictionary()
251280
}
252281
}
253282

254-
foreach (var compat in compatibilityList)
283+
ruleParameters.compatibility = compatibilityList.ToArray();
284+
reference = defaultReference;
285+
#if DEBUG
286+
// Setup reference file
287+
object referenceObject;
288+
if (ruleArgs.TryGetValue("reference", out referenceObject))
255289
{
256-
string psedition, psversion, os;
257-
258-
// ignore (warn) invalid entries
259-
if (GetVersionInfoFromPlatformString(compat, out psedition, out psversion, out os))
290+
reference = referenceObject as string;
291+
if (reference == null)
260292
{
261-
platformSpecMap.Add(compat, new { PSEdition = psedition, PSVersion = psversion, OS = os });
262-
curCmdletCompatibilityMap.Add(compat, true);
293+
reference = GetStringArgFromListStringArg(referenceObject);
294+
if (reference == null)
295+
{
296+
return;
297+
}
263298
}
264299
}
300+
#endif
301+
ruleParameters.reference = reference;
302+
303+
// check if the reference file has valid platformSpec
304+
if (!IsValidPlatformString(reference))
305+
{
306+
return;
307+
}
265308

309+
string settingsPath;
310+
settingsPath = GetShippedSettingsDirectory();
311+
#if DEBUG
266312
object modeObject;
267313
if (ruleArgs.TryGetValue("mode", out modeObject))
268314
{
269315
// This is for testing only. User should not be specifying mode!
270316
var mode = GetStringArgFromListStringArg(modeObject);
317+
ruleParameters.mode = mode;
271318
switch (mode)
272319
{
273320
case "offline":
274-
ProcessOfflineModeArgs(ruleArgs);
321+
settingsPath = GetStringArgFromListStringArg(ruleArgs["uri"]);
275322
break;
276323

277324
case "online": // not implemented yet.
278325
case null:
279326
default:
280-
break;
327+
return;
281328
}
282329

330+
}
331+
#endif
332+
if (settingsPath == null
333+
|| !ContainsReferenceFile(settingsPath))
334+
{
283335
return;
284336
}
285337

286-
var settingsPath = GetSettingsDirectory();
287-
if (settingsPath == null)
338+
var extentedCompatibilityList = compatibilityList.Concat(Enumerable.Repeat(reference, 1));
339+
foreach (var compat in extentedCompatibilityList)
340+
{
341+
string psedition, psversion, os;
342+
343+
// ignore (warn) invalid entries
344+
if (GetVersionInfoFromPlatformString(compat, out psedition, out psversion, out os))
345+
{
346+
platformSpecMap.Add(compat, new { PSEdition = psedition, PSVersion = psversion, OS = os });
347+
curCmdletCompatibilityMap.Add(compat, true);
348+
}
349+
}
350+
351+
ProcessDirectory(
352+
settingsPath,
353+
extentedCompatibilityList);
354+
if (psCmdletMap.Keys.Count != extentedCompatibilityList.Count())
288355
{
289356
return;
290357
}
291358

292-
ProcessDirectory(settingsPath);
359+
// reached this point, so no error
360+
hasInitializationError = false;
361+
}
362+
363+
/// <summary>
364+
/// Checks if the given directory has the reference file
365+
/// directory must be non-null
366+
/// </summary>
367+
private bool ContainsReferenceFile(string directory)
368+
{
369+
return File.Exists(Path.Combine(directory, reference + ".json"));
293370
}
294371

295372
/// <summary>
@@ -307,7 +384,7 @@ private void ResetCurCmdletCompatibilityMap()
307384
/// <summary>
308385
/// Retrieves the Settings directory from the Module directory structure
309386
/// </summary>
310-
private string GetSettingsDirectory()
387+
private string GetShippedSettingsDirectory()
311388
{
312389
// Find the compatibility files in Settings folder
313390
var path = this.GetType().GetTypeInfo().Assembly.Location;
@@ -332,6 +409,16 @@ private string GetSettingsDirectory()
332409
return settingsPath;
333410
}
334411

412+
private bool IsValidPlatformString(string fileNameWithoutExt)
413+
{
414+
string psedition, psversion, os;
415+
return GetVersionInfoFromPlatformString(
416+
fileNameWithoutExt,
417+
out psedition,
418+
out psversion,
419+
out os);
420+
}
421+
335422
/// <summary>
336423
/// Gets PowerShell Edition, Version and OS from input string
337424
/// </summary>
@@ -375,30 +462,10 @@ private string GetStringArgFromListStringArg(object arg)
375462
return strList[0];
376463
}
377464

378-
/// <summary>
379-
/// Process arguments when 'offline' mode is specified
380-
/// </summary>
381-
private void ProcessOfflineModeArgs(Dictionary<string, object> ruleArgs)
382-
{
383-
var uri = GetStringArgFromListStringArg(ruleArgs["uri"]);
384-
if (uri == null)
385-
{
386-
// TODO: log this
387-
return;
388-
}
389-
if (!Directory.Exists(uri))
390-
{
391-
// TODO: log this
392-
return;
393-
}
394-
395-
ProcessDirectory(uri);
396-
}
397-
398465
/// <summary>
399466
/// Search a directory for files of form [PSEdition]-[PSVersion]-[OS].json
400467
/// </summary>
401-
private void ProcessDirectory(string path)
468+
private void ProcessDirectory(string path, IEnumerable<string> acceptablePlatformSpecs)
402469
{
403470
foreach (var filePath in Directory.EnumerateFiles(path))
404471
{
@@ -410,36 +477,14 @@ private void ProcessDirectory(string path)
410477
}
411478

412479
var fileNameWithoutExt = Path.GetFileNameWithoutExtension(filePath);
413-
if (!platformSpecMap.ContainsKey(fileNameWithoutExt))
480+
if (acceptablePlatformSpecs != null
481+
&& !acceptablePlatformSpecs.Contains(fileNameWithoutExt, StringComparer.OrdinalIgnoreCase))
414482
{
415483
continue;
416484
}
417485

418486
psCmdletMap[fileNameWithoutExt] = GetCmdletsFromData(JObject.Parse(File.ReadAllText(filePath)));
419487
}
420-
421-
RemoveUnavailableKeys();
422-
}
423-
424-
/// <summary>
425-
/// Remove keys that are not present in psCmdletMap but present in platformSpecMap and curCmdletCompatibilityMap
426-
/// </summary>
427-
private void RemoveUnavailableKeys()
428-
{
429-
var keysToRemove = new List<string>();
430-
foreach (var key in platformSpecMap.Keys)
431-
{
432-
if (!psCmdletMap.ContainsKey(key))
433-
{
434-
keysToRemove.Add(key);
435-
}
436-
}
437-
438-
foreach (var key in keysToRemove)
439-
{
440-
platformSpecMap.Remove(key);
441-
curCmdletCompatibilityMap.Remove(key);
442-
}
443488
}
444489

445490
/// <summary>
@@ -451,11 +496,20 @@ private HashSet<string> GetCmdletsFromData(dynamic deserializedObject)
451496
{
452497
var cmdlets = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
453498
dynamic modules = deserializedObject.Modules;
454-
foreach (var module in modules)
499+
foreach (dynamic module in modules)
455500
{
456-
foreach (var cmdlet in module.ExportedCommands)
501+
if (module.ExportedCommands == null)
457502
{
458-
var name = cmdlet.Name.Value as string;
503+
continue;
504+
}
505+
506+
foreach (dynamic cmdlet in module.ExportedCommands)
507+
{
508+
var name = cmdlet.Name as string;
509+
if (name == null)
510+
{
511+
name = cmdlet.Name.ToObject<string>();
512+
}
459513
cmdlets.Add(name);
460514
}
461515
}

Tests/PSScriptAnalyzerTestHelper.psm1

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,15 @@ Function Test-PSEditionCoreCLRLinux
3838
(Test-PSEditionCoreCLR) -and $IsLinux
3939
}
4040

41+
Function Get-Count
42+
{
43+
Begin {$count = 0}
44+
Process {$count++}
45+
End {$count}
46+
}
47+
4148
Export-ModuleMember -Function Get-ExtentText
4249
Export-ModuleMember -Function Test-CorrectionExtent
4350
Export-ModuleMember -Function Test-PSEditionCoreCLR
44-
Export-ModuleMember -Function Test-PSEditionCoreCLRLinux
51+
Export-ModuleMember -Function Test-PSEditionCoreCLRLinux
52+
Export-ModuleMember -Function Get-Count
Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
Import-Module PSScriptAnalyzer
2-
$ruleName = "PSUseCompatibleCmdlets"
1+
$ruleName = "PSUseCompatibleCmdlets"
32
$directory = Split-Path $MyInvocation.MyCommand.Path -Parent
3+
$testRootDirectory = Split-Path -Parent $directory
44
$ruleTestDirectory = Join-Path $directory 'UseCompatibleCmdlets'
55

6+
Import-Module PSScriptAnalyzer
7+
Import-Module (Join-Path $testRootDirectory 'PSScriptAnalyzerTestHelper.psm1')
8+
69
Describe "UseCompatibleCmdlets" {
710
Context "script has violation" {
811
It "detects violation" {
@@ -12,4 +15,44 @@ Describe "UseCompatibleCmdlets" {
1215
$diagnosticRecords.Count | Should Be 1
1316
}
1417
}
18+
19+
Function Test-Command
20+
{
21+
param (
22+
[Parameter(ValueFromPipeline)]
23+
$command,
24+
$settings,
25+
$expectedViolations
26+
)
27+
process
28+
{
29+
It ("found {0} violations for '{1}'" -f $expectedViolations, $command) {
30+
Invoke-ScriptAnalyzer -ScriptDefinition $command -IncludeRule $ruleName -Settings $settings | `
31+
Get-Count | `
32+
Should Be $expectedViolations
33+
}
34+
}
35+
}
36+
37+
$settings = @{rules=@{PSUseCompatibleCmdlets=@{compatibility=@("core-6.0.0-alpha-windows")}}}
38+
39+
Context "Microsoft.PowerShell.Core" {
40+
@('Enter-PSSession', 'Foreach-Object', 'Get-Command') | `
41+
Test-Command -Settings $settings -ExpectedViolations 0
42+
}
43+
44+
Context "Non-builtin commands" {
45+
@('get-foo', 'get-bar', 'get-baz') | `
46+
Test-Command -Settings $settings -ExpectedViolations 0
47+
}
48+
49+
Context "Aliases" {
50+
@('where', 'select', 'cd') | `
51+
Test-Command -Settings $settings -ExpectedViolations 0
52+
}
53+
54+
Context "Commands present in reference platform but not in target platform" {
55+
@("Start-VM", "New-SmbShare", "Get-Disk") | `
56+
Test-Command -Settings $settings -ExpectedViolations 1
57+
}
1558
}

0 commit comments

Comments
 (0)