@@ -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 }
0 commit comments