@@ -33,6 +33,13 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
33
33
#endif
34
34
public class UseCompatibleCmdlets : AstVisitor , IScriptRule
35
35
{
36
+ private struct RuleParameters
37
+ {
38
+ public string mode ;
39
+ public string [ ] compatibility ;
40
+ public string reference ;
41
+ }
42
+
36
43
private List < DiagnosticRecord > diagnosticRecords ;
37
44
private Dictionary < string , HashSet < string > > psCmdletMap ;
38
45
private readonly List < string > validParameters ;
@@ -41,10 +48,14 @@ public class UseCompatibleCmdlets : AstVisitor, IScriptRule
41
48
private Dictionary < string , dynamic > platformSpecMap ;
42
49
private string scriptPath ;
43
50
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 ;
44
55
45
56
public UseCompatibleCmdlets ( )
46
57
{
47
- validParameters = new List < string > { "mode" , "uri" , "compatibility" } ;
58
+ validParameters = new List < string > { "mode" , "uri" , "compatibility" , "reference" } ;
48
59
IsInitialized = false ;
49
60
}
50
61
@@ -124,6 +135,11 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
124
135
Initialize ( ) ;
125
136
}
126
137
138
+ if ( hasInitializationError )
139
+ {
140
+ yield break ;
141
+ }
142
+
127
143
if ( ast == null )
128
144
{
129
145
throw new ArgumentNullException ( "ast" ) ;
@@ -168,12 +184,22 @@ public override AstVisitAction VisitCommand(CommandAst commandAst)
168
184
/// </summary>
169
185
private void GenerateDiagnosticRecords ( )
170
186
{
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 )
172
197
{
173
- if ( ! curCmdletCompat . Value )
198
+ var curCmdletCompat = curCmdletCompatibilityMap [ platform ] ;
199
+ if ( ! curCmdletCompat && referenceCompatibility )
174
200
{
175
201
var cmdletName = curCmdletAst . GetCommandName ( ) ;
176
- var platformInfo = platformSpecMap [ curCmdletCompat . Key ] ;
202
+ var platformInfo = platformSpecMap [ platform ] ;
177
203
var funcNameTokens = Helper . Instance . Tokens . Where (
178
204
token =>
179
205
Helper . ContainsExtent ( curCmdletAst . Extent , token . Extent )
@@ -215,6 +241,9 @@ private void Initialize()
215
241
/// </summary>
216
242
private void SetupCmdletsDictionary ( )
217
243
{
244
+ // If the method encounters any error, it returns early
245
+ // which implies there is an initialization error
246
+ hasInitializationError = true ;
218
247
Dictionary < string , object > ruleArgs = Helper . Instance . GetRuleArguments ( GetName ( ) ) ;
219
248
if ( ruleArgs == null )
220
249
{
@@ -251,45 +280,93 @@ private void SetupCmdletsDictionary()
251
280
}
252
281
}
253
282
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 ) )
255
289
{
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 )
260
292
{
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
+ }
263
298
}
264
299
}
300
+ #endif
301
+ ruleParameters . reference = reference ;
302
+
303
+ // check if the reference file has valid platformSpec
304
+ if ( ! IsValidPlatformString ( reference ) )
305
+ {
306
+ return ;
307
+ }
265
308
309
+ string settingsPath ;
310
+ settingsPath = GetShippedSettingsDirectory ( ) ;
311
+ #if DEBUG
266
312
object modeObject ;
267
313
if ( ruleArgs . TryGetValue ( "mode" , out modeObject ) )
268
314
{
269
315
// This is for testing only. User should not be specifying mode!
270
316
var mode = GetStringArgFromListStringArg ( modeObject ) ;
317
+ ruleParameters . mode = mode ;
271
318
switch ( mode )
272
319
{
273
320
case "offline" :
274
- ProcessOfflineModeArgs ( ruleArgs ) ;
321
+ settingsPath = GetStringArgFromListStringArg ( ruleArgs [ "uri" ] ) ;
275
322
break ;
276
323
277
324
case "online" : // not implemented yet.
278
325
case null :
279
326
default :
280
- break ;
327
+ return ;
281
328
}
282
329
330
+ }
331
+ #endif
332
+ if ( settingsPath == null
333
+ || ! ContainsReferenceFile ( settingsPath ) )
334
+ {
283
335
return ;
284
336
}
285
337
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 ( ) )
288
355
{
289
356
return ;
290
357
}
291
358
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" ) ) ;
293
370
}
294
371
295
372
/// <summary>
@@ -307,7 +384,7 @@ private void ResetCurCmdletCompatibilityMap()
307
384
/// <summary>
308
385
/// Retrieves the Settings directory from the Module directory structure
309
386
/// </summary>
310
- private string GetSettingsDirectory ( )
387
+ private string GetShippedSettingsDirectory ( )
311
388
{
312
389
// Find the compatibility files in Settings folder
313
390
var path = this . GetType ( ) . GetTypeInfo ( ) . Assembly . Location ;
@@ -332,6 +409,16 @@ private string GetSettingsDirectory()
332
409
return settingsPath ;
333
410
}
334
411
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
+
335
422
/// <summary>
336
423
/// Gets PowerShell Edition, Version and OS from input string
337
424
/// </summary>
@@ -375,30 +462,10 @@ private string GetStringArgFromListStringArg(object arg)
375
462
return strList [ 0 ] ;
376
463
}
377
464
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
-
398
465
/// <summary>
399
466
/// Search a directory for files of form [PSEdition]-[PSVersion]-[OS].json
400
467
/// </summary>
401
- private void ProcessDirectory ( string path )
468
+ private void ProcessDirectory ( string path , IEnumerable < string > acceptablePlatformSpecs )
402
469
{
403
470
foreach ( var filePath in Directory . EnumerateFiles ( path ) )
404
471
{
@@ -410,36 +477,14 @@ private void ProcessDirectory(string path)
410
477
}
411
478
412
479
var fileNameWithoutExt = Path . GetFileNameWithoutExtension ( filePath ) ;
413
- if ( ! platformSpecMap . ContainsKey ( fileNameWithoutExt ) )
480
+ if ( acceptablePlatformSpecs != null
481
+ && ! acceptablePlatformSpecs . Contains ( fileNameWithoutExt , StringComparer . OrdinalIgnoreCase ) )
414
482
{
415
483
continue ;
416
484
}
417
485
418
486
psCmdletMap [ fileNameWithoutExt ] = GetCmdletsFromData ( JObject . Parse ( File . ReadAllText ( filePath ) ) ) ;
419
487
}
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
- }
443
488
}
444
489
445
490
/// <summary>
@@ -451,11 +496,20 @@ private HashSet<string> GetCmdletsFromData(dynamic deserializedObject)
451
496
{
452
497
var cmdlets = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
453
498
dynamic modules = deserializedObject . Modules ;
454
- foreach ( var module in modules )
499
+ foreach ( dynamic module in modules )
455
500
{
456
- foreach ( var cmdlet in module . ExportedCommands )
501
+ if ( module . ExportedCommands == null )
457
502
{
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
+ }
459
513
cmdlets . Add ( name ) ;
460
514
}
461
515
}
0 commit comments