diff --git a/Engine/Commands/InvokeScriptAnalyzerCommand.cs b/Engine/Commands/InvokeScriptAnalyzerCommand.cs index 4a7a61413..b8a2e9d7f 100644 --- a/Engine/Commands/InvokeScriptAnalyzerCommand.cs +++ b/Engine/Commands/InvokeScriptAnalyzerCommand.cs @@ -38,6 +38,10 @@ namespace Microsoft.Windows.PowerShell.ScriptAnalyzer.Commands HelpUri = "http://go.microsoft.com/fwlink/?LinkId=525914")] public class InvokeScriptAnalyzerCommand : PSCmdlet, IOutputWriter { + #region Private variables + List processedPaths; + #endregion // Private variables + #region Parameters /// /// Path: The path to the file or folder to invoke PSScriptAnalyzer on. @@ -218,9 +222,63 @@ protected override void BeginProcessing() string[] rulePaths = Helper.ProcessCustomRulePaths(customRulePath, this.SessionState, recurseCustomRulePath); + if (IsFileParameterSet()) + { + ProcessPath(); + } + + var settingFileHasErrors = false; + if (settings == null + && processedPaths != null + && processedPaths.Count == 1) + { + // add a directory separator character because if there is no trailing separator character, it will return the parent + var directory = processedPaths[0].TrimEnd(System.IO.Path.DirectorySeparatorChar); + if (File.Exists(directory)) + { + // if given path is a file, get its directory + directory = System.IO.Path.GetDirectoryName(directory); + } + + this.WriteVerbose( + String.Format( + "Settings not provided. Will look for settings file in the given path {0}.", + path)); + var settingsFileAutoDiscovered = false; + if (Directory.Exists(directory)) + { + // if settings are not provided explicitly, look for it in the given path + // check if pssasettings.psd1 exists + var settingsFilename = "PSScriptAnalyzerSettings.psd1"; + var settingsFilepath = System.IO.Path.Combine(directory, settingsFilename); + if (File.Exists(settingsFilepath)) + { + settingsFileAutoDiscovered = true; + this.WriteVerbose( + String.Format( + "Found {0} in {1}. Will use it to provide settings for this invocation.", + settingsFilename, + directory)); + settingFileHasErrors = !ScriptAnalyzer.Instance.ParseProfile(settingsFilepath, this.SessionState.Path, this); + } + } - if (!ScriptAnalyzer.Instance.ParseProfile(this.settings, this.SessionState.Path, this)) + if (!settingsFileAutoDiscovered) + { + this.WriteVerbose( + String.Format( + "Cannot find a settings file in the given path {0}.", + path)); + } + } + else { + settingFileHasErrors = !ScriptAnalyzer.Instance.ParseProfile(this.settings, this.SessionState.Path, this); + } + + if (settingFileHasErrors) + { + this.WriteWarning("Cannot parse settings. Will abort the invocation."); stopProcessing = true; return; } @@ -287,15 +345,11 @@ protected override void StopProcessing() private void ProcessInput() { IEnumerable diagnosticsList = Enumerable.Empty(); - if (String.Equals(this.ParameterSetName, "File", StringComparison.OrdinalIgnoreCase)) + if (IsFileParameterSet()) { - // throws Item Not Found Exception - Collection paths = this.SessionState.Path.GetResolvedPSPathFromPSPath(path); - foreach (PathInfo p in paths) + foreach (var p in processedPaths) { - diagnosticsList = ScriptAnalyzer.Instance.AnalyzePath( - this.SessionState.Path.GetUnresolvedProviderPathFromPSPath(p.Path), - this.recurse); + diagnosticsList = ScriptAnalyzer.Instance.AnalyzePath(p, this.recurse); WriteToOutput(diagnosticsList); } } @@ -316,6 +370,21 @@ private void WriteToOutput(IEnumerable diagnosticRecords) } } } + + private void ProcessPath() + { + Collection paths = this.SessionState.Path.GetResolvedPSPathFromPSPath(path); + processedPaths = new List(); + foreach (PathInfo p in paths) + { + processedPaths.Add(this.SessionState.Path.GetUnresolvedProviderPathFromPSPath(p.Path)); + } + } + + private bool IsFileParameterSet() + { + return String.Equals(this.ParameterSetName, "File", StringComparison.OrdinalIgnoreCase); + } #endregion } } \ No newline at end of file diff --git a/README.md b/README.md index 2d96b73d7..0ac2c6db2 100644 --- a/README.md +++ b/README.md @@ -174,15 +174,15 @@ Param( Settings Support in ScriptAnalyzer ======================================== Settings that describe ScriptAnalyzer rules to include/exclude based on `Severity` can be created and supplied to -`Invoke-ScriptAnalyzer` using the `Setting` parameter. This enables a user to create a custom configuration for a specific environment. +`Invoke-ScriptAnalyzer` using the `Setting` parameter. This enables a user to create a custom configuration for a specific environment. We support the following modes for specifying the settings file. -Using Settings support: +## Explicit The following example excludes two rules from the default set of rules and any rule that does not output an Error or Warning diagnostic record. ``` PowerShell -# ScriptAnalyzerSettings.psd1 +# PSScriptAnalyzerSettings.psd1 @{ Severity=@('Error','Warning') ExcludeRules=@('PSAvoidUsingCmdletAliases', @@ -199,7 +199,7 @@ Invoke-ScriptAnalyzer -Path MyScript.ps1 -Setting ScriptAnalyzerSettings.psd1 The next example selects a few rules to execute instead of all the default rules. ``` PowerShell -# ScriptAnalyzerSettings.psd1 +# PSScriptAnalyzerSettings.psd1 @{ IncludeRules=@('PSAvoidUsingPlainTextForPassword', 'PSAvoidUsingConvertToSecureStringWithPlainText') @@ -211,6 +211,15 @@ Then invoke that settings file when using: Invoke-ScriptAnalyzer -Path MyScript.ps1 -Setting ScriptAnalyzerSettings.psd1 ``` +## Implicit +If you place a PSScriptAnayzer settings file named `PSScriptAnalyzerSettings.psd1` in your project root, PSScriptAnalyzer will discover it if you pass the project root as the `Path` parameter. + +```PowerShell +Invoke-ScriptAnalyzer -Path "C:\path\to\project" -Recurse +``` + +Note that providing settings explicitly takes higher precedence over this implicit mode. Sample settings files are provided [here](https://github.com/PowerShell/PSScriptAnalyzer/tree/master/Engine/Settings). + ScriptAnalyzer as a .NET library ================================ diff --git a/Tests/Engine/Settings.tests.ps1 b/Tests/Engine/Settings.tests.ps1 new file mode 100644 index 000000000..56f6f7c55 --- /dev/null +++ b/Tests/Engine/Settings.tests.ps1 @@ -0,0 +1,31 @@ +if (!(Get-Module PSScriptAnalyzer)) +{ + Import-Module PSScriptAnalyzer +} + +$directory = Split-Path $MyInvocation.MyCommand.Path +Describe "Settings Precedence" { + $settingsTestDirectory = [System.IO.Path]::Combine($directory, "SettingsTest") + $project1Root = [System.IO.Path]::Combine($settingsTestDirectory, "Project1") + $project2Root = [System.IO.Path]::Combine($settingsTestDirectory, "Project2") + Context "settings object is explicit" { + It "runs rules from the explicit setting file" { + $settingsFilepath = [System.IO.Path]::Combine($project1Root, "ExplicitSettings.psd1") + $violations = Invoke-ScriptAnalyzer -Path $project1Root -Settings $settingsFilepath -Recurse + $violations.Count | Should Be 1 + $violations[0].RuleName | Should Be "PSAvoidUsingWriteHost" + } + } + Context "settings file is implicit" { + It "runs rules from the implicit setting file" { + $violations = Invoke-ScriptAnalyzer -Path $project1Root -Recurse + $violations.Count | Should Be 1 + $violations[0].RuleName | Should Be "PSAvoidUsingCmdletAliases" + } + + It "cannot find file if not named PSScriptAnalyzerSettings.psd1" { + $violations = Invoke-ScriptAnalyzer -Path $project2Root -Recurse + $violations.Count | Should Be 2 + } + } +} \ No newline at end of file diff --git a/Tests/Engine/SettingsTest/Project1/ExplicitSettings.psd1 b/Tests/Engine/SettingsTest/Project1/ExplicitSettings.psd1 new file mode 100644 index 000000000..9ad44a11c --- /dev/null +++ b/Tests/Engine/SettingsTest/Project1/ExplicitSettings.psd1 @@ -0,0 +1,3 @@ +@{ + "IncludeRules" = @("PSAvoidUsingWriteHost") +} \ No newline at end of file diff --git a/Tests/Engine/SettingsTest/Project1/PSScriptAnalyzerSettings.psd1 b/Tests/Engine/SettingsTest/Project1/PSScriptAnalyzerSettings.psd1 new file mode 100644 index 000000000..f7aac9573 --- /dev/null +++ b/Tests/Engine/SettingsTest/Project1/PSScriptAnalyzerSettings.psd1 @@ -0,0 +1,3 @@ +@{ + "IncludeRules" = @("PSAvoidUsingCmdletAliases") +} \ No newline at end of file diff --git a/Tests/Engine/SettingsTest/Project1/project1.ps1 b/Tests/Engine/SettingsTest/Project1/project1.ps1 new file mode 100644 index 000000000..a5f744449 --- /dev/null +++ b/Tests/Engine/SettingsTest/Project1/project1.ps1 @@ -0,0 +1,2 @@ +gci +Write-Host "Do not use write-host" \ No newline at end of file diff --git a/Tests/Engine/SettingsTest/Project2/RandomNameSettings.psd1 b/Tests/Engine/SettingsTest/Project2/RandomNameSettings.psd1 new file mode 100644 index 000000000..9ad44a11c --- /dev/null +++ b/Tests/Engine/SettingsTest/Project2/RandomNameSettings.psd1 @@ -0,0 +1,3 @@ +@{ + "IncludeRules" = @("PSAvoidUsingWriteHost") +} \ No newline at end of file diff --git a/Tests/Engine/SettingsTest/Project2/project2.ps1 b/Tests/Engine/SettingsTest/Project2/project2.ps1 new file mode 100644 index 000000000..a5f744449 --- /dev/null +++ b/Tests/Engine/SettingsTest/Project2/project2.ps1 @@ -0,0 +1,2 @@ +gci +Write-Host "Do not use write-host" \ No newline at end of file