1414using System . ComponentModel . Composition ;
1515#endif
1616using System . Globalization ;
17+ using System . IO ;
1718using System . Linq ;
1819using System . Management . Automation . Language ;
20+ using System . Text . RegularExpressions ;
1921using Microsoft . Windows . PowerShell . ScriptAnalyzer . Generic ;
2022
23+ using Newtonsoft . Json ;
24+
2125namespace Microsoft . Windows . PowerShell . ScriptAnalyzer . BuiltinRules
2226{
2327 /// <summary>
@@ -26,8 +30,172 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules
2630 #if ! CORECLR
2731 [ Export ( typeof ( IScriptRule ) ) ]
2832#endif
29- class UseCompatibleCmdlets : IScriptRule
33+ class UseCompatibleCmdlets : AstVisitor , IScriptRule
3034 {
35+ private List < DiagnosticRecord > diagnosticRecords ;
36+ private Dictionary < string , HashSet < string > > psCmdletMap ;
37+ private readonly List < string > validParameters ;
38+ private CommandAst curCmdletAst ;
39+ private Dictionary < string , bool > curCmdletCompatibilityMap ;
40+ private Dictionary < string , dynamic > platformSpecMap ;
41+ private string scriptPath ;
42+
43+ public UseCompatibleCmdlets ( )
44+ {
45+ diagnosticRecords = new List < DiagnosticRecord > ( ) ;
46+ psCmdletMap = new Dictionary < string , HashSet < string > > ( ) ;
47+ validParameters = new List < string > { "mode" , "uri" , "compatibility" } ;
48+ curCmdletCompatibilityMap = new Dictionary < string , bool > ( StringComparer . OrdinalIgnoreCase ) ;
49+ platformSpecMap = new Dictionary < string , dynamic > ( StringComparer . OrdinalIgnoreCase ) ;
50+ SetupCmdletsDictionary ( ) ;
51+ }
52+
53+ private void SetupCmdletsDictionary ( )
54+ {
55+ Dictionary < string , object > ruleArgs = Helper . Instance . GetRuleArguments ( GetName ( ) ) ;
56+ if ( ruleArgs == null )
57+ {
58+ return ;
59+ }
60+
61+ if ( ! RuleParamsValid ( ruleArgs ) )
62+ {
63+ return ;
64+ }
65+
66+ var compatibilityList = ruleArgs [ "compatibility" ] as List < string > ;
67+ if ( compatibilityList == null )
68+ {
69+ return ;
70+ }
71+
72+ foreach ( var compat in compatibilityList )
73+ {
74+ string psedition , psversion , os ;
75+ if ( GetVersionInfoFromPlatformString ( compat , out psedition , out psversion , out os ) )
76+ {
77+ platformSpecMap . Add ( compat , new { PSEdition = psedition , PSVersion = psversion , OS = os } ) ;
78+ curCmdletCompatibilityMap . Add ( compat , false ) ;
79+ }
80+ }
81+
82+ var mode = GetStringArgFromListStringArg ( ruleArgs [ "mode" ] ) ;
83+ switch ( mode )
84+ {
85+ case "online" :
86+ ProcessOnlineModeArgs ( ruleArgs ) ;
87+ break ;
88+
89+ case "offline" :
90+ ProcessOfflineModeArgs ( ruleArgs ) ;
91+ break ;
92+
93+ case null :
94+ default :
95+ return ;
96+ }
97+ }
98+
99+ private bool GetVersionInfoFromPlatformString (
100+ string fileName ,
101+ out string psedition ,
102+ out string psversion ,
103+ out string os )
104+ {
105+ psedition = null ;
106+ psversion = null ;
107+ os = null ;
108+ const string pattern = @"^(?<psedition>core|desktop)-(?<psversion>[\S]+)-(?<os>windows|linux|macOS)$" ;
109+ var match = Regex . Match ( fileName , pattern , RegexOptions . IgnoreCase ) ;
110+ if ( match == Match . Empty )
111+ {
112+ return false ;
113+ }
114+ psedition = match . Groups [ "psedition" ] . Value ;
115+ psversion = match . Groups [ "psversion" ] . Value ;
116+ os = match . Groups [ "os" ] . Value ;
117+ return true ;
118+ }
119+
120+ private string GetStringArgFromListStringArg ( object arg )
121+ {
122+ if ( arg == null )
123+ {
124+ return null ;
125+ }
126+ var strList = arg as List < string > ;
127+ if ( strList == null
128+ || strList . Count != 1 )
129+ {
130+ return null ;
131+ }
132+ return strList [ 0 ] ;
133+ }
134+
135+ private void ProcessOfflineModeArgs ( Dictionary < string , object > ruleArgs )
136+ {
137+ var uri = GetStringArgFromListStringArg ( ruleArgs [ "uri" ] ) ;
138+ if ( uri == null )
139+ {
140+ // TODO: log this
141+ return ;
142+ }
143+ if ( ! Directory . Exists ( uri ) )
144+ {
145+ // TODO: log this
146+ return ;
147+ }
148+ foreach ( var filePath in Directory . EnumerateFiles ( uri ) )
149+ {
150+ var extension = Path . GetExtension ( filePath ) ;
151+ if ( String . IsNullOrWhiteSpace ( extension )
152+ || ! extension . Equals ( ".json" , StringComparison . OrdinalIgnoreCase ) )
153+ {
154+ continue ;
155+ }
156+
157+ var fileNameWithoutExt = Path . GetFileNameWithoutExtension ( filePath ) ;
158+ if ( ! platformSpecMap . ContainsKey ( fileNameWithoutExt ) )
159+ {
160+ continue ;
161+ }
162+
163+ psCmdletMap [ fileNameWithoutExt ] = GetCmdletsFromData ( JsonConvert . DeserializeObject ( File . ReadAllText ( filePath ) ) ) ;
164+ }
165+ }
166+
167+ private HashSet < string > GetCmdletsFromData ( dynamic deserializedObject )
168+ {
169+ var cmdlets = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
170+ foreach ( var module in deserializedObject )
171+ {
172+ if ( module . HasValues == false )
173+ {
174+ continue ;
175+ }
176+
177+ foreach ( var cmdlet in module . Value )
178+ {
179+ if ( cmdlet . Name != null )
180+ {
181+ cmdlets . Add ( cmdlet . Name ) ;
182+ }
183+ }
184+ }
185+ return cmdlets ;
186+ }
187+
188+ private void ProcessOnlineModeArgs ( Dictionary < string , object > ruleArgs )
189+ {
190+ throw new NotImplementedException ( ) ;
191+ }
192+
193+ private bool RuleParamsValid ( Dictionary < string , object > ruleArgs )
194+ {
195+ return ruleArgs . Keys . All (
196+ key => validParameters . Any ( x => x . Equals ( key , StringComparison . OrdinalIgnoreCase ) ) ) ;
197+ }
198+
31199 /// <summary>
32200 /// Analyzes the given ast to find the [violation]
33201 /// </summary>
@@ -41,8 +209,80 @@ public IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string fileName)
41209 throw new ArgumentNullException ( "ast" ) ;
42210 }
43211
44- // your code goes here
45- yield return new DiagnosticRecord ( ) ;
212+ scriptPath = fileName ;
213+ diagnosticRecords . Clear ( ) ;
214+ ast . Visit ( this ) ;
215+ foreach ( var dr in diagnosticRecords )
216+ {
217+ yield return dr ;
218+ }
219+ }
220+
221+
222+ public override AstVisitAction VisitCommand ( CommandAst commandAst )
223+ {
224+ if ( commandAst == null )
225+ {
226+ return AstVisitAction . SkipChildren ;
227+ }
228+
229+ var commandName = commandAst . GetCommandName ( ) ;
230+ if ( commandName == null )
231+ {
232+ return AstVisitAction . SkipChildren ;
233+ }
234+
235+ curCmdletAst = commandAst ;
236+ CheckCompatibility ( ) ;
237+ GenerateDiagnosticRecords ( ) ;
238+ return AstVisitAction . Continue ;
239+ }
240+
241+ private void GenerateDiagnosticRecords ( )
242+ {
243+ foreach ( var curCmdletCompat in curCmdletCompatibilityMap )
244+ {
245+ if ( ! curCmdletCompat . Value )
246+ {
247+ var cmdletName = curCmdletAst . GetCommandName ( ) ;
248+ var platformInfo = platformSpecMap [ curCmdletCompat . Key ] ;
249+ var funcNameTokens = Helper . Instance . Tokens . Where (
250+ token =>
251+ Helper . ContainsExtent ( curCmdletAst . Extent , token . Extent )
252+ && token . Text . Equals ( cmdletName ) ) ;
253+ var funcNameToken = funcNameTokens . FirstOrDefault ( ) ;
254+ var extent = funcNameToken == null ? null : funcNameToken . Extent ;
255+ diagnosticRecords . Add ( new DiagnosticRecord (
256+ String . Format (
257+ Strings . UseCompatibleCmdletsError ,
258+ cmdletName ,
259+ platformInfo . PSEdition ,
260+ platformInfo . PSVersion ,
261+ platformInfo . OS ) ,
262+ extent ,
263+ GetName ( ) ,
264+ GetDiagnosticSeverity ( ) ,
265+ scriptPath ,
266+ null ,
267+ null ) ) ;
268+ }
269+ }
270+ }
271+
272+ private void CheckCompatibility ( )
273+ {
274+ string commandName = curCmdletAst . GetCommandName ( ) ;
275+ foreach ( var platformSpec in psCmdletMap )
276+ {
277+ if ( platformSpec . Value . Contains ( commandName ) )
278+ {
279+ curCmdletCompatibilityMap [ platformSpec . Key ] = true ;
280+ }
281+ else
282+ {
283+ curCmdletCompatibilityMap [ platformSpec . Key ] = false ;
284+ }
285+ }
46286 }
47287
48288 /// <summary>
0 commit comments