diff --git a/Engine/Commands/GetScriptAnalyzerRuleCommand.cs b/Engine/Commands/GetScriptAnalyzerRuleCommand.cs
index ed4799c40..a914e1f59 100644
--- a/Engine/Commands/GetScriptAnalyzerRuleCommand.cs
+++ b/Engine/Commands/GetScriptAnalyzerRuleCommand.cs
@@ -24,7 +24,7 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Commands
/// GetScriptAnalyzerRuleCommand: Cmdlet to list all the analyzer rule names and descriptions.
///
[Cmdlet(VerbsCommon.Get, "ScriptAnalyzerRule", HelpUri = "http://go.microsoft.com/fwlink/?LinkId=525913")]
- public class GetScriptAnalyzerRuleCommand : PSCmdlet
+ public class GetScriptAnalyzerRuleCommand : PSCmdlet, IOutputWriter
{
#region Parameters
///
@@ -69,12 +69,6 @@ public string[] Severity
#endregion Parameters
- #region Private Members
-
- Dictionary> validationResults = new Dictionary>();
-
- #endregion
-
#region Overrides
///
@@ -82,43 +76,7 @@ public string[] Severity
///
protected override void BeginProcessing()
{
- #region Set PSCmdlet property of Helper
-
- Helper.Instance.MyCmdlet = this;
-
- #endregion
- // Verifies rule extensions
- if (customizedRulePath != null)
- {
- validationResults = ScriptAnalyzer.Instance.CheckRuleExtension(customizedRulePath, this);
- foreach (string extension in validationResults["InvalidPaths"])
- {
- WriteWarning(string.Format(CultureInfo.CurrentCulture,Strings.MissingRuleExtension, extension));
- }
- }
- else
- {
- validationResults.Add("InvalidPaths", new List());
- validationResults.Add("ValidModPaths", new List());
- validationResults.Add("ValidDllPaths", new List());
- }
-
- try
- {
- if (validationResults["ValidDllPaths"].Count == 0)
- {
- ScriptAnalyzer.Instance.Initialize();
- }
- else
- {
- ScriptAnalyzer.Instance.Initilaize(validationResults);
- }
- }
- catch (Exception ex)
- {
- ThrowTerminatingError(new ErrorRecord(ex, ex.HResult.ToString("X", CultureInfo.CurrentCulture),
- ErrorCategory.NotSpecified, this));
- }
+ ScriptAnalyzer.Instance.Initialize(this, customizedRulePath);
}
///
@@ -126,11 +84,7 @@ protected override void BeginProcessing()
///
protected override void ProcessRecord()
{
- string[] modNames = null;
- if (validationResults["ValidModPaths"].Count > 0)
- {
- modNames = validationResults["ValidModPaths"].ToArray();
- }
+ string[] modNames = ScriptAnalyzer.Instance.GetValidModulePaths();
IEnumerable rules = ScriptAnalyzer.Instance.GetRule(modNames, name);
if (rules == null)
diff --git a/Engine/Commands/InvokeScriptAnalyzerCommand.cs b/Engine/Commands/InvokeScriptAnalyzerCommand.cs
index 2fa611404..d50b40795 100644
--- a/Engine/Commands/InvokeScriptAnalyzerCommand.cs
+++ b/Engine/Commands/InvokeScriptAnalyzerCommand.cs
@@ -31,7 +31,7 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Commands
/// InvokeScriptAnalyzerCommand: Cmdlet to statically check PowerShell scripts.
///
[Cmdlet(VerbsLifecycle.Invoke, "ScriptAnalyzer", HelpUri = "http://go.microsoft.com/fwlink/?LinkId=525914")]
- public class InvokeScriptAnalyzerCommand : PSCmdlet
+ public class InvokeScriptAnalyzerCommand : PSCmdlet, IOutputWriter
{
#region Parameters
///
@@ -128,81 +128,23 @@ public SwitchParameter SuppressedOnly
#region Private Members
Dictionary> validationResults = new Dictionary>();
- private ScriptBlockAst ast = null;
- private IEnumerable rules = null;
#endregion
- #region Ovserrides
+ #region Overrides
///
/// Imports all known rules and loggers.
///
protected override void BeginProcessing()
{
- #region Set PSCmdlet property of Helper
-
- Helper.Instance.MyCmdlet = this;
-
- #endregion
-
- #region Verifies rule extensions and loggers path
-
- List paths = new List();
-
- if (customizedRulePath != null) paths.AddRange(customizedRulePath.ToList());
-
- if (paths.Count > 0)
- {
- validationResults = ScriptAnalyzer.Instance.CheckRuleExtension(paths.ToArray(), this);
- foreach (string extension in validationResults["InvalidPaths"])
- {
- WriteWarning(string.Format(CultureInfo.CurrentCulture, Strings.MissingRuleExtension, extension));
- }
- }
- else
- {
- validationResults.Add("InvalidPaths", new List());
- validationResults.Add("ValidModPaths", new List());
- validationResults.Add("ValidDllPaths", new List());
- }
-
- #endregion
-
- #region Initializes Rules
-
- try
- {
- if (validationResults["ValidDllPaths"].Count == 0 &&
- validationResults["ValidModPaths"].Count == 0)
- {
- ScriptAnalyzer.Instance.Initialize();
- }
- else
- {
- ScriptAnalyzer.Instance.Initilaize(validationResults);
- }
- }
- catch (Exception ex)
- {
- ThrowTerminatingError(new ErrorRecord(ex, ex.HResult.ToString("X", CultureInfo.CurrentCulture),
- ErrorCategory.NotSpecified, this));
- }
-
- #endregion
-
- #region Verify rules
-
- rules = ScriptAnalyzer.Instance.ScriptRules.Union(
- ScriptAnalyzer.Instance.TokenRules).Union(
- ScriptAnalyzer.Instance.ExternalRules ?? Enumerable.Empty());
-
- if (rules == null || rules.Count() == 0)
- {
- ThrowTerminatingError(new ErrorRecord(new Exception(), string.Format(CultureInfo.CurrentCulture, Strings.RulesNotFound), ErrorCategory.ResourceExists, this));
- }
-
- #endregion
+ ScriptAnalyzer.Instance.Initialize(
+ this,
+ customizedRulePath,
+ this.includeRule,
+ this.excludeRule,
+ this.severity,
+ this.suppressedOnly);
}
///
@@ -221,452 +163,15 @@ protected override void ProcessRecord()
private void ProcessPath(string path)
{
- const string ps1Suffix = "ps1";
- const string psm1Suffix = "psm1";
- const string psd1Suffix = "psd1";
-
- if (path == null)
- {
- ThrowTerminatingError(new ErrorRecord(new FileNotFoundException(),
- string.Format(CultureInfo.CurrentCulture, Strings.FileNotFound, path),
- ErrorCategory.InvalidArgument, this));
- }
-
- if (Directory.Exists(path))
- {
- if (recurse)
- {
-
- foreach (string filePath in Directory.GetFiles(path))
- {
- ProcessPath(filePath);
- }
- foreach (string filePath in Directory.GetDirectories(path))
- {
- ProcessPath(filePath);
- }
- }
- else
- {
- foreach (string filePath in Directory.GetFiles(path))
- {
- ProcessPath(filePath);
- }
- }
- }
- else if (File.Exists(path))
- {
- if ((path.Length > ps1Suffix.Length && path.Substring(path.Length - ps1Suffix.Length).Equals(ps1Suffix, StringComparison.OrdinalIgnoreCase)) ||
- (path.Length > psm1Suffix.Length && path.Substring(path.Length - psm1Suffix.Length).Equals(psm1Suffix, StringComparison.OrdinalIgnoreCase)) ||
- (path.Length > psd1Suffix.Length && path.Substring(path.Length - psd1Suffix.Length).Equals(psd1Suffix, StringComparison.OrdinalIgnoreCase)))
- {
- WriteVerbose(string.Format(CultureInfo.CurrentCulture, Strings.VerboseFileMessage, path));
- AnalyzeFile(path);
- }
- }
- else
- {
- WriteError(new ErrorRecord(new FileNotFoundException(), string.Format(CultureInfo.CurrentCulture, Strings.FileNotFound, path), ErrorCategory.InvalidArgument, this));
- }
-
- }
-
- ConcurrentBag diagnostics;
- ConcurrentBag suppressed;
- Dictionary> ruleSuppressions;
- List includeRegexList;
- List excludeRegexList;
- ConcurrentDictionary> ruleDictionary;
-
- ///
- /// Analyzes a single script file.
- ///
- /// The path to the file ot analyze
- private void AnalyzeFile(string filePath)
- {
- Token[] tokens = null;
- ParseError[] errors = null;
- ConcurrentBag diagnostics = new ConcurrentBag();
- ConcurrentBag suppressed = new ConcurrentBag();
- BlockingCollection> verboseOrErrors = new BlockingCollection>();
-
- // Use a List of KVP rather than dictionary, since for a script containing inline functions with same signature, keys clash
- List> cmdInfoTable = new List>();
-
- //Check wild card input for the Include/ExcludeRules and create regex match patterns
- includeRegexList = new List();
- excludeRegexList = new List();
- if (includeRule != null)
- {
- foreach (string rule in includeRule)
- {
- Regex includeRegex = new Regex(String.Format("^{0}$", Regex.Escape(rule).Replace(@"\*", ".*")), RegexOptions.IgnoreCase);
- includeRegexList.Add(includeRegex);
- }
- }
- if (excludeRule != null)
- {
- foreach (string rule in excludeRule)
- {
- Regex excludeRegex = new Regex(String.Format("^{0}$", Regex.Escape(rule).Replace(@"\*", ".*")), RegexOptions.IgnoreCase);
- excludeRegexList.Add(excludeRegex);
- }
- }
-
-
- //Parse the file
- if (File.Exists(filePath))
- {
- ast = Parser.ParseFile(filePath, out tokens, out errors);
- }
- else
- {
- ThrowTerminatingError(new ErrorRecord(new FileNotFoundException(),
- string.Format(CultureInfo.CurrentCulture, Strings.InvalidPath, filePath),
- ErrorCategory.InvalidArgument, filePath));
- }
-
- if (errors != null && errors.Length > 0)
- {
- foreach (ParseError error in errors)
- {
- string parseErrorMessage = String.Format(CultureInfo.CurrentCulture, Strings.ParserErrorFormat, error.Extent.File, error.Message.TrimEnd('.'), error.Extent.StartLineNumber, error.Extent.StartColumnNumber);
- WriteError(new ErrorRecord(new ParseException(parseErrorMessage), parseErrorMessage, ErrorCategory.ParserError, error.ErrorId));
- }
- }
-
- if (errors.Length > 10)
- {
- string manyParseErrorMessage = String.Format(CultureInfo.CurrentCulture, Strings.ParserErrorMessage, System.IO.Path.GetFileName(filePath));
- WriteError(new ErrorRecord(new ParseException(manyParseErrorMessage), manyParseErrorMessage, ErrorCategory.ParserError, filePath));
-
- return;
- }
-
- ruleSuppressions = Helper.Instance.GetRuleSuppression(ast);
-
- foreach (List ruleSuppressionsList in ruleSuppressions.Values)
- {
- foreach (RuleSuppression ruleSuppression in ruleSuppressionsList)
- {
- if (!String.IsNullOrWhiteSpace(ruleSuppression.Error))
- {
- WriteError(new ErrorRecord(new ArgumentException(ruleSuppression.Error), ruleSuppression.Error, ErrorCategory.InvalidArgument, ruleSuppression));
- }
- }
- }
-
- #region Run VariableAnalysis
- try
- {
- Helper.Instance.InitializeVariableAnalysis(ast);
- }
- catch { }
- #endregion
-
- Helper.Instance.Tokens = tokens;
-
- #region Run ScriptRules
- //Trim down to the leaf element of the filePath and pass it to Diagnostic Record
- string fileName = System.IO.Path.GetFileName(filePath);
-
- if (ScriptAnalyzer.Instance.ScriptRules != null)
- {
- var tasks = ScriptAnalyzer.Instance.ScriptRules.Select(scriptRule => Task.Factory.StartNew(() =>
- {
- bool includeRegexMatch = false;
- bool excludeRegexMatch = false;
-
- foreach (Regex include in includeRegexList)
- {
- if (include.IsMatch(scriptRule.GetName()))
- {
- includeRegexMatch = true;
- break;
- }
- }
-
- foreach (Regex exclude in excludeRegexList)
- {
- if (exclude.IsMatch(scriptRule.GetName()))
- {
- excludeRegexMatch = true;
- break;
- }
- }
-
- if ((includeRule == null || includeRegexMatch) && (excludeRule == null || !excludeRegexMatch))
- {
- List
public class Helper
{
+ #region Private members
+
+ private CommandInvocationIntrinsics invokeCommand;
+ private IOutputWriter outputWriter;
+
+ #endregion
+
#region Singleton
private static object syncRoot = new Object();
@@ -41,15 +48,21 @@ public static Helper Instance
{
if (instance == null)
{
- lock (syncRoot)
- {
- if (instance == null)
- instance = new Helper();
- }
+ Instance = new Helper();
}
return instance;
}
+ internal set
+ {
+ lock (syncRoot)
+ {
+ if (instance == null)
+ {
+ instance = value;
+ }
+ }
+ }
}
#endregion
@@ -66,11 +79,6 @@ public static Helper Instance
///
private Dictionary AliasToCmdletDictionary;
- ///
- /// ScriptAnalyzer Cmdlet, used for getting commandinfos of other commands.
- ///
- public PSCmdlet MyCmdlet { get; set; }
-
internal TupleComparer tupleComparer = new TupleComparer();
///
@@ -93,6 +101,32 @@ public static Helper Instance
#endregion
+ ///
+ /// Initializes the Helper class.
+ ///
+ private Helper()
+ {
+ }
+
+ ///
+ /// Initializes the Helper class.
+ ///
+ ///
+ /// A CommandInvocationIntrinsics instance for use in gathering
+ /// information about available commands and aliases.
+ ///
+ ///
+ /// An IOutputWriter instance for use in writing output
+ /// to the PowerShell environment.
+ ///
+ public Helper(
+ CommandInvocationIntrinsics invokeCommand,
+ IOutputWriter outputWriter)
+ {
+ this.invokeCommand = invokeCommand;
+ this.outputWriter = outputWriter;
+ }
+
#region Methods
///
/// Initialize : Initializes dictionary of alias.
@@ -104,7 +138,7 @@ public void Initialize()
KeywordBlockDictionary = new Dictionary>>(StringComparer.OrdinalIgnoreCase);
VariableAnalysisDictionary = new Dictionary();
- IEnumerable aliases = MyCmdlet.InvokeCommand.GetCommands("*", CommandTypes.Alias, true);
+ IEnumerable aliases = this.invokeCommand.GetCommands("*", CommandTypes.Alias, true);
foreach (AliasInfo aliasInfo in aliases)
{
@@ -317,7 +351,7 @@ public bool PositionalParameterUsed(CommandAst cmdAst)
///
public CommandInfo GetCommandInfo(string name, CommandTypes commandType = CommandTypes.All)
{
- return Helper.Instance.MyCmdlet.InvokeCommand.GetCommand(name, commandType);
+ return this.invokeCommand.GetCommand(name, commandType);
}
///
@@ -914,7 +948,7 @@ public Tuple, List> SuppressRule(string
{
ruleSuppression.Error = String.Format(CultureInfo.CurrentCulture, Strings.RuleSuppressionErrorFormat, ruleSuppression.StartAttributeLine,
System.IO.Path.GetFileName(diagnostics.First().Extent.File), String.Format(Strings.RuleSuppressionIDError, ruleSuppression.RuleSuppressionID));
- Helper.Instance.MyCmdlet.WriteError(new ErrorRecord(new ArgumentException(ruleSuppression.Error), ruleSuppression.Error, ErrorCategory.InvalidArgument, ruleSuppression));
+ this.outputWriter.WriteError(new ErrorRecord(new ArgumentException(ruleSuppression.Error), ruleSuppression.Error, ErrorCategory.InvalidArgument, ruleSuppression));
}
}
diff --git a/Engine/IOutputWriter.cs b/Engine/IOutputWriter.cs
new file mode 100644
index 000000000..8e3ee3a3b
--- /dev/null
+++ b/Engine/IOutputWriter.cs
@@ -0,0 +1,52 @@
+//
+// Copyright (c) Microsoft Corporation.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+using System.Management.Automation;
+
+namespace Microsoft.Windows.PowerShell.ScriptAnalyzer
+{
+ ///
+ /// Provides an interface for writing output to a PowerShell session.
+ ///
+ public interface IOutputWriter
+ {
+ ///
+ /// Writes an error to the session.
+ ///
+ /// The ErrorRecord to write.
+ void WriteError(ErrorRecord error);
+
+ ///
+ /// Writes a warning to the session.
+ ///
+ /// The warning string to write.
+ void WriteWarning(string message);
+
+ ///
+ /// Writes a verbose message to the session.
+ ///
+ /// The verbose message to write.
+ void WriteVerbose(string message);
+
+ ///
+ /// Writes a debug message to the session.
+ ///
+ /// The debug message to write.
+ void WriteDebug(string message);
+
+ ///
+ /// Throws a terminating error in the session.
+ ///
+ /// The ErrorRecord which describes the failure.
+ void ThrowTerminatingError(ErrorRecord record);
+ }
+}
diff --git a/Engine/ScriptAnalyzer.cs b/Engine/ScriptAnalyzer.cs
index 24c2d0152..82e17f39e 100644
--- a/Engine/ScriptAnalyzer.cs
+++ b/Engine/ScriptAnalyzer.cs
@@ -24,14 +24,24 @@
using System.Management.Automation.Runspaces;
using System.Reflection;
using System.Globalization;
+using System.Collections.Concurrent;
+using System.Threading.Tasks;
namespace Microsoft.Windows.PowerShell.ScriptAnalyzer
{
- internal class ScriptAnalyzer
+ public sealed class ScriptAnalyzer
{
- #region Private memebers
+ #region Private members
+ private IOutputWriter outputWriter;
private CompositionContainer container;
+ Dictionary> validationResults = new Dictionary>();
+ string[] includeRule;
+ string[] excludeRule;
+ string[] severity;
+ List includeRegexList;
+ List excludeRegexList;
+ bool suppressedOnly;
#endregion
@@ -74,7 +84,7 @@ public static ScriptAnalyzer Instance
[ImportMany]
public IEnumerable DSCResourceRules { get; private set; }
- public List ExternalRules { get; private set; }
+ internal List ExternalRules { get; set; }
#endregion
@@ -83,43 +93,177 @@ public static ScriptAnalyzer Instance
///
/// Initialize : Initializes default rules, loggers and helper.
///
- public void Initialize()
+ internal void Initialize(
+ TCmdlet cmdlet,
+ string[] customizedRulePath = null,
+ string[] includeRuleNames = null,
+ string[] excludeRuleNames = null,
+ string[] severity = null,
+ bool suppressedOnly = false)
+ where TCmdlet : PSCmdlet, IOutputWriter
{
- // Clear external rules for each invoke.
- ExternalRules = new List();
+ if (cmdlet == null)
+ {
+ throw new ArgumentNullException("cmdlet");
+ }
- // Initialize helper
- Helper.Instance.Initialize();
+ this.Initialize(
+ cmdlet,
+ cmdlet.SessionState.Path,
+ cmdlet.SessionState.InvokeCommand,
+ customizedRulePath,
+ includeRuleNames,
+ excludeRuleNames,
+ severity,
+ suppressedOnly);
+ }
- // An aggregate catalog that combines multiple catalogs.
- using (AggregateCatalog catalog = new AggregateCatalog())
+ ///
+ /// Initialize : Initializes default rules, loggers and helper.
+ ///
+ public void Initialize(
+ Runspace runspace,
+ IOutputWriter outputWriter,
+ string[] customizedRulePath = null,
+ string[] includeRuleNames = null,
+ string[] excludeRuleNames = null,
+ string[] severity = null,
+ bool suppressedOnly = false)
+ {
+ if (runspace == null)
{
- // Adds all the parts found in the same directory.
- string dirName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
+ throw new ArgumentNullException("runspace");
+ }
- // Assembly.GetExecutingAssembly().Location
- catalog.Catalogs.Add(new DirectoryCatalog(dirName));
+ this.Initialize(
+ outputWriter,
+ runspace.SessionStateProxy.Path,
+ runspace.SessionStateProxy.InvokeCommand,
+ customizedRulePath,
+ includeRuleNames,
+ excludeRuleNames,
+ severity,
+ suppressedOnly);
+ }
- // Create the CompositionContainer with the parts in the catalog.
- container = new CompositionContainer(catalog);
+ private void Initialize(
+ IOutputWriter outputWriter,
+ PathIntrinsics path,
+ CommandInvocationIntrinsics invokeCommand,
+ string[] customizedRulePath,
+ string[] includeRuleNames,
+ string[] excludeRuleNames,
+ string[] severity,
+ bool suppressedOnly = false)
+ {
+ if (outputWriter == null)
+ {
+ throw new ArgumentNullException("outputWriter");
+ }
- // Fill the imports of this object.
- try
+ this.outputWriter = outputWriter;
+
+ #region Verifies rule extensions and loggers path
+
+ List paths = this.GetValidCustomRulePaths(customizedRulePath, path);
+
+ #endregion
+
+ #region Initializes Rules
+
+ this.severity = severity;
+ this.suppressedOnly = suppressedOnly;
+ this.includeRule = includeRuleNames;
+ this.excludeRule = excludeRuleNames;
+ this.includeRegexList = new List();
+ this.excludeRegexList = new List();
+
+ //Check wild card input for the Include/ExcludeRules and create regex match patterns
+ if (this.includeRule != null)
+ {
+ foreach (string rule in includeRule)
{
- container.ComposeParts(this);
+ Regex includeRegex = new Regex(String.Format("^{0}$", Regex.Escape(rule).Replace(@"\*", ".*")), RegexOptions.IgnoreCase);
+ this.includeRegexList.Add(includeRegex);
}
- catch (CompositionException compositionException)
+ }
+ if (this.excludeRule != null)
+ {
+ foreach (string rule in excludeRule)
{
- Console.WriteLine(compositionException.ToString());
+ Regex excludeRegex = new Regex(String.Format("^{0}$", Regex.Escape(rule).Replace(@"\*", ".*")), RegexOptions.IgnoreCase);
+ this.excludeRegexList.Add(excludeRegex);
}
}
+
+ try
+ {
+ this.LoadRules(this.validationResults, invokeCommand);
+ }
+ catch (Exception ex)
+ {
+ this.outputWriter.ThrowTerminatingError(
+ new ErrorRecord(
+ ex,
+ ex.HResult.ToString("X", CultureInfo.CurrentCulture),
+ ErrorCategory.NotSpecified, this));
+ }
+
+ #endregion
+
+ #region Verify rules
+
+ IEnumerable rules =
+ this.ScriptRules.Union(
+ this.TokenRules).Union(
+ this.ExternalRules ?? Enumerable.Empty());
+
+ // Ensure that rules were actually loaded
+ if (rules == null || rules.Count() == 0)
+ {
+ this.outputWriter.ThrowTerminatingError(
+ new ErrorRecord(
+ new Exception(),
+ string.Format(
+ CultureInfo.CurrentCulture,
+ Strings.RulesNotFound),
+ ErrorCategory.ResourceExists,
+ this));
+ }
+
+ #endregion
}
- ///
- /// Initilaize : Initializes default rules, external rules and loggers.
- ///
- /// Path validation result.
- public void Initilaize(Dictionary> result)
+ private List GetValidCustomRulePaths(string[] customizedRulePath, PathIntrinsics path)
+ {
+ List paths = new List();
+
+ if (customizedRulePath != null)
+ {
+ paths.AddRange(
+ customizedRulePath.ToList());
+ }
+
+ if (paths.Count > 0)
+ {
+ this.validationResults = this.CheckRuleExtension(paths.ToArray(), path);
+ foreach (string extension in this.validationResults["InvalidPaths"])
+ {
+ this.outputWriter.WriteWarning(string.Format(CultureInfo.CurrentCulture, Strings.MissingRuleExtension, extension));
+ }
+ }
+ else
+ {
+ this.validationResults = new Dictionary>();
+ this.validationResults.Add("InvalidPaths", new List());
+ this.validationResults.Add("ValidModPaths", new List());
+ this.validationResults.Add("ValidDllPaths", new List());
+ }
+
+ return paths;
+ }
+
+ private void LoadRules(Dictionary> result, CommandInvocationIntrinsics invokeCommand)
{
List paths = new List();
@@ -127,8 +271,12 @@ public void Initilaize(Dictionary> result)
ExternalRules = new List();
// Initialize helper
+ Helper.Instance = new Helper(invokeCommand, this.outputWriter);
Helper.Instance.Initialize();
+ // Clear external rules for each invoke.
+ ExternalRules = new List();
+
// An aggregate catalog that combines multiple catalogs.
using (AggregateCatalog catalog = new AggregateCatalog())
{
@@ -140,7 +288,7 @@ public void Initilaize(Dictionary> result)
paths = result.ContainsKey("ValidDllPaths") ? result["ValidDllPaths"] : result["ValidPaths"];
foreach (string path in paths)
{
- if (String.Equals(Path.GetExtension(path),".dll",StringComparison.OrdinalIgnoreCase))
+ if (String.Equals(Path.GetExtension(path), ".dll", StringComparison.OrdinalIgnoreCase))
{
catalog.Catalogs.Add(new AssemblyCatalog(path));
}
@@ -160,7 +308,7 @@ public void Initilaize(Dictionary> result)
}
catch (CompositionException compositionException)
{
- Console.WriteLine(compositionException.ToString());
+ this.outputWriter.WriteWarning(compositionException.ToString());
}
}
@@ -169,6 +317,18 @@ public void Initilaize(Dictionary> result)
ExternalRules = GetExternalRule(result["ValidModPaths"].ToArray());
}
+ internal string[] GetValidModulePaths()
+ {
+ List validModulePaths = null;
+
+ if (!this.validationResults.TryGetValue("ValidModPaths", out validModulePaths))
+ {
+ validModulePaths = new List();
+ }
+
+ return validModulePaths.ToArray();
+ }
+
public IEnumerable GetRule(string[] moduleNames, string[] ruleNames)
{
IEnumerable results = null;
@@ -196,9 +356,9 @@ public IEnumerable GetRule(string[] moduleNames, string[] ruleNames)
}
results = from rule in rules
- from regex in regexList
- where regex.IsMatch(rule.GetName())
- select rule;
+ from regex in regexList
+ where regex.IsMatch(rule.GetName())
+ select rule;
}
else
{
@@ -208,7 +368,7 @@ where regex.IsMatch(rule.GetName())
return results;
}
- public List GetExternalRule(string[] moduleNames)
+ private List GetExternalRule(string[] moduleNames)
{
List rules = new List();
@@ -247,15 +407,15 @@ public List GetExternalRule(string[] moduleNames)
ParameterMetadata param = funcInfo.Parameters.Values
.First(item => item.Name.EndsWith("ast", StringComparison.OrdinalIgnoreCase) ||
item.Name.EndsWith("token", StringComparison.OrdinalIgnoreCase));
-
+
//Only add functions that are defined as rules.
if (param != null)
{
script = string.Format(CultureInfo.CurrentCulture, "(Get-Help -Name {0}).Description | Out-String", funcInfo.Name);
- string desc =posh.AddScript(script).Invoke()[0].ImmediateBaseObject.ToString()
+ string desc = posh.AddScript(script).Invoke()[0].ImmediateBaseObject.ToString()
.Replace("\r\n", " ").Trim();
- rules.Add(new ExternalRule(funcInfo.Name, funcInfo.Name, desc, param.Name,param.ParameterType.FullName,
+ rules.Add(new ExternalRule(funcInfo.Name, funcInfo.Name, desc, param.Name, param.ParameterType.FullName,
funcInfo.ModuleName, funcInfo.Module.Path));
}
}
@@ -274,7 +434,7 @@ public List GetExternalRule(string[] moduleNames)
///
///
///
- public IEnumerable GetExternalRecord(Ast ast, Token[] token, ExternalRule[] rules, InvokeScriptAnalyzerCommand command, string filePath)
+ internal IEnumerable GetExternalRecord(Ast ast, Token[] token, ExternalRule[] rules, string filePath)
{
// Defines InitialSessionState.
InitialSessionState state = InitialSessionState.CreateDefault2();
@@ -341,7 +501,7 @@ public IEnumerable GetExternalRecord(Ast ast, Token[] token, E
{
// Find all AstTypes that appeared in rule groups.
IEnumerable childAsts = ast.FindAll(new Func((testAst) =>
- (astRuleGroup.Key.IndexOf(testAst.GetType().FullName,StringComparison.OrdinalIgnoreCase) != -1)), false);
+ (astRuleGroup.Key.IndexOf(testAst.GetType().FullName, StringComparison.OrdinalIgnoreCase) != -1)), false);
foreach (Ast childAst in childAsts)
{
@@ -393,7 +553,7 @@ public IEnumerable GetExternalRecord(Ast ast, Token[] token, E
if (psobject.ImmediateBaseObject is ErrorRecord)
{
ErrorRecord record = (ErrorRecord)psobject.ImmediateBaseObject;
- command.WriteError(record);
+ this.outputWriter.WriteError(record);
continue;
}
@@ -407,7 +567,7 @@ public IEnumerable GetExternalRecord(Ast ast, Token[] token, E
}
catch (Exception ex)
{
- command.WriteError(new ErrorRecord(ex, ex.HResult.ToString("X"), ErrorCategory.NotSpecified, this));
+ this.outputWriter.WriteError(new ErrorRecord(ex, ex.HResult.ToString("X"), ErrorCategory.NotSpecified, this));
continue;
}
@@ -420,9 +580,9 @@ public IEnumerable GetExternalRecord(Ast ast, Token[] token, E
}
}
//Catch exception where customized defined rules have exceptins when doing invoke
- catch(Exception ex)
+ catch (Exception ex)
{
- command.WriteError(new ErrorRecord(ex, ex.HResult.ToString("X"), ErrorCategory.NotSpecified, this));
+ this.outputWriter.WriteError(new ErrorRecord(ex, ex.HResult.ToString("X"), ErrorCategory.NotSpecified, this));
}
return diagnostics;
@@ -430,7 +590,7 @@ public IEnumerable GetExternalRecord(Ast ast, Token[] token, E
}
}
- public Dictionary> CheckRuleExtension(string[] path, PSCmdlet cmdlet)
+ public Dictionary> CheckRuleExtension(string[] path, PathIntrinsics basePath)
{
Dictionary> results = new Dictionary>();
@@ -443,7 +603,7 @@ public Dictionary> CheckRuleExtension(string[] path, PSCmdl
{
try
{
- cmdlet.WriteVerbose(string.Format(CultureInfo.CurrentCulture, Strings.CheckModuleName, childPath));
+ this.outputWriter.WriteVerbose(string.Format(CultureInfo.CurrentCulture, Strings.CheckModuleName, childPath));
string resolvedPath = string.Empty;
@@ -458,7 +618,7 @@ public Dictionary> CheckRuleExtension(string[] path, PSCmdl
}
else
{
- resolvedPath = cmdlet.SessionState.Path
+ resolvedPath = basePath
.GetResolvedPSPathFromPSPath(childPath).First().ToString();
}
@@ -491,12 +651,12 @@ public Dictionary> CheckRuleExtension(string[] path, PSCmdl
{
try
{
- string resolvedPath = cmdlet.SessionState.Path
+ string resolvedPath = basePath
.GetResolvedPSPathFromPSPath(childPath).First().ToString();
- cmdlet.WriteDebug(string.Format(CultureInfo.CurrentCulture, Strings.CheckAssemblyFile, resolvedPath));
+ this.outputWriter.WriteDebug(string.Format(CultureInfo.CurrentCulture, Strings.CheckAssemblyFile, resolvedPath));
- if (String.Equals(Path.GetExtension(resolvedPath),".dll", StringComparison.OrdinalIgnoreCase))
+ if (String.Equals(Path.GetExtension(resolvedPath), ".dll", StringComparison.OrdinalIgnoreCase))
{
if (!File.Exists(resolvedPath))
{
@@ -526,12 +686,12 @@ public Dictionary> CheckRuleExtension(string[] path, PSCmdl
{
for (int i = 0; i < validModPaths.Count; i++)
{
- validModPaths[i] = cmdlet.SessionState.Path
+ validModPaths[i] = basePath
.GetResolvedPSPathFromPSPath(validModPaths[i]).First().ToString();
}
for (int i = 0; i < validDllPaths.Count; i++)
{
- validDllPaths[i] = cmdlet.SessionState.Path
+ validDllPaths[i] = basePath
.GetResolvedPSPathFromPSPath(validDllPaths[i]).First().ToString();
}
}
@@ -550,5 +710,470 @@ public Dictionary> CheckRuleExtension(string[] path, PSCmdl
}
#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
+ /// script files that are found.
+ ///
+ /// An enumeration of DiagnosticRecords that were found by rules.
+ public IEnumerable AnalyzePath(string path, bool searchRecursively = false)
+ {
+ List scriptFilePaths = new List();
+
+ if (path == null)
+ {
+ this.outputWriter.ThrowTerminatingError(
+ new ErrorRecord(
+ new FileNotFoundException(),
+ string.Format(CultureInfo.CurrentCulture, Strings.FileNotFound, path),
+ ErrorCategory.InvalidArgument,
+ this));
+ }
+
+ // Precreate 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
+ // caller can pull them one at a time
+ foreach (var diagnosticRecord in this.AnalyzeFile(scriptFilePath))
+ {
+ yield return diagnosticRecord;
+ }
+ }
+ }
+
+ private void BuildScriptPathList(
+ string path,
+ bool searchRecursively,
+ IList scriptFilePaths)
+ {
+ const string ps1Suffix = "ps1";
+ const string psm1Suffix = "psm1";
+ const string psd1Suffix = "psd1";
+
+ if (Directory.Exists(path))
+ {
+ if (searchRecursively)
+ {
+ foreach (string filePath in Directory.GetFiles(path))
+ {
+ this.BuildScriptPathList(filePath, searchRecursively, scriptFilePaths);
+ }
+ foreach (string filePath in Directory.GetDirectories(path))
+ {
+ this.BuildScriptPathList(filePath, searchRecursively, scriptFilePaths);
+ }
+ }
+ else
+ {
+ foreach (string filePath in Directory.GetFiles(path))
+ {
+ this.BuildScriptPathList(filePath, searchRecursively, scriptFilePaths);
+ }
+ }
+ }
+ else if (File.Exists(path))
+ {
+ if ((path.Length > ps1Suffix.Length && path.Substring(path.Length - ps1Suffix.Length).Equals(ps1Suffix, StringComparison.OrdinalIgnoreCase)) ||
+ (path.Length > psm1Suffix.Length && path.Substring(path.Length - psm1Suffix.Length).Equals(psm1Suffix, StringComparison.OrdinalIgnoreCase)) ||
+ (path.Length > psd1Suffix.Length && path.Substring(path.Length - psd1Suffix.Length).Equals(psd1Suffix, StringComparison.OrdinalIgnoreCase)))
+ {
+ scriptFilePaths.Add(path);
+ }
+ }
+ else
+ {
+ this.outputWriter.WriteError(
+ new ErrorRecord(
+ new FileNotFoundException(),
+ string.Format(CultureInfo.CurrentCulture, Strings.FileNotFound, path),
+ ErrorCategory.InvalidArgument,
+ this));
+ }
+ }
+
+ private IEnumerable AnalyzeFile(string filePath)
+ {
+ ScriptBlockAst scriptAst = null;
+ Token[] scriptTokens = null;
+ ParseError[] errors;
+
+ this.outputWriter.WriteVerbose(string.Format(CultureInfo.CurrentCulture, Strings.VerboseFileMessage, filePath));
+
+ //Parse the file
+ if (File.Exists(filePath))
+ {
+ scriptAst = Parser.ParseFile(filePath, out scriptTokens, out errors);
+ }
+ else
+ {
+ this.outputWriter.ThrowTerminatingError(new ErrorRecord(new FileNotFoundException(),
+ string.Format(CultureInfo.CurrentCulture, Strings.InvalidPath, filePath),
+ ErrorCategory.InvalidArgument, filePath));
+
+ return null;
+ }
+
+ if (errors != null && errors.Length > 0)
+ {
+ foreach (ParseError error in errors)
+ {
+ string parseErrorMessage = String.Format(CultureInfo.CurrentCulture, Strings.ParserErrorFormat, error.Extent.File, error.Message.TrimEnd('.'), error.Extent.StartLineNumber, error.Extent.StartColumnNumber);
+ this.outputWriter.WriteError(new ErrorRecord(new ParseException(parseErrorMessage), parseErrorMessage, ErrorCategory.ParserError, error.ErrorId));
+ }
+ }
+
+ if (errors.Length > 10)
+ {
+ 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();
+ }
+
+ return this.AnalyzeSyntaxTree(scriptAst, scriptTokens, filePath);
+ }
+
+ ///
+ /// Analyzes the syntax tree of a script file that has already been parsed.
+ ///
+ /// The ScriptBlockAst from the parsed script.
+ /// The tokens found in the script.
+ /// The path to the file that was parsed.
+ /// An enumeration of DiagnosticRecords that were found by rules.
+ public IEnumerable AnalyzeSyntaxTree(
+ ScriptBlockAst scriptAst,
+ Token[] scriptTokens,
+ string filePath)
+ {
+ Dictionary> ruleSuppressions;
+ ConcurrentBag diagnostics = new ConcurrentBag();
+ ConcurrentBag suppressed = new ConcurrentBag();
+ BlockingCollection> verboseOrErrors = new BlockingCollection>();
+
+ // Use a List of KVP rather than dictionary, since for a script containing inline functions with same signature, keys clash
+ List> cmdInfoTable = new List>();
+
+ ruleSuppressions = Helper.Instance.GetRuleSuppression(scriptAst);
+
+ foreach (List ruleSuppressionsList in ruleSuppressions.Values)
+ {
+ foreach (RuleSuppression ruleSuppression in ruleSuppressionsList)
+ {
+ if (!String.IsNullOrWhiteSpace(ruleSuppression.Error))
+ {
+ this.outputWriter.WriteError(new ErrorRecord(new ArgumentException(ruleSuppression.Error), ruleSuppression.Error, ErrorCategory.InvalidArgument, ruleSuppression));
+ }
+ }
+ }
+
+ #region Run VariableAnalysis
+ try
+ {
+ Helper.Instance.InitializeVariableAnalysis(scriptAst);
+ }
+ catch { }
+ #endregion
+
+ Helper.Instance.Tokens = scriptTokens;
+
+ #region Run ScriptRules
+ //Trim down to the leaf element of the filePath and pass it to Diagnostic Record
+ string fileName = System.IO.Path.GetFileName(filePath);
+
+ if (this.ScriptRules != null)
+ {
+ var tasks = this.ScriptRules.Select(scriptRule => Task.Factory.StartNew(() =>
+ {
+ bool includeRegexMatch = false;
+ bool excludeRegexMatch = false;
+
+ foreach (Regex include in includeRegexList)
+ {
+ if (include.IsMatch(scriptRule.GetName()))
+ {
+ includeRegexMatch = true;
+ break;
+ }
+ }
+
+ foreach (Regex exclude in excludeRegexList)
+ {
+ if (exclude.IsMatch(scriptRule.GetName()))
+ {
+ excludeRegexMatch = true;
+ break;
+ }
+ }
+
+ if ((includeRule == null || includeRegexMatch) && (excludeRule == null || !excludeRegexMatch))
+ {
+ List result = new List();
+
+ result.Add(string.Format(CultureInfo.CurrentCulture, Strings.VerboseRunningMessage, scriptRule.GetName()));
+
+ // Ensure that any unhandled errors from Rules are converted to non-terminating errors
+ // We want the Engine to continue functioning even if one or more Rules throws an exception
+ try
+ {
+ var records = Helper.Instance.SuppressRule(scriptRule.GetName(), ruleSuppressions, scriptRule.AnalyzeScript(scriptAst, scriptAst.Extent.File).ToList());
+ foreach (var record in records.Item2)
+ {
+ diagnostics.Add(record);
+ }
+ foreach (var suppressedRec in records.Item1)
+ {
+ suppressed.Add(suppressedRec);
+ }
+ }
+ catch (Exception scriptRuleException)
+ {
+ result.Add(new ErrorRecord(scriptRuleException, Strings.RuleErrorMessage, ErrorCategory.InvalidOperation, scriptAst.Extent.File));
+ }
+
+ verboseOrErrors.Add(result);
+ }
+ }));
+
+ Task.Factory.ContinueWhenAll(tasks.ToArray(), t => verboseOrErrors.CompleteAdding());
+
+ while (!verboseOrErrors.IsCompleted)
+ {
+ List data = null;
+ try
+ {
+ data = verboseOrErrors.Take();
+ }
+ catch (InvalidOperationException) { }
+
+ if (data != null)
+ {
+ this.outputWriter.WriteVerbose(data[0] as string);
+ if (data.Count == 2)
+ {
+ this.outputWriter.WriteError(data[1] as ErrorRecord);
+ }
+ }
+ }
+ }
+
+ #endregion
+
+ #region Run Token Rules
+
+ if (this.TokenRules != null)
+ {
+ foreach (ITokenRule tokenRule in this.TokenRules)
+ {
+ bool includeRegexMatch = false;
+ bool excludeRegexMatch = false;
+ foreach (Regex include in includeRegexList)
+ {
+ if (include.IsMatch(tokenRule.GetName()))
+ {
+ includeRegexMatch = true;
+ break;
+ }
+ }
+ foreach (Regex exclude in excludeRegexList)
+ {
+ if (exclude.IsMatch(tokenRule.GetName()))
+ {
+ excludeRegexMatch = true;
+ break;
+ }
+ }
+ if ((includeRule == null || includeRegexMatch) && (excludeRule == null || !excludeRegexMatch))
+ {
+ this.outputWriter.WriteVerbose(string.Format(CultureInfo.CurrentCulture, Strings.VerboseRunningMessage, tokenRule.GetName()));
+
+ // Ensure that any unhandled errors from Rules are converted to non-terminating errors
+ // We want the Engine to continue functioning even if one or more Rules throws an exception
+ try
+ {
+ var records = Helper.Instance.SuppressRule(tokenRule.GetName(), ruleSuppressions, tokenRule.AnalyzeTokens(scriptTokens, filePath).ToList());
+ foreach (var record in records.Item2)
+ {
+ diagnostics.Add(record);
+ }
+ foreach (var suppressedRec in records.Item1)
+ {
+ suppressed.Add(suppressedRec);
+ }
+ }
+ catch (Exception tokenRuleException)
+ {
+ this.outputWriter.WriteError(new ErrorRecord(tokenRuleException, Strings.RuleErrorMessage, ErrorCategory.InvalidOperation, fileName));
+ }
+ }
+ }
+ }
+
+ #endregion
+
+ #region DSC Resource Rules
+ if (this.DSCResourceRules != null)
+ {
+ // Invoke AnalyzeDSCClass only if the ast is a class based resource
+ if (Helper.Instance.IsDscResourceClassBased(scriptAst))
+ {
+ // Run DSC Class rule
+ foreach (IDSCResourceRule dscResourceRule in this.DSCResourceRules)
+ {
+ bool includeRegexMatch = false;
+ bool excludeRegexMatch = false;
+
+ foreach (Regex include in includeRegexList)
+ {
+ if (include.IsMatch(dscResourceRule.GetName()))
+ {
+ includeRegexMatch = true;
+ break;
+ }
+ }
+
+ foreach (Regex exclude in excludeRegexList)
+ {
+ if (exclude.IsMatch(dscResourceRule.GetName()))
+ {
+ excludeRegexMatch = true;
+ break;
+ }
+ }
+
+ if ((includeRule == null || includeRegexMatch) && (excludeRule == null || excludeRegexMatch))
+ {
+ this.outputWriter.WriteVerbose(string.Format(CultureInfo.CurrentCulture, Strings.VerboseRunningMessage, dscResourceRule.GetName()));
+
+ // Ensure that any unhandled errors from Rules are converted to non-terminating errors
+ // We want the Engine to continue functioning even if one or more Rules throws an exception
+ try
+ {
+ var records = Helper.Instance.SuppressRule(dscResourceRule.GetName(), ruleSuppressions, dscResourceRule.AnalyzeDSCClass(scriptAst, filePath).ToList());
+ foreach (var record in records.Item2)
+ {
+ diagnostics.Add(record);
+ }
+ foreach (var suppressedRec in records.Item1)
+ {
+ suppressed.Add(suppressedRec);
+ }
+ }
+ catch (Exception dscResourceRuleException)
+ {
+ this.outputWriter.WriteError(new ErrorRecord(dscResourceRuleException, Strings.RuleErrorMessage, ErrorCategory.InvalidOperation, filePath));
+ }
+ }
+ }
+ }
+
+ // Check if the supplied artifact is indeed part of the DSC resource
+ if (Helper.Instance.IsDscResourceModule(filePath))
+ {
+ // Run all DSC Rules
+ foreach (IDSCResourceRule dscResourceRule in this.DSCResourceRules)
+ {
+ bool includeRegexMatch = false;
+ bool excludeRegexMatch = false;
+ foreach (Regex include in includeRegexList)
+ {
+ if (include.IsMatch(dscResourceRule.GetName()))
+ {
+ includeRegexMatch = true;
+ break;
+ }
+ }
+ foreach (Regex exclude in excludeRegexList)
+ {
+ if (exclude.IsMatch(dscResourceRule.GetName()))
+ {
+ excludeRegexMatch = true;
+ }
+ }
+ if ((includeRule == null || includeRegexMatch) && (excludeRule == null || !excludeRegexMatch))
+ {
+ this.outputWriter.WriteVerbose(string.Format(CultureInfo.CurrentCulture, Strings.VerboseRunningMessage, dscResourceRule.GetName()));
+
+ // Ensure that any unhandled errors from Rules are converted to non-terminating errors
+ // We want the Engine to continue functioning even if one or more Rules throws an exception
+ try
+ {
+ var records = Helper.Instance.SuppressRule(dscResourceRule.GetName(), ruleSuppressions, dscResourceRule.AnalyzeDSCResource(scriptAst, filePath).ToList());
+ foreach (var record in records.Item2)
+ {
+ diagnostics.Add(record);
+ }
+ foreach (var suppressedRec in records.Item1)
+ {
+ suppressed.Add(suppressedRec);
+ }
+ }
+ catch (Exception dscResourceRuleException)
+ {
+ this.outputWriter.WriteError(new ErrorRecord(dscResourceRuleException, Strings.RuleErrorMessage, ErrorCategory.InvalidOperation, filePath));
+ }
+ }
+ }
+
+ }
+ }
+ #endregion
+
+ #region Run External Rules
+
+ if (this.ExternalRules != null)
+ {
+ List exRules = new List();
+
+ foreach (ExternalRule exRule in this.ExternalRules)
+ {
+ if ((includeRule == null || includeRule.Contains(exRule.GetName(), StringComparer.OrdinalIgnoreCase)) &&
+ (excludeRule == null || !excludeRule.Contains(exRule.GetName(), StringComparer.OrdinalIgnoreCase)))
+ {
+ string ruleName = string.Format(CultureInfo.CurrentCulture, "{0}\\{1}", exRule.GetSourceName(), exRule.GetName());
+ this.outputWriter.WriteVerbose(string.Format(CultureInfo.CurrentCulture, Strings.VerboseRunningMessage, ruleName));
+
+ // Ensure that any unhandled errors from Rules are converted to non-terminating errors
+ // We want the Engine to continue functioning even if one or more Rules throws an exception
+ try
+ {
+ exRules.Add(exRule);
+ }
+ catch (Exception externalRuleException)
+ {
+ this.outputWriter.WriteError(new ErrorRecord(externalRuleException, Strings.RuleErrorMessage, ErrorCategory.InvalidOperation, fileName));
+ }
+ }
+ }
+
+ foreach (var record in this.GetExternalRecord(scriptAst, scriptTokens, exRules.ToArray(), fileName))
+ {
+ diagnostics.Add(record);
+ }
+ }
+
+ #endregion
+
+ IEnumerable diagnosticsList = diagnostics;
+
+ if (severity != null)
+ {
+ var diagSeverity = severity.Select(item => Enum.Parse(typeof(DiagnosticSeverity), item, true));
+ diagnosticsList = diagnostics.Where(item => diagSeverity.Contains(item.Severity));
+ }
+
+ return this.suppressedOnly ?
+ suppressed.OfType() :
+ diagnosticsList;
+ }
}
}
diff --git a/Engine/ScriptAnalyzerEngine.csproj b/Engine/ScriptAnalyzerEngine.csproj
index 06f02c73e..912072190 100644
--- a/Engine/ScriptAnalyzerEngine.csproj
+++ b/Engine/ScriptAnalyzerEngine.csproj
@@ -69,6 +69,7 @@
+
diff --git a/Tests/Engine/CustomizedRule.tests.ps1 b/Tests/Engine/CustomizedRule.tests.ps1
index f7fac7213..cf87ed818 100644
--- a/Tests/Engine/CustomizedRule.tests.ps1
+++ b/Tests/Engine/CustomizedRule.tests.ps1
@@ -1,4 +1,11 @@
-Import-Module PSScriptAnalyzer
+# Check if PSScriptAnalyzer is already loaded so we don't
+# overwrite a test version of Invoke-ScriptAnalyzer by
+# accident
+if (!(Get-Module PSScriptAnalyzer) -and !$testingLibraryUsage)
+{
+ Import-Module PSScriptAnalyzer
+}
+
$directory = Split-Path -Parent $MyInvocation.MyCommand.Path
$message = "this is help"
$measure = "Measure-RequiresRunAsAdministrator"
diff --git a/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 b/Tests/Engine/InvokeScriptAnalyzer.tests.ps1
index ad4c411b2..05a008c48 100644
--- a/Tests/Engine/InvokeScriptAnalyzer.tests.ps1
+++ b/Tests/Engine/InvokeScriptAnalyzer.tests.ps1
@@ -1,4 +1,11 @@
-Import-Module PSScriptAnalyzer
+# Check if PSScriptAnalyzer is already loaded so we don't
+# overwrite a test version of Invoke-ScriptAnalyzer by
+# accident
+if (!(Get-Module PSScriptAnalyzer) -and !$testingLibraryUsage)
+{
+ Import-Module PSScriptAnalyzer
+}
+
$sa = Get-Command Invoke-ScriptAnalyzer
$directory = Split-Path -Parent $MyInvocation.MyCommand.Path
$singularNouns = "PSUseSingularNouns"
@@ -216,7 +223,17 @@ Describe "Test CustomizedRulePath" {
Context "When used incorrectly" {
It "file cannot be found" {
$wrongRule = Invoke-ScriptAnalyzer $directory\TestScript.ps1 -CustomizedRulePath "This is a wrong rule" 3>&1 | Select-Object -First 1
- $wrongRule | Should Match "Cannot find rule extension 'This is a wrong rule'."
+
+ if ($testingLibraryUsage)
+ {
+ # Special case for library usage testing: warning output written
+ # with PSHost.UI.WriteWarningLine does not get redirected correctly
+ # so we can't use this approach for checking the warning message.
+ # Instead, reach into the test IOutputWriter implementation to find it.
+ $wrongRule = $testOutputWriter.MostRecentWarningMessage
+ }
+
+ $wrongRule | Should Match "Cannot find rule extension 'This is a wrong rule'."
}
}
}
\ No newline at end of file
diff --git a/Tests/Engine/LibraryUsage.tests.ps1 b/Tests/Engine/LibraryUsage.tests.ps1
new file mode 100644
index 000000000..37137c832
--- /dev/null
+++ b/Tests/Engine/LibraryUsage.tests.ps1
@@ -0,0 +1,125 @@
+Import-Module PSScriptAnalyzer
+
+$directory = Split-Path -Parent $MyInvocation.MyCommand.Path
+
+# Overwrite Invoke-ScriptAnalyzer with a version that
+# wraps the usage of ScriptAnalyzer as a .NET library
+function Invoke-ScriptAnalyzer {
+ param (
+ [parameter(Mandatory = $true, Position = 0)]
+ [Alias("PSPath")]
+ [string] $Path,
+
+ [Parameter(Mandatory = $false)]
+ [string[]] $CustomizedRulePath = $null,
+
+ [Parameter(Mandatory=$false)]
+ [string[]] $ExcludeRule = $null,
+
+ [Parameter(Mandatory = $false)]
+ [string[]] $IncludeRule = $null,
+
+ [ValidateSet("Warning", "Error", "Information", IgnoreCase = $true)]
+ [Parameter(Mandatory = $false)]
+ [string[]] $Severity = $null,
+
+ [Parameter(Mandatory = $false)]
+ [switch] $Recurse,
+
+ [Parameter(Mandatory = $false)]
+ [switch] $SuppressedOnly
+ )
+
+ $scriptAnalyzer = New-Object "Microsoft.Windows.PowerShell.ScriptAnalyzer.ScriptAnalyzer"
+ $scriptAnalyzer.Initialize(
+ $runspace,
+ $testOutputWriter,
+ $CustomizedRulePath,
+ $IncludeRule,
+ $ExcludeRule,
+ $Severity,
+ $SuppressedOnly.IsPresent
+ );
+
+ return $scriptAnalyzer.AnalyzePath($Path, $Recurse.IsPresent);
+}
+
+# Define an implementation of the IOutputWriter interface
+Add-Type -Language CSharp @"
+using System.Management.Automation;
+using System.Management.Automation.Host;
+using Microsoft.Windows.PowerShell.ScriptAnalyzer;
+
+public class PesterTestOutputWriter : IOutputWriter
+{
+ private PSHost psHost;
+
+ public string MostRecentWarningMessage { get; private set; }
+
+ public static PesterTestOutputWriter Create(PSHost psHost)
+ {
+ PesterTestOutputWriter testOutputWriter = new PesterTestOutputWriter();
+ testOutputWriter.psHost = psHost;
+ return testOutputWriter;
+ }
+
+ public void WriteError(ErrorRecord error)
+ {
+ // We don't write errors to avoid misleading
+ // error messages in test output
+ }
+
+ public void WriteWarning(string message)
+ {
+ psHost.UI.WriteWarningLine(message);
+
+ this.MostRecentWarningMessage = message;
+ }
+
+ public void WriteVerbose(string message)
+ {
+ // We don't write verbose output to avoid lots
+ // of unnecessary messages in test output
+ }
+
+ public void WriteDebug(string message)
+ {
+ psHost.UI.WriteDebugLine(message);
+ }
+
+ public void ThrowTerminatingError(ErrorRecord record)
+ {
+ throw new RuntimeException(
+ "Test failed due to terminating error: \r\n" + record.ToString(),
+ null,
+ record);
+ }
+}
+"@ -ReferencedAssemblies "Microsoft.Windows.PowerShell.ScriptAnalyzer" -ErrorAction SilentlyContinue
+
+if ($testOutputWriter -eq $null)
+{
+ $testOutputWriter = [PesterTestOutputWriter]::Create($Host);
+}
+
+# Create a fresh runspace to pass into the ScriptAnalyzer class
+$initialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault2();
+$runspace = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace([System.Management.Automation.Host.PSHost]$Host, [System.Management.Automation.Runspaces.InitialSessionState]$initialSessionState);
+$runspace.Open();
+
+# Let other test scripts know we are testing library usage now
+$testingLibraryUsage = $true
+
+# Invoke existing test files that use Invoke-ScriptAnalyzer
+. $directory\InvokeScriptAnalyzer.tests.ps1
+. $directory\RuleSuppression.tests.ps1
+. $directory\CustomizedRule.tests.ps1
+
+# We're done testing library usage
+$testingLibraryUsage = $false
+
+# Clean up the test runspace
+$runspace.Dispose();
+
+# Re-import the PSScriptAnalyzer module to overwrite the library test cmdlet
+Import-Module PSScriptAnalyzer
diff --git a/Tests/Engine/RuleSuppression.tests.ps1 b/Tests/Engine/RuleSuppression.tests.ps1
index 17d8a719d..de32e5a94 100644
--- a/Tests/Engine/RuleSuppression.tests.ps1
+++ b/Tests/Engine/RuleSuppression.tests.ps1
@@ -1,4 +1,11 @@
-Import-Module -Verbose PSScriptAnalyzer
+# Check if PSScriptAnalyzer is already loaded so we don't
+# overwrite a test version of Invoke-ScriptAnalyzer by
+# accident
+if (!(Get-Module PSScriptAnalyzer) -and !$testingLibraryUsage)
+{
+ Import-Module -Verbose PSScriptAnalyzer
+}
+
$directory = Split-Path -Parent $MyInvocation.MyCommand.Path
$violations = Invoke-ScriptAnalyzer $directory\RuleSuppression.ps1