Skip to content

Commit 6009a23

Browse files
author
Kapil Borle
committed
Update UseCompatibleCmdlets rule to use offline data
1 parent 06712e1 commit 6009a23

3 files changed

Lines changed: 245 additions & 5 deletions

File tree

Rules/Strings.Designer.cs

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Rules/Strings.resx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,6 @@
844844
<value>Use cmdlets compatible with the given PowerShell version and edition and operating system</value>
845845
</data>
846846
<data name="UseCompatibleCmdletsError" xml:space="preserve">
847-
<value>{0} is not compatible with PowerShell version {0}, edition {1} and OS {2}</value>
847+
<value>'{0}' is not compatible with PowerShell edition '{1}', version '{2}' and OS '{3}'</value>
848848
</data>
849849
</root>

Rules/UseCompatibleCmdlets.cs

Lines changed: 243 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@
1414
using System.ComponentModel.Composition;
1515
#endif
1616
using System.Globalization;
17+
using System.IO;
1718
using System.Linq;
1819
using System.Management.Automation.Language;
20+
using System.Text.RegularExpressions;
1921
using Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic;
2022

23+
using Newtonsoft.Json;
24+
2125
namespace 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

Comments
 (0)