diff --git a/Engine/Commands/InvokeScriptAnalyzerCommand.cs b/Engine/Commands/InvokeScriptAnalyzerCommand.cs
index 9a097cb81..bbaf6fcab 100644
--- a/Engine/Commands/InvokeScriptAnalyzerCommand.cs
+++ b/Engine/Commands/InvokeScriptAnalyzerCommand.cs
@@ -25,6 +25,7 @@
using System.Threading.Tasks;
using System.Collections.Concurrent;
using System.Threading;
+using System.Management.Automation.Runspaces;
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Commands
{
@@ -44,7 +45,7 @@ public class InvokeScriptAnalyzerCommand : PSCmdlet, IOutputWriter
[Parameter(Position = 0,
ParameterSetName = "File",
Mandatory = true,
- ValueFromPipeline = true,
+ ValueFromPipeline = true,
ValueFromPipelineByPropertyName = true)]
[ValidateNotNull]
[Alias("PSPath")]
@@ -188,6 +189,16 @@ public object Settings
private bool stopProcessing;
+ ///
+ /// Resolve DSC resource dependency
+ ///
+ [Parameter(Mandatory = false)]
+ public SwitchParameter SaveDscResourceDependency
+ {
+ get { return saveDscResourceDependency; }
+ set { saveDscResourceDependency = value; }
+ }
+ private bool saveDscResourceDependency;
#endregion Parameters
#region Overrides
@@ -227,18 +238,22 @@ protected override void ProcessRecord()
return;
}
- if (String.Equals(this.ParameterSetName, "File", StringComparison.OrdinalIgnoreCase))
+ // TODO Support dependency resolution for analyzing script definitions
+ if (saveDscResourceDependency)
{
- // throws Item Not Found Exception
- Collection paths = this.SessionState.Path.GetResolvedPSPathFromPSPath(path);
- foreach (PathInfo p in paths)
+ using (var rsp = RunspaceFactory.CreateRunspace())
{
- ProcessPathOrScriptDefinition(this.SessionState.Path.GetUnresolvedProviderPathFromPSPath(p.Path));
+ rsp.Open();
+ using (var moduleHandler = new ModuleDependencyHandler(rsp))
+ {
+ ScriptAnalyzer.Instance.ModuleHandler = moduleHandler;
+ ProcessInput();
+ }
}
}
- else if (String.Equals(this.ParameterSetName, "ScriptDefinition", StringComparison.OrdinalIgnoreCase))
+ else
{
- ProcessPathOrScriptDefinition(scriptDefinition);
+ ProcessInput();
}
}
@@ -257,30 +272,38 @@ protected override void StopProcessing()
#endregion
#region Methods
-
- private void ProcessPathOrScriptDefinition(string pathOrScriptDefinition)
+ private void ProcessInput()
{
IEnumerable diagnosticsList = Enumerable.Empty();
-
if (String.Equals(this.ParameterSetName, "File", StringComparison.OrdinalIgnoreCase))
{
- diagnosticsList = ScriptAnalyzer.Instance.AnalyzePath(pathOrScriptDefinition, this.recurse);
+ // throws Item Not Found Exception
+ Collection paths = this.SessionState.Path.GetResolvedPSPathFromPSPath(path);
+ foreach (PathInfo p in paths)
+ {
+ diagnosticsList = ScriptAnalyzer.Instance.AnalyzePath(
+ this.SessionState.Path.GetUnresolvedProviderPathFromPSPath(p.Path),
+ this.recurse);
+ WriteToOutput(diagnosticsList);
+ }
}
else if (String.Equals(this.ParameterSetName, "ScriptDefinition", StringComparison.OrdinalIgnoreCase))
{
- diagnosticsList = ScriptAnalyzer.Instance.AnalyzeScriptDefinition(pathOrScriptDefinition);
+ diagnosticsList = ScriptAnalyzer.Instance.AnalyzeScriptDefinition(scriptDefinition);
+ WriteToOutput(diagnosticsList);
}
+ }
- //Output through loggers
+ private void WriteToOutput(IEnumerable diagnosticRecords)
+ {
foreach (ILogger logger in ScriptAnalyzer.Instance.Loggers)
{
- foreach (DiagnosticRecord diagnostic in diagnosticsList)
+ foreach (DiagnosticRecord diagnostic in diagnosticRecords)
{
logger.LogObject(diagnostic, this);
}
}
}
-
#endregion
}
}
\ No newline at end of file
diff --git a/Engine/Generic/ModuleDependencyHandler.cs b/Engine/Generic/ModuleDependencyHandler.cs
new file mode 100644
index 000000000..1494816a1
--- /dev/null
+++ b/Engine/Generic/ModuleDependencyHandler.cs
@@ -0,0 +1,507 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Management.Automation;
+using System.Management.Automation.Language;
+using System.Management.Automation.Runspaces;
+
+namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic
+{
+ // TODO Use runspace pool
+ // TODO Support for verbose mode
+ public class ModuleDependencyHandler : IDisposable
+ {
+ #region Private Variables
+ private Runspace runspace;
+ private string moduleRepository;
+ private string tempPath; // path to the user temporary directory
+ private string tempModulePath; // path to temp directory containing modules
+ Dictionary modulesFound;
+ private string localAppdataPath;
+ private string pssaAppDataPath;
+ private const string symLinkName = "TempModuleDir";
+ private const string tempPrefix = "PSSAModules-";
+ private string symLinkPath;
+ private string oldPSModulePath;
+ private string curPSModulePath;
+ #endregion Private Variables
+
+ #region Properties
+ ///
+ /// Path where the object stores the modules
+ ///
+ public string TempModulePath
+ {
+ get { return tempModulePath; }
+ }
+
+ ///
+ /// Temporary path of the current user scope
+ ///
+ public string TempPath
+ {
+ get
+ {
+ return tempPath;
+ }
+ // it must be set only during initialization
+ private set
+ {
+ tempPath
+ = string.IsNullOrWhiteSpace(value)
+ ? Path.GetTempPath()
+ : value;
+ }
+
+ }
+
+ ///
+ /// Local App Data path
+ ///
+ public string LocalAppDataPath
+ {
+ get
+ {
+ return localAppdataPath;
+ }
+ private set
+ {
+ localAppdataPath
+ = string.IsNullOrWhiteSpace(value)
+ ? Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)
+ : value;
+ }
+ }
+
+ ///
+ /// Module Respository
+ /// By default it is PSGallery
+ ///
+ public string ModuleRepository
+ {
+ get
+ {
+ return moduleRepository;
+ }
+ set
+ {
+ moduleRepository
+ = string.IsNullOrWhiteSpace(value)
+ ? "PSGallery"
+ : value;
+ }
+ }
+
+ ///
+ /// Local App data of PSSScriptAnalyzer
+ ///
+ public string PSSAAppDataPath
+ {
+ get
+ {
+ return pssaAppDataPath;
+ }
+ }
+
+ ///
+ /// Module Paths
+ ///
+ public string PSModulePath
+ {
+ get { return curPSModulePath; }
+ }
+
+ ///
+ /// Runspace in which the object invokes powershell cmdlets
+ ///
+ public Runspace Runspace
+ {
+ get { return runspace; }
+ set { runspace = value; }
+ }
+
+
+ #endregion Properties
+
+ #region Private Methods
+ private static void ThrowIfNull(T obj, string name)
+ {
+ if (obj == null)
+ {
+ throw new ArgumentNullException(name);
+ }
+ }
+
+ private void SetupPSSAAppData()
+ {
+ // check if pssa exists in local appdata
+ if (Directory.Exists(pssaAppDataPath))
+ {
+ // check if there is a link
+ if (File.Exists(symLinkPath))
+ {
+ tempModulePath = GetTempModulePath(symLinkPath);
+
+ // check if the temp dir exists
+ if (Directory.Exists(tempModulePath))
+ {
+ return;
+ }
+ }
+ }
+ else
+ {
+ Directory.CreateDirectory(pssaAppDataPath);
+ }
+ SetupTempDir();
+ }
+
+ private bool IsModulePresentInTempModulePath(string moduleName)
+ {
+ foreach (var dir in Directory.EnumerateDirectories(TempModulePath))
+ {
+ if (moduleName.Equals(dir, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void SetupTempDir()
+ {
+ tempModulePath = GetPSSATempDirPath();
+ Directory.CreateDirectory(tempModulePath);
+ File.WriteAllLines(symLinkPath, new string[] { tempModulePath });
+ }
+
+ private string GetPSSATempDirPath()
+ {
+ string path;
+ do
+ {
+ path = Path.Combine(
+ tempPath,
+ tempPrefix + Path.GetFileNameWithoutExtension(Path.GetRandomFileName()));
+ } while (Directory.Exists(path));
+ return path;
+ }
+
+ // Return the first line of the file
+ private string GetTempModulePath(string symLinkPath)
+ {
+ string line;
+ using (var fileStream = new StreamReader(symLinkPath))
+ {
+ line = fileStream.ReadLine();
+ }
+ return line;
+ }
+
+ private void SaveModule(PSObject module)
+ {
+ ThrowIfNull(module, "module");
+
+ // TODO validate module
+ using (var ps = System.Management.Automation.PowerShell.Create())
+ {
+ ps.Runspace = runspace;
+ ps.AddCommand("Save-Module")
+ .AddParameter("Path", tempModulePath)
+ .AddParameter("InputObject", module);
+ ps.Invoke();
+ }
+ }
+
+ private void SetupPSModulePath()
+ {
+ oldPSModulePath = Environment.GetEnvironmentVariable("PSModulePath");
+ curPSModulePath = oldPSModulePath + ";" + tempModulePath;
+ Environment.SetEnvironmentVariable("PSModulePath", curPSModulePath, EnvironmentVariableTarget.Process);
+ }
+
+ private void RestorePSModulePath()
+ {
+ Environment.SetEnvironmentVariable("PSModulePath", oldPSModulePath, EnvironmentVariableTarget.Process);
+ }
+ #endregion Private Methods
+
+ #region Public Methods
+
+ ///
+ /// Creates an instance of the ModuleDependencyHandler class
+ ///
+ /// Runspace in which the instance runs powershell cmdlets to find and save modules
+ /// Name of the repository from where to download the modules. By default it is PSGallery. This should be a registered repository.
+ /// Path to the user scoped temporary directory
+ /// Path to the local app data directory
+ public ModuleDependencyHandler(
+ Runspace runspace,
+ string moduleRepository = null,
+ string tempPath = null,
+ string localAppDataPath = null)
+ {
+
+ ThrowIfNull(runspace, "runspace");
+ if (runspace.RunspaceStateInfo.State != RunspaceState.Opened)
+ {
+ throw new ArgumentException(string.Format(
+ "Runspace state cannot be in {0} state. It must be in Opened state",
+ runspace.RunspaceStateInfo.State.ToString()));
+ }
+ Runspace = runspace;
+
+ // TODO should set PSSA environment variables outside this class
+ // Should be set in ScriptAnalyzer class
+ // and then passed into modulehandler
+ TempPath = tempPath;
+ LocalAppDataPath = localAppDataPath;
+ ModuleRepository = moduleRepository;
+ pssaAppDataPath = Path.Combine(
+ LocalAppDataPath,
+ string.IsNullOrWhiteSpace(pssaAppDataPath)
+ ? "PSScriptAnalyzer"
+ : pssaAppDataPath);
+
+ modulesFound = new Dictionary();
+
+ // TODO Add PSSA Version in the path
+ symLinkPath = Path.Combine(pssaAppDataPath, symLinkName);
+ SetupPSSAAppData();
+ SetupPSModulePath();
+
+ }
+
+ ///
+ /// Encapsulates Find-Module
+ ///
+ /// Name of the module
+ /// A PSObject if it finds the modules otherwise returns null
+ public PSObject FindModule(string moduleName)
+ {
+ ThrowIfNull(moduleName, "moduleName");
+ moduleName = moduleName.ToLower();
+ if (modulesFound.ContainsKey(moduleName))
+ {
+ return modulesFound[moduleName];
+ }
+ Collection modules = null;
+ using (var ps = System.Management.Automation.PowerShell.Create())
+ {
+ ps.Runspace = runspace;
+ ps.AddCommand("Find-Module", true)
+ .AddParameter("Name", moduleName)
+ .AddParameter("Repository", moduleRepository);
+ modules = ps.Invoke();
+ }
+ if (modules == null)
+ {
+ return null;
+ }
+ var module = modules.FirstOrDefault();
+ if (module == null )
+ {
+ return null;
+ }
+ modulesFound.Add(moduleName, module);
+ return module;
+ }
+
+ ///
+ /// SaveModule version that doesn't throw
+ ///
+ /// Name of the module
+ /// True if it can save a module otherwise false.
+ public bool TrySaveModule(string moduleName)
+ {
+ try
+ {
+ SaveModule(moduleName);
+ return true;
+ }
+ catch
+ {
+ // log exception to verbose
+ return false;
+ }
+ }
+
+ ///
+ /// Encapsulates Save-Module cmdlet
+ ///
+ /// Name of the module
+ public void SaveModule(string moduleName)
+ {
+ ThrowIfNull(moduleName, "moduleName");
+ if (IsModulePresentInTempModulePath(moduleName))
+ {
+ return;
+ }
+ using (var ps = System.Management.Automation.PowerShell.Create())
+ {
+ ps.Runspace = runspace;
+ ps.AddCommand("Save-Module")
+ .AddParameter("Path", tempModulePath)
+ .AddParameter("Name", moduleName)
+ .AddParameter("Repository", moduleRepository)
+ .AddParameter("Force");
+ ps.Invoke();
+ }
+ }
+
+ ///
+ /// Encapsulates Get-Module to check the availability of the module on the system
+ ///
+ ///
+ /// True indicating the presence of the module, otherwise false
+ public bool IsModuleAvailable(string moduleName)
+ {
+ ThrowIfNull(moduleName, "moduleName");
+ IEnumerable availableModules;
+ using (var ps = System.Management.Automation.PowerShell.Create())
+ {
+ ps.Runspace = runspace;
+ availableModules = ps.AddCommand("Get-Module")
+ .AddParameter("Name", moduleName)
+ .AddParameter("ListAvailable")
+ .Invoke();
+ }
+ return availableModules != null ? availableModules.Any() : false;
+ }
+
+ ///
+ /// Extracts out the module names from the error extent that are not available
+ ///
+ /// This handles the following case.
+ /// Import-DSCResourceModule -ModuleName ModulePresent,ModuleAbsent
+ ///
+ /// ModulePresent is present in PSModulePath whereas ModuleAbsent is not.
+ /// But the error exent coverts the entire extent and hence we need to check
+ /// which module is actually not present so as to be downloaded
+ ///
+ ///
+ ///
+ /// An enumeration over the module names that are not available
+ public IEnumerable GetUnavailableModuleNameFromErrorExtent(ParseError error, ScriptBlockAst ast)
+ {
+ ThrowIfNull(error, "error");
+ ThrowIfNull(ast, "ast");
+ var moduleNames = ModuleDependencyHandler.GetModuleNameFromErrorExtent(error, ast);
+ if (moduleNames == null)
+ {
+ return null;
+ }
+ var unavailableModules = new List();
+ foreach (var moduleName in moduleNames)
+ {
+ if (!IsModuleAvailable(moduleName))
+ {
+ unavailableModules.Add(moduleName);
+ }
+ }
+ //return moduleNames.Where(x => !IsModuleAvailable(x));
+ return unavailableModules;
+ }
+
+ ///
+ /// Get the module name from the error extent
+ ///
+ /// If a parser encounters Import-DSCResource -ModuleName SomeModule
+ /// and if SomeModule is not present in any of the PSModulePaths, the
+ /// parser throws ModuleNotFoundDuringParse Error. We correlate the
+ /// error message with extent to extract the module name as the error
+ /// record doesn't provide direct access to the missing module name.
+ ///
+ /// Parse error
+ /// AST of the script that contians the parse error
+ /// The name of the module that caused the parser to throw the error. Returns null if it cannot extract the module name.
+ public static IEnumerable GetModuleNameFromErrorExtent(ParseError error, ScriptBlockAst ast)
+ {
+ ThrowIfNull(error, "error");
+ ThrowIfNull(ast, "ast");
+ var statement = ast.Find(x => x.Extent.Equals(error.Extent), true);
+ var dynamicKywdAst = statement as DynamicKeywordStatementAst;
+ if (dynamicKywdAst == null)
+ {
+ return null;
+ }
+ // check if the command name is import-dscmodule
+ // right now we handle only the following forms
+ // 1. Import-DSCResourceModule -ModuleName somemodule
+ // 2. Import-DSCResourceModule -ModuleName somemodule1,somemodule2
+ if (dynamicKywdAst.CommandElements.Count < 3)
+ {
+ return null;
+ }
+
+ var dscKeywordAst = dynamicKywdAst.CommandElements[0] as StringConstantExpressionAst;
+ if (dscKeywordAst == null || !dscKeywordAst.Value.Equals("Import-DscResource", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ // find a parameter named modulename
+ int k;
+ for (k = 1; k < dynamicKywdAst.CommandElements.Count; k++)
+ {
+ var paramAst = dynamicKywdAst.CommandElements[1] as CommandParameterAst;
+ // TODO match the initial letters only
+ if (paramAst == null || !paramAst.ParameterName.Equals("ModuleName", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+ break;
+ }
+
+ if (k == dynamicKywdAst.CommandElements.Count)
+ {
+ // cannot find modulename
+ return null;
+ }
+ var modules = new List();
+
+ // k < count - 1, because only -ModuleName throws parse error and hence not possible
+ var paramValAst = dynamicKywdAst.CommandElements[++k];
+
+ // import-dscresource -ModuleName module1
+ var paramValStrConstExprAst = paramValAst as StringConstantExpressionAst;
+ if (paramValStrConstExprAst != null)
+ {
+ modules.Add(paramValStrConstExprAst.Value);
+ return modules;
+ }
+
+ // import-dscresource -ModuleName module1,module2
+ var paramValArrLtrlAst = paramValAst as ArrayLiteralAst;
+ if (paramValArrLtrlAst != null)
+ {
+ foreach (var elem in paramValArrLtrlAst.Elements)
+ {
+ var elemStrConstExprAst = elem as StringConstantExpressionAst;
+ if (elemStrConstExprAst != null)
+ {
+ modules.Add(elemStrConstExprAst.Value);
+ }
+ }
+ if (modules.Count == 0)
+ {
+ return null;
+ }
+ return modules;
+ }
+ return null;
+ }
+
+ ///
+ /// Disposes the runspace and restores the PSModulePath.
+ ///
+ public void Dispose()
+ {
+ RestorePSModulePath();
+ }
+
+ #endregion Public Methods
+ }
+}
\ No newline at end of file
diff --git a/Engine/ScriptAnalyzer.cs b/Engine/ScriptAnalyzer.cs
index 8dd1fb82b..1f8e239c1 100644
--- a/Engine/ScriptAnalyzer.cs
+++ b/Engine/ScriptAnalyzer.cs
@@ -45,14 +45,12 @@ public sealed class ScriptAnalyzer
List includeRegexList;
List excludeRegexList;
bool suppressedOnly;
-
+ ModuleDependencyHandler moduleHandler;
#endregion
#region Singleton
private static object syncRoot = new Object();
-
private static ScriptAnalyzer instance;
-
public static ScriptAnalyzer Instance
{
get
@@ -88,6 +86,17 @@ public static ScriptAnalyzer Instance
public IEnumerable DSCResourceRules { get; private set; }
internal List ExternalRules { get; set; }
+ public ModuleDependencyHandler ModuleHandler {
+ get
+ {
+ return moduleHandler;
+ }
+
+ internal set
+ {
+ moduleHandler = value;
+ }
+ }
#endregion
@@ -97,9 +106,9 @@ public static ScriptAnalyzer Instance
/// Initialize : Initializes default rules, loggers and helper.
///
internal void Initialize(
- TCmdlet cmdlet,
- string[] customizedRulePath = null,
- string[] includeRuleNames = null,
+ TCmdlet cmdlet,
+ string[] customizedRulePath = null,
+ string[] includeRuleNames = null,
string[] excludeRuleNames = null,
string[] severity = null,
bool includeDefaultRules = false,
@@ -110,7 +119,7 @@ internal void Initialize(
{
throw new ArgumentNullException("cmdlet");
}
-
+
this.Initialize(
cmdlet,
cmdlet.SessionState.Path,
@@ -127,10 +136,10 @@ internal void Initialize(
/// Initialize : Initializes default rules, loggers and helper.
///
public void Initialize(
- Runspace runspace,
- IOutputWriter outputWriter,
- string[] customizedRulePath = null,
- string[] includeRuleNames = null,
+ Runspace runspace,
+ IOutputWriter outputWriter,
+ string[] customizedRulePath = null,
+ string[] includeRuleNames = null,
string[] excludeRuleNames = null,
string[] severity = null,
bool includeDefaultRules = false,
@@ -204,7 +213,7 @@ internal bool ParseProfile(object profileObject, PathIntrinsics path, IOutputWri
hasError = ParseProfileString(profile, path, writer, severityList, includeRuleList, excludeRuleList);
}
}
-
+
if (hasError)
{
return false;
@@ -269,7 +278,7 @@ private bool ParseProfileHashtable(Hashtable profile, PathIntrinsics path, IOutp
hasError = true;
continue;
}
-
+
// checks whether it falls into list of valid keys
if (!validKeys.Contains(key))
{
@@ -325,7 +334,7 @@ private bool ParseProfileHashtable(Hashtable profile, PathIntrinsics path, IOutp
}
AddProfileItem(key, values, severityList, includeRuleList, excludeRuleList);
-
+
}
return hasError;
@@ -459,10 +468,10 @@ private bool ParseProfileString(string profile, PathIntrinsics path, IOutputWrit
}
private void Initialize(
- IOutputWriter outputWriter,
- PathIntrinsics path,
- CommandInvocationIntrinsics invokeCommand,
- string[] customizedRulePath,
+ IOutputWriter outputWriter,
+ PathIntrinsics path,
+ CommandInvocationIntrinsics invokeCommand,
+ string[] customizedRulePath,
string[] includeRuleNames,
string[] excludeRuleNames,
string[] severity,
@@ -475,6 +484,8 @@ private void Initialize(
throw new ArgumentNullException("outputWriter");
}
+ this.moduleHandler = null;
+
this.outputWriter = outputWriter;
#region Verifies rule extensions and loggers path
@@ -576,7 +587,7 @@ private void Initialize(
{
this.outputWriter.ThrowTerminatingError(
new ErrorRecord(
- ex,
+ ex,
ex.HResult.ToString("X", CultureInfo.CurrentCulture),
ErrorCategory.NotSpecified, this));
}
@@ -727,7 +738,7 @@ public IEnumerable GetRule(string[] moduleNames, string[] ruleNames)
if (null != ScriptRules)
{
rules = ScriptRules.Union(TokenRules).Union(DSCResourceRules);
- }
+ }
// Gets PowerShell Rules.
if (moduleNames != null)
@@ -781,7 +792,7 @@ private List GetExternalRule(string[] moduleNames)
{
shortModuleName = loadedModules.First().Name;
}
-
+
// Invokes Get-Command and Get-Help for each functions in the module.
posh.Commands.Clear();
posh.AddCommand("Get-Command").AddParameter("Module", shortModuleName);
@@ -801,7 +812,7 @@ private List GetExternalRule(string[] moduleNames)
item.Name.EndsWith("token", StringComparison.OrdinalIgnoreCase));
}
catch
- {
+ {
}
//Only add functions that are defined as rules.
@@ -811,7 +822,7 @@ private List GetExternalRule(string[] moduleNames)
// using Update-Help. This results in an interactive prompt - which we cannot handle
// Workaround to prevent Update-Help from running is to set the following reg key
// HKLM:\Software\Microsoft\PowerShell\DisablePromptToUpdateHelp
- // OR execute Update-Help in an elevated admin mode before running ScriptAnalyzer
+ // OR execute Update-Help in an elevated admin mode before running ScriptAnalyzer
Collection helpContent = null;
try
{
@@ -831,11 +842,11 @@ private List GetExternalRule(string[] moduleNames)
dynamic description = helpContent[0].Properties["Description"];
if (null != description && null != description.Value && description.Value.GetType().IsArray)
- {
+ {
desc = description.Value[0].Text;
}
}
-
+
rules.Add(new ExternalRule(funcInfo.Name, funcInfo.Name, desc, param.Name, param.ParameterType.FullName,
funcInfo.ModuleName, funcInfo.Module.Path));
}
@@ -980,7 +991,7 @@ internal IEnumerable GetExternalRecord(Ast ast, Token[] token,
// DiagnosticRecord may not be correctly returned from external rule.
try
- {
+ {
severity = (DiagnosticSeverity)Enum.Parse(typeof(DiagnosticSeverity), psobject.Properties["Severity"].Value.ToString());
message = psobject.Properties["Message"].Value.ToString();
extent = (IScriptExtent)psobject.Properties["Extent"].Value;
@@ -1028,11 +1039,11 @@ public Dictionary> CheckRuleExtension(string[] path, PathIn
string resolvedPath = string.Empty;
- // Users may provide a valid module path or name,
+ // Users may provide a valid module path or name,
// We have to identify the childPath is really a directory or just a module name.
// You can also consider following two commands.
// Get-ScriptAnalyzerRule -RuleExtension "ContosoAnalyzerRules"
- // Get-ScriptAnalyzerRule -RuleExtension "%USERPROFILE%\WindowsPowerShell\Modules\ContosoAnalyzerRules"
+ // Get-ScriptAnalyzerRule -RuleExtension "%USERPROFILE%\WindowsPowerShell\Modules\ContosoAnalyzerRules"
if (Path.GetDirectoryName(childPath) == string.Empty)
{
resolvedPath = childPath;
@@ -1041,21 +1052,21 @@ public Dictionary> CheckRuleExtension(string[] path, PathIn
{
resolvedPath = basePath
.GetResolvedPSPathFromPSPath(childPath).First().ToString();
- }
-
+ }
+
// Import the module
- InitialSessionState state = InitialSessionState.CreateDefault2();
+ InitialSessionState state = InitialSessionState.CreateDefault2();
using (System.Management.Automation.PowerShell posh =
System.Management.Automation.PowerShell.Create(state))
- {
+ {
posh.AddCommand("Import-Module").AddArgument(resolvedPath).AddParameter("PassThru");
Collection loadedModules = posh.Invoke();
- if (loadedModules != null
+ if (loadedModules != null
&& loadedModules.Count > 0
&& loadedModules.First().ExportedFunctions.Count > 0)
- {
- validModPaths.Add(resolvedPath);
- }
+ {
+ validModPaths.Add(resolvedPath);
+ }
}
}
catch
@@ -1134,14 +1145,14 @@ public Dictionary> CheckRuleExtension(string[] path, PathIn
}
#endregion
-
+
///
/// Analyzes a script file or a directory containing script files.
///
/// The path of the file or directory to analyze.
///
- /// If true, recursively searches the given file path and analyzes any
+ /// If true, recursively searches the given file path and analyzes any
/// script files that are found.
///
/// An enumeration of DiagnosticRecords that were found by rules.
@@ -1155,18 +1166,17 @@ public IEnumerable AnalyzePath(string path, bool searchRecursi
new ErrorRecord(
new FileNotFoundException(),
string.Format(CultureInfo.CurrentCulture, Strings.FileNotFound, path),
- ErrorCategory.InvalidArgument,
+ ErrorCategory.InvalidArgument,
this));
}
- // Precreate the list of script file paths to analyze. This
+ // Create in advance the list of script file paths to analyze. This
// is an optimization over doing the whole operation at once
// and calling .Concat on IEnumerables to join results.
this.BuildScriptPathList(path, searchRecursively, scriptFilePaths);
-
foreach (string scriptFilePath in scriptFilePaths)
{
- // Yield each record in the result so that the
+ // Yield each record in the result so that the
// caller can pull them one at a time
foreach (var diagnosticRecord in this.AnalyzeFile(scriptFilePath))
{
@@ -1219,8 +1229,8 @@ public IEnumerable AnalyzeScriptDefinition(string scriptDefini
}
private void BuildScriptPathList(
- string path,
- bool searchRecursively,
+ string path,
+ bool searchRecursively,
IList scriptFilePaths)
{
const string ps1Suffix = ".ps1";
@@ -1266,13 +1276,14 @@ private void BuildScriptPathList(
{
this.outputWriter.WriteError(
new ErrorRecord(
- new FileNotFoundException(),
- string.Format(CultureInfo.CurrentCulture, Strings.FileNotFound, path),
- ErrorCategory.InvalidArgument,
+ new FileNotFoundException(),
+ string.Format(CultureInfo.CurrentCulture, Strings.FileNotFound, path),
+ ErrorCategory.InvalidArgument,
this));
}
}
+
private IEnumerable AnalyzeFile(string filePath)
{
ScriptBlockAst scriptAst = null;
@@ -1297,6 +1308,28 @@ private IEnumerable AnalyzeFile(string filePath)
return null;
}
+ bool parseAgain = false;
+ if (moduleHandler != null && errors != null && errors.Length > 0)
+ {
+ foreach (ParseError error in errors.Where(IsModuleNotFoundError))
+ {
+ var moduleNames = moduleHandler.GetUnavailableModuleNameFromErrorExtent(error, scriptAst);
+ if (moduleNames != null)
+ {
+ parseAgain |= moduleNames.Any(x => moduleHandler.TrySaveModule(x));
+ }
+ }
+ }
+
+ //try parsing again
+ //var oldDefault = Runspace.DefaultRunspace;
+ //Runspace.DefaultRunspace = moduleHandler.Runspace;
+ if (parseAgain)
+ {
+ scriptAst = Parser.ParseFile(filePath, out scriptTokens, out errors);
+ }
+
+ //Runspace.DefaultRunspace = oldDefault;
if (errors != null && errors.Length > 0)
{
foreach (ParseError error in errors)
@@ -1310,7 +1343,6 @@ private IEnumerable AnalyzeFile(string filePath)
{
string manyParseErrorMessage = String.Format(CultureInfo.CurrentCulture, Strings.ParserErrorMessage, System.IO.Path.GetFileName(filePath));
this.outputWriter.WriteError(new ErrorRecord(new ParseException(manyParseErrorMessage), manyParseErrorMessage, ErrorCategory.ParserError, filePath));
-
return new List();
}
}
@@ -1327,19 +1359,25 @@ private IEnumerable AnalyzeFile(string filePath)
return this.AnalyzeSyntaxTree(scriptAst, scriptTokens, filePath);
}
+ private bool IsModuleNotFoundError(ParseError error)
+ {
+ return error.ErrorId != null
+ && error.ErrorId.Equals("ModuleNotFoundDuringParse", StringComparison.OrdinalIgnoreCase);
+ }
+
private bool IsSeverityAllowed(IEnumerable allowedSeverities, IRule rule)
{
- return severity == null
- || (allowedSeverities != null
- && rule != null
- && HasGetSeverity(rule)
+ return severity == null
+ || (allowedSeverities != null
+ && rule != null
+ && HasGetSeverity(rule)
&& allowedSeverities.Contains((uint)rule.GetSeverity()));
}
IEnumerable GetAllowedSeveritiesInInt()
{
- return severity != null
- ? severity.Select(item => (uint)Enum.Parse(typeof(DiagnosticSeverity), item, true))
+ return severity != null
+ ? severity.Select(item => (uint)Enum.Parse(typeof(DiagnosticSeverity), item, true))
: null;
}
@@ -1420,8 +1458,8 @@ private Tuple, List> SuppressRule(
///
/// An enumeration of DiagnosticRecords that were found by rules.
public IEnumerable AnalyzeSyntaxTree(
- ScriptBlockAst scriptAst,
- Token[] scriptTokens,
+ ScriptBlockAst scriptAst,
+ Token[] scriptTokens,
string filePath)
{
Dictionary> ruleSuppressions = new Dictionary>();
@@ -1463,7 +1501,7 @@ public IEnumerable AnalyzeSyntaxTree(
Helper.Instance.Tokens = scriptTokens;
}
-
+
#region Run ScriptRules
//Trim down to the leaf element of the filePath and pass it to Diagnostic Record
string fileName = filePathIsNullOrWhiteSpace ? String.Empty : System.IO.Path.GetFileName(filePath);
@@ -1495,8 +1533,8 @@ public IEnumerable AnalyzeSyntaxTree(
List suppressRuleErrors;
var ruleRecords = scriptRule.AnalyzeScript(scriptAst, scriptAst.Extent.File).ToList();
var records = Helper.Instance.SuppressRule(
- scriptRule.GetName(),
- ruleSuppressions,
+ scriptRule.GetName(),
+ ruleSuppressions,
ruleRecords,
out suppressRuleErrors);
result.AddRange(suppressRuleErrors);
diff --git a/Engine/ScriptAnalyzerEngine.csproj b/Engine/ScriptAnalyzerEngine.csproj
index 84442e131..abf8b7b6c 100644
--- a/Engine/ScriptAnalyzerEngine.csproj
+++ b/Engine/ScriptAnalyzerEngine.csproj
@@ -70,6 +70,7 @@
+
diff --git a/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 b/Tests/Engine/InvokeScriptAnalyzer.tests.ps1
index 953a4f6c1..bbdf62dda 100644
--- a/Tests/Engine/InvokeScriptAnalyzer.tests.ps1
+++ b/Tests/Engine/InvokeScriptAnalyzer.tests.ps1
@@ -19,7 +19,7 @@ Describe "Test available parameters" {
It "has a Path parameter" {
$params.ContainsKey("Path") | Should Be $true
}
-
+
It "accepts string" {
$params["Path"].ParameterType.FullName | Should Be "System.String"
}
@@ -29,8 +29,8 @@ Describe "Test available parameters" {
It "has a ScriptDefinition parameter" {
$params.ContainsKey("ScriptDefinition") | Should Be $true
}
-
- It "accepts string" {
+
+ It "accepts string" {
$params["ScriptDefinition"].ParameterType.FullName | Should Be "System.String"
}
}
@@ -69,6 +69,19 @@ Describe "Test available parameters" {
}
}
+ if (!$testingLibraryUsage)
+ {
+ Context "SaveDSCResourceDependency parameter" {
+ It "has the parameter" {
+ $params.ContainsKey("SaveDscResourceDependency") | Should Be $true
+ }
+
+ It "is a switch parameter" {
+ $params["SaveDscResourceDependency"].ParameterType.FullName | Should Be "System.Management.Automation.SwitchParameter"
+ }
+ }
+ }
+
Context "It has 2 parameter sets: File and ScriptDefinition" {
It "Has 2 parameter sets" {
$sa.ParameterSets.Count | Should Be 2
@@ -144,14 +157,14 @@ Describe "Test Path" {
if (!$testingLibraryUsage)
{
#There is probably a more concise way to do this but for now we will settle for this!
- Function GetFreeDrive ($freeDriveLen) {
+ Function GetFreeDrive ($freeDriveLen) {
$ordA = 65
$ordZ = 90
$freeDrive = ""
$freeDriveName = ""
do{
$freeDriveName = (1..$freeDriveLen | %{[char](Get-Random -Maximum $ordZ -Minimum $ordA)}) -join ''
- $freeDrive = $freeDriveName + ":"
+ $freeDrive = $freeDriveName + ":"
}while(Test-Path $freeDrive)
$freeDrive, $freeDriveName
}
@@ -180,7 +193,7 @@ Describe "Test Path" {
Context "When given a directory" {
$withoutPathWithDirectory = Invoke-ScriptAnalyzer -Recurse $directory\RecursionDirectoryTest
$withPathWithDirectory = Invoke-ScriptAnalyzer -Recurse -Path $directory\RecursionDirectoryTest
-
+
It "Has the same count as without Path parameter"{
$withoutPathWithDirectory.Count -eq $withPathWithDirectory.Count | Should Be $true
}
@@ -206,7 +219,7 @@ Describe "Test ExcludeRule" {
It "excludes 3 rules" {
$noViolations = Invoke-ScriptAnalyzer $directory\..\Rules\BadCmdlet.ps1 -ExcludeRule $rules | Where-Object {$rules -contains $_.RuleName}
- $noViolations.Count | Should Be 0
+ $noViolations.Count | Should Be 0
}
}
@@ -329,13 +342,13 @@ Describe "Test CustomizedRulePath" {
It "When supplied with a collection of paths" {
$customizedRulePath = Invoke-ScriptAnalyzer $directory\TestScript.ps1 -CustomRulePath ("$directory\CommunityAnalyzerRules", "$directory\SampleRule", "$directory\SampleRule\SampleRule2")
$customizedRulePath.Count | Should Be 3
- }
+ }
}
Context "When used incorrectly" {
- It "file cannot be found" {
+ It "file cannot be found" {
try
{
Invoke-ScriptAnalyzer $directory\TestScript.ps1 -CustomRulePath "Invalid CustomRulePath"
@@ -343,9 +356,9 @@ Describe "Test CustomizedRulePath" {
catch
{
if (-not $testingLibraryUsage)
- {
- $Error[0].FullyQualifiedErrorId | should match "PathNotFound,Microsoft.Windows.PowerShell.ScriptAnalyzer.Commands.InvokeScriptAnalyzerCommand"
- }
+ {
+ $Error[0].FullyQualifiedErrorId | should match "PathNotFound,Microsoft.Windows.PowerShell.ScriptAnalyzer.Commands.InvokeScriptAnalyzerCommand"
+ }
}
}
}
diff --git a/Tests/Engine/MissingDSCResource.ps1 b/Tests/Engine/MissingDSCResource.ps1
new file mode 100644
index 000000000..c61a94cc5
--- /dev/null
+++ b/Tests/Engine/MissingDSCResource.ps1
@@ -0,0 +1,4 @@
+Configuration SomeConfiguration
+{
+ Import-DscResource -ModuleName MyDSCResource
+}
\ No newline at end of file
diff --git a/Tests/Engine/ModuleDependencyHandler.tests.ps1 b/Tests/Engine/ModuleDependencyHandler.tests.ps1
new file mode 100644
index 000000000..225fe8082
--- /dev/null
+++ b/Tests/Engine/ModuleDependencyHandler.tests.ps1
@@ -0,0 +1,162 @@
+if (!(Get-Module PSScriptAnalyzer) -and !$testingLibraryUsage)
+{
+ Import-Module PSScriptAnalyzer
+}
+
+if ($testingLibraryUsage)
+{
+ return
+}
+
+$directory = Split-Path -Parent $MyInvocation.MyCommand.Path
+$violationFileName = 'MissingDSCResource.ps1'
+$violationFilePath = Join-Path $directory $violationFileName
+
+Describe "Resolve DSC Resource Dependency" {
+
+ Function Test-EnvironmentVariables($oldEnv)
+ {
+ $newEnv = Get-Item Env:\* | Sort-Object -Property Key
+ $newEnv.Count | Should Be $oldEnv.Count
+ foreach ($index in 1..$newEnv.Count)
+ {
+ $newEnv[$index].Key | Should Be $oldEnv[$index].Key
+ $newEnv[$index].Value | Should Be $oldEnv[$index].Value
+ }
+ }
+
+ Context "Module handler class" {
+ $moduleHandlerType = [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.ModuleDependencyHandler]
+ $oldEnvVars = Get-Item Env:\* | Sort-Object -Property Key
+ $oldPSModulePath = $env:PSModulePath
+ It "Sets defaults correctly" {
+ $rsp = [runspacefactory]::CreateRunspace()
+ $rsp.Open()
+ $depHandler = $moduleHandlerType::new($rsp)
+
+ $expectedPath = [System.IO.Path]::GetTempPath()
+ $depHandler.TempPath | Should Be $expectedPath
+
+ $expectedLocalAppDataPath = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::LocalApplicationData)
+ $depHandler.LocalAppDataPath | Should Be $expectedLocalAppDataPath
+
+ $expectedModuleRepository = "PSGallery"
+ $depHandler.ModuleRepository | Should Be $expectedModuleRepository
+
+ $expectedPssaAppDataPath = Join-Path $depHandler.LocalAppDataPath "PSScriptAnalyzer"
+ $depHandler.PSSAAppDataPath | Should Be $expectedPssaAppDataPath
+
+ $expectedPSModulePath = $oldPSModulePath + [System.IO.Path]::PathSeparator + $depHandler.TempModulePath
+ $env:PSModulePath | Should Be $expectedPSModulePath
+
+ $depHandler.Dispose()
+ $rsp.Dispose()
+ }
+
+ It "Keeps the environment variables unchanged" {
+ Test-EnvironmentVariables($oldEnvVars)
+ }
+
+ It "Throws if runspace is null" {
+ {$moduleHandlerType::new($null)} | Should Throw
+ }
+
+ It "Throws if runspace is not opened" {
+ $rsp = [runspacefactory]::CreateRunspace()
+ {$moduleHandlerType::new($rsp)} | Should Throw
+ $rsp.Dispose()
+ }
+
+ It "Extracts 1 module name" {
+ $sb = @"
+{Configuration SomeConfiguration
+{
+ Import-DscResource -ModuleName SomeDscModule1
+}}
+"@
+ $tokens = $null
+ $parseError = $null
+ $ast = [System.Management.Automation.Language.Parser]::ParseInput($sb, [ref]$tokens, [ref]$parseError)
+ $resultModuleNames = $moduleHandlerType::GetModuleNameFromErrorExtent($parseError[0], $ast).ToArray()
+ $resultModuleNames[0] | Should Be 'SomeDscModule1'
+ }
+
+ It "Extracts more than 1 module names" {
+ $sb = @"
+{Configuration SomeConfiguration
+{
+ Import-DscResource -ModuleName SomeDscModule1,SomeDscModule2,SomeDscModule3
+}}
+"@
+ $tokens = $null
+ $parseError = $null
+ $ast = [System.Management.Automation.Language.Parser]::ParseInput($sb, [ref]$tokens, [ref]$parseError)
+ $resultModuleNames = $moduleHandlerType::GetModuleNameFromErrorExtent($parseError[0], $ast).ToArray()
+ $resultModuleNames[0] | Should Be 'SomeDscModule1'
+ $resultModuleNames[1] | Should Be 'SomeDscModule2'
+ $resultModuleNames[2] | Should Be 'SomeDscModule3'
+ }
+ }
+
+ Context "Invoke-ScriptAnalyzer without switch" {
+ It "Has parse errors" {
+ $dr = Invoke-ScriptAnalyzer -Path $violationFilePath -ErrorVariable parseErrors -ErrorAction SilentlyContinue
+ $parseErrors.Count | Should Be 1
+ }
+ }
+
+ Context "Invoke-ScriptAnalyzer without switch but with module in temp path" {
+ $oldEnvVars = Get-Item Env:\* | Sort-Object -Property Key
+ $moduleName = "MyDscResource"
+ $modulePath = Join-Path (Join-Path (Join-Path (Split-Path $directory) "Rules") "DSCResources") $moduleName
+ # Save the current environment variables
+ $oldLocalAppDataPath = $env:LOCALAPPDATA
+ $oldTempPath = $env:TEMP
+ $oldPSModulePath = $env:PSModulePath
+
+ # set the environment variables
+ $tempPath = Join-Path $oldTempPath ([guid]::NewGUID()).ToString()
+ $newLocalAppDataPath = Join-Path $tempPath "LocalAppData"
+ $newTempPath = Join-Path $tempPath "Temp"
+ $env:LOCALAPPDATA = $newLocalAppDataPath
+ $env:TEMP = $newTempPath
+
+ # create the temporary directories
+ New-Item -Type Directory -Path $newLocalAppDataPath
+ New-Item -Type Directory -Path $newTempPath
+
+ # create and dispose module dependency handler object
+ # to setup the temporary module
+ $rsp = [runspacefactory]::CreateRunspace()
+ $rsp.Open()
+ $depHandler = [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.ModuleDependencyHandler]::new($rsp)
+ $pssaAppDataPath = $depHandler.PSSAAppDataPath
+ $tempModulePath = $depHandler.TempModulePath
+ $rsp.Dispose()
+ $depHandler.Dispose()
+
+ # copy myresource module to the temporary location
+ # we could let the module dependency handler download it from psgallery
+ Copy-Item -Recurse -Path $modulePath -Destination $tempModulePath
+
+ It "Doesn't have parse errors" {
+ # invoke script analyzer
+ $dr = Invoke-ScriptAnalyzer -Path $violationFilePath -ErrorVariable parseErrors -ErrorAction SilentlyContinue
+ $dr.Count | Should Be 0
+ }
+
+ It "Keeps PSModulePath unchanged before and after invocation" {
+ $dr = Invoke-ScriptAnalyzer -Path $violationFilePath -ErrorVariable parseErrors -ErrorAction SilentlyContinue
+ $env:PSModulePath | Should Be $oldPSModulePath
+ }
+ #restore environment variables and clean up temporary location
+ $env:LOCALAPPDATA = $oldLocalAppDataPath
+ $env:TEMP = $oldTempPath
+ Remove-Item -Recurse -Path $tempModulePath -Force
+ Remove-Item -Recurse -Path $tempPath -Force
+
+ It "Keeps the environment variables unchanged" {
+ Test-EnvironmentVariables($oldEnvVars)
+ }
+ }
+}
\ No newline at end of file