Skip to content

Commit cba1f64

Browse files
author
Kapil Borle
committed
Add module dependency handler for missing modules
Whenever Invoke-ScriptAnalyzer (isa) is run on a script having the dynamic keyword "Import-DSCResource -ModuleName <somemodule>", if <somemodule> is not present in any of the PSModulePath, isa gives parse error. This error is caused by the powershell parser not being able to find the symbol for <somemodule>.
1 parent a467aff commit cba1f64

File tree

3 files changed

+302
-20
lines changed

3 files changed

+302
-20
lines changed
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.ObjectModel;
4+
using System.IO;
5+
using System.Linq;
6+
using System.Management.Automation;
7+
using System.Management.Automation.Language;
8+
using System.Management.Automation.Runspaces;
9+
using System.Text;
10+
11+
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic
12+
{
13+
// TODO Use runspace pool
14+
// TODO Create a new process for the runspace
15+
public class ModuleDependencyHandler : IDisposable
16+
{
17+
#region Private Variables
18+
private Runspace runspace;
19+
private readonly string moduleRepository;
20+
private string tempDirPath;
21+
Dictionary<string, PSObject> modulesFound;
22+
HashSet<string> modulesSaved;
23+
private string oldPSModulePath;
24+
private string currentModulePath;
25+
26+
#endregion Private Variables
27+
28+
#region Properties
29+
public string ModulePath
30+
{
31+
get { return tempDirPath; }
32+
}
33+
34+
public Runspace Runspace
35+
{
36+
get { return runspace; }
37+
}
38+
39+
#endregion
40+
41+
#region Private Methods
42+
private static void ThrowIfNull<T>(T obj, string name)
43+
{
44+
if (obj == null)
45+
{
46+
throw new ArgumentNullException(name);
47+
}
48+
}
49+
private void SetupTempDir()
50+
{
51+
//var tempPath = Path.GetTempPath();
52+
//do
53+
//{
54+
// tempDirPath = Path.Combine(
55+
// tempPath,
56+
// Path.GetFileNameWithoutExtension(Path.GetRandomFileName()));
57+
//} while (Directory.Exists(tempDirPath));
58+
//Directory.CreateDirectory(tempDirPath);
59+
tempDirPath = "C:\\Users\\kabawany\\tmp\\modules\\";
60+
}
61+
62+
private void RemoveTempDir()
63+
{
64+
//Directory.Delete(tempDirPath, true);
65+
}
66+
67+
private void SetupPSModulePath()
68+
{
69+
oldPSModulePath = Environment.GetEnvironmentVariable("PSModulePath", EnvironmentVariableTarget.Process);
70+
var sb = new StringBuilder();
71+
sb.Append(oldPSModulePath)
72+
.Append(Path.DirectorySeparatorChar)
73+
.Append(tempDirPath);
74+
currentModulePath = sb.ToString();
75+
}
76+
77+
private void CleanUp()
78+
{
79+
runspace.Dispose();
80+
RemoveTempDir();
81+
RestorePSModulePath();
82+
}
83+
84+
private void RestorePSModulePath()
85+
{
86+
Environment.SetEnvironmentVariable("PSModulePath", oldPSModulePath, EnvironmentVariableTarget.Process);
87+
}
88+
89+
private void SaveModule(PSObject module)
90+
{
91+
ThrowIfNull(module, "module");
92+
// TODO validate module
93+
var ps = System.Management.Automation.PowerShell.Create();
94+
ps.Runspace = runspace;
95+
ps.AddCommand("Save-Module")
96+
.AddParameter("Path", tempDirPath)
97+
.AddParameter("InputObject", module);
98+
ps.Invoke();
99+
}
100+
101+
#endregion Private Methods
102+
103+
#region Public Methods
104+
105+
public ModuleDependencyHandler()
106+
{
107+
runspace = null;
108+
moduleRepository = "PSGallery";
109+
modulesSaved = new HashSet<string>();
110+
modulesFound = new Dictionary<string, PSObject>();
111+
SetupTempDir();
112+
//SetupPSModulePath();
113+
}
114+
115+
public ModuleDependencyHandler(Runspace runspace) : this()
116+
{
117+
ThrowIfNull(runspace, "runspace");
118+
this.runspace = runspace;
119+
this.runspace.Open();
120+
}
121+
122+
123+
public void SetupDefaultRunspace()
124+
{
125+
runspace = RunspaceFactory.CreateRunspace();
126+
}
127+
128+
public void SetupDefaultRunspace(Runspace runspace)
129+
{
130+
ThrowIfNull(runspace, "runspace");
131+
if (runspace != null)
132+
{
133+
this.runspace = runspace;
134+
}
135+
Runspace.DefaultRunspace = this.runspace;
136+
}
137+
138+
public PSObject FindModule(string moduleName)
139+
{
140+
ThrowIfNull(moduleName, "moduleName");
141+
if (modulesFound.ContainsKey(moduleName))
142+
{
143+
return modulesFound[moduleName];
144+
}
145+
var ps = System.Management.Automation.PowerShell.Create();
146+
Collection<PSObject> modules = null;
147+
ps.Runspace = runspace;
148+
ps.AddCommand("Find-Module", true)
149+
.AddParameter("Name", moduleName)
150+
.AddParameter("Repository", moduleRepository);
151+
modules = ps.Invoke<PSObject>();
152+
153+
if (modules == null)
154+
{
155+
return null;
156+
}
157+
var module = modules.FirstOrDefault();
158+
if (module == null )
159+
{
160+
return null;
161+
}
162+
modulesFound.Add(moduleName, module);
163+
return module;
164+
}
165+
166+
public void SaveModule(string moduleName)
167+
{
168+
ThrowIfNull(moduleName, "moduleName");
169+
if (modulesSaved.Contains(moduleName))
170+
{
171+
return;
172+
}
173+
var module = FindModule(moduleName);
174+
if (module == null)
175+
{
176+
throw new ItemNotFoundException(
177+
string.Format(
178+
"Cannot find {0} in {1} repository.",
179+
moduleName,
180+
moduleRepository));
181+
}
182+
SaveModule(module);
183+
modulesSaved.Add(moduleName);
184+
}
185+
186+
public static string GetModuleNameFromErrorExtent(ParseError error, ScriptBlockAst ast)
187+
{
188+
ThrowIfNull(error, "error");
189+
ThrowIfNull(ast, "ast");
190+
var statement = ast.Find(x => x.Extent.Equals(error.Extent), true);
191+
var dynamicKywdAst = statement as DynamicKeywordStatementAst;
192+
if (dynamicKywdAst == null)
193+
{
194+
return null;
195+
}
196+
// check if the command name is import-dscmodule
197+
// right now we handle only the following form
198+
// Import-DSCResource -ModuleName xActiveDirectory
199+
if (dynamicKywdAst.CommandElements.Count != 3)
200+
{
201+
return null;
202+
}
203+
204+
var dscKeywordAst = dynamicKywdAst.CommandElements[0] as StringConstantExpressionAst;
205+
if (dscKeywordAst == null || !dscKeywordAst.Value.Equals("Import-DscResource", StringComparison.OrdinalIgnoreCase))
206+
{
207+
return null;
208+
}
209+
210+
var paramAst = dynamicKywdAst.CommandElements[1] as CommandParameterAst;
211+
if (paramAst == null || !paramAst.ParameterName.Equals("ModuleName", StringComparison.OrdinalIgnoreCase))
212+
{
213+
return null;
214+
}
215+
216+
var paramValAst = dynamicKywdAst.CommandElements[2] as StringConstantExpressionAst;
217+
if (paramValAst == null)
218+
{
219+
return null;
220+
}
221+
222+
return paramValAst.Value;
223+
}
224+
225+
public void Dispose()
226+
{
227+
CleanUp();
228+
}
229+
230+
#endregion Public Methods
231+
}
232+
}

Engine/ScriptAnalyzer.cs

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ public sealed class ScriptAnalyzer
4545
List<Regex> includeRegexList;
4646
List<Regex> excludeRegexList;
4747
bool suppressedOnly;
48+
Runspace runspace;
49+
ModuleDependencyHandler moduleHandler;
4850

4951
#endregion
5052

@@ -165,7 +167,8 @@ public void CleanUp()
165167
severity = null;
166168
includeRegexList = null;
167169
excludeRegexList = null;
168-
suppressedOnly = false;
170+
suppressedOnly = false;
171+
moduleHandler.Dispose();
169172
}
170173

171174
internal bool ParseProfile(object profileObject, PathIntrinsics path, IOutputWriter writer)
@@ -476,6 +479,10 @@ private void Initialize(
476479
}
477480

478481
this.outputWriter = outputWriter;
482+
483+
// TODO Create a runspace pool
484+
runspace = RunspaceFactory.CreateRunspace();
485+
moduleHandler = new ModuleDependencyHandler(runspace);
479486

480487
#region Verifies rule extensions and loggers path
481488

@@ -1271,8 +1278,9 @@ private void BuildScriptPathList(
12711278
ErrorCategory.InvalidArgument,
12721279
this));
12731280
}
1274-
}
1281+
}
12751282

1283+
12761284
private IEnumerable<DiagnosticRecord> AnalyzeFile(string filePath)
12771285
{
12781286
ScriptBlockAst scriptAst = null;
@@ -1297,6 +1305,41 @@ private IEnumerable<DiagnosticRecord> AnalyzeFile(string filePath)
12971305
return null;
12981306
}
12991307

1308+
1309+
// TODO Handle Parse Errors causes by missing modules
1310+
// if errors are due to ModuleNotFoundDuringParse
1311+
// - EITHER
1312+
// - create a runspace and set the default runspace (make sure backup the default runspace before modifying it)
1313+
// - check if it is present in the gallery
1314+
// - if present on the gallery, save it to a temporary file, and load the module
1315+
// - now parse again
1316+
// - if parse is successful, proceed
1317+
// - else inform the user of the issue
1318+
// - OR
1319+
// - swallow the these errors
1320+
1321+
1322+
if (errors != null && errors.Length > 0)
1323+
{
1324+
foreach (ParseError error in errors)
1325+
{
1326+
if (IsModuleNotFoundError(error))
1327+
{
1328+
var moduleName = ModuleDependencyHandler.GetModuleNameFromErrorExtent(error, scriptAst);
1329+
if (moduleName != null)
1330+
{
1331+
moduleHandler.SaveModule(moduleName);
1332+
}
1333+
}
1334+
}
1335+
}
1336+
1337+
//try parsing again
1338+
//var oldDefault = Runspace.DefaultRunspace;
1339+
//Runspace.DefaultRunspace = moduleHandler.Runspace;
1340+
scriptAst = Parser.ParseFile(filePath, out scriptTokens, out errors);
1341+
//Runspace.DefaultRunspace = oldDefault;
1342+
13001343
if (errors != null && errors.Length > 0)
13011344
{
13021345
foreach (ParseError error in errors)
@@ -1327,6 +1370,12 @@ private IEnumerable<DiagnosticRecord> AnalyzeFile(string filePath)
13271370
return this.AnalyzeSyntaxTree(scriptAst, scriptTokens, filePath);
13281371
}
13291372

1373+
private bool IsModuleNotFoundError(ParseError error)
1374+
{
1375+
return error.ErrorId != null
1376+
&& error.ErrorId.Equals("ModuleNotFoundDuringParse", StringComparison.OrdinalIgnoreCase);
1377+
}
1378+
13301379
private bool IsSeverityAllowed(IEnumerable<uint> allowedSeverities, IRule rule)
13311380
{
13321381
return severity == null

Engine/ScriptAnalyzerEngine.csproj

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,23 +34,23 @@
3434
<RootNamespace>Microsoft.Windows.PowerShell.ScriptAnalyzer</RootNamespace>
3535
</PropertyGroup>
3636
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'PSV3 Debug|AnyCPU'">
37-
<DebugSymbols>true</DebugSymbols>
38-
<OutputPath>bin\PSV3 Debug\</OutputPath>
39-
<DefineConstants>TRACE;DEBUG;PSV3</DefineConstants>
40-
<DebugType>full</DebugType>
41-
<PlatformTarget>AnyCPU</PlatformTarget>
42-
<ErrorReport>prompt</ErrorReport>
43-
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
44-
</PropertyGroup>
45-
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'PSV3 Release|AnyCPU'">
46-
<OutputPath>bin\PSV3 Release\</OutputPath>
47-
<DefineConstants>TRACE;PSV3</DefineConstants>
48-
<Optimize>true</Optimize>
49-
<DebugType>pdbonly</DebugType>
50-
<PlatformTarget>AnyCPU</PlatformTarget>
51-
<ErrorReport>prompt</ErrorReport>
52-
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
53-
</PropertyGroup>
37+
<DebugSymbols>true</DebugSymbols>
38+
<OutputPath>bin\PSV3 Debug\</OutputPath>
39+
<DefineConstants>TRACE;DEBUG;PSV3</DefineConstants>
40+
<DebugType>full</DebugType>
41+
<PlatformTarget>AnyCPU</PlatformTarget>
42+
<ErrorReport>prompt</ErrorReport>
43+
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
44+
</PropertyGroup>
45+
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'PSV3 Release|AnyCPU'">
46+
<OutputPath>bin\PSV3 Release\</OutputPath>
47+
<DefineConstants>TRACE;PSV3</DefineConstants>
48+
<Optimize>true</Optimize>
49+
<DebugType>pdbonly</DebugType>
50+
<PlatformTarget>AnyCPU</PlatformTarget>
51+
<ErrorReport>prompt</ErrorReport>
52+
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
53+
</PropertyGroup>
5454
<ItemGroup>
5555
<Reference Include="Microsoft.CSharp" />
5656
<Reference Include="System" />
@@ -70,6 +70,7 @@
7070
<Compile Include="Commands\InvokeScriptAnalyzerCommand.cs" />
7171
<Compile Include="Generic\AvoidCmdletGeneric.cs" />
7272
<Compile Include="Generic\AvoidParameterGeneric.cs" />
73+
<Compile Include="Generic\ModuleDependencyHandler.cs" />
7374
<Compile Include="Generic\SuppressedRecord.cs" />
7475
<Compile Include="Generic\DiagnosticRecord.cs" />
7576
<Compile Include="Generic\ExternalRule.cs" />
@@ -101,7 +102,7 @@
101102
<Compile Include="VariableAnalysisBase.cs" />
102103
</ItemGroup>
103104
<ItemGroup>
104-
<None Include="PSScriptAnalyzer.psm1" />
105+
<None Include="PSScriptAnalyzer.psm1" />
105106
<None Include="PSScriptAnalyzer.psd1" />
106107
<None Include="ScriptAnalyzer.format.ps1xml" />
107108
<None Include="ScriptAnalyzer.types.ps1xml" />

0 commit comments

Comments
 (0)