From 61877f9dc9026e460ed0043339f5ab9cb01a64a5 Mon Sep 17 00:00:00 2001 From: Kapil Borle Date: Wed, 7 Sep 2016 15:14:16 -0700 Subject: [PATCH 1/6] Add feature to discover settings file --- .../Commands/InvokeScriptAnalyzerCommand.cs | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/Engine/Commands/InvokeScriptAnalyzerCommand.cs b/Engine/Commands/InvokeScriptAnalyzerCommand.cs index 4a7a61413..7b82f3560 100644 --- a/Engine/Commands/InvokeScriptAnalyzerCommand.cs +++ b/Engine/Commands/InvokeScriptAnalyzerCommand.cs @@ -219,8 +219,51 @@ protected override void BeginProcessing() string[] rulePaths = Helper.ProcessCustomRulePaths(customRulePath, this.SessionState, recurseCustomRulePath); - if (!ScriptAnalyzer.Instance.ParseProfile(this.settings, this.SessionState.Path, this)) + var settingFileHasErrors = false; + if (settings == null) { + // add a directory separator character because if there is no trailing separator character, it will return the parent + var directory = System.IO.Path.GetDirectoryName(path + System.IO.Path.DirectorySeparatorChar); + 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 (!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; } From 151e8f1fa023cf166353f6802e3d254d1bcd031d Mon Sep 17 00:00:00 2001 From: Kapil Borle Date: Wed, 7 Sep 2016 15:14:42 -0700 Subject: [PATCH 2/6] Update settings documentation in README --- README.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2d96b73d7..79d9468ae 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/development/Engine/Settings). + ScriptAnalyzer as a .NET library ================================ From 480b2e7fd315e022e15e1f3e0069a391e503a8c7 Mon Sep 17 00:00:00 2001 From: Kapil Borle Date: Wed, 7 Sep 2016 16:04:48 -0700 Subject: [PATCH 3/6] Add tests for settings discovery --- Tests/Engine/Settings.tests.ps1 | 31 +++++++++++++++++++ .../Project1/ExplicitSettings.psd1 | 3 ++ .../Project1/PSScriptAnalyzerSettings.psd1 | 3 ++ .../Engine/SettingsTest/Project1/project1.ps1 | 2 ++ .../Engine/SettingsTest/Project2/project2.ps1 | 2 ++ 5 files changed, 41 insertions(+) create mode 100644 Tests/Engine/Settings.tests.ps1 create mode 100644 Tests/Engine/SettingsTest/Project1/ExplicitSettings.psd1 create mode 100644 Tests/Engine/SettingsTest/Project1/PSScriptAnalyzerSettings.psd1 create mode 100644 Tests/Engine/SettingsTest/Project1/project1.ps1 create mode 100644 Tests/Engine/SettingsTest/Project2/project2.ps1 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/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 From 51e5fbda594c55b76f14484397a323f63a17bc25 Mon Sep 17 00:00:00 2001 From: Kapil Borle Date: Wed, 7 Sep 2016 16:35:10 -0700 Subject: [PATCH 4/6] Process paths before settings discovery --- .../Commands/InvokeScriptAnalyzerCommand.cs | 46 +++++++++++++++---- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/Engine/Commands/InvokeScriptAnalyzerCommand.cs b/Engine/Commands/InvokeScriptAnalyzerCommand.cs index 7b82f3560..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,12 +222,24 @@ protected override void BeginProcessing() string[] rulePaths = Helper.ProcessCustomRulePaths(customRulePath, this.SessionState, recurseCustomRulePath); + if (IsFileParameterSet()) + { + ProcessPath(); + } var settingFileHasErrors = false; - if (settings == null) + 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 = System.IO.Path.GetDirectoryName(path + System.IO.Path.DirectorySeparatorChar); + 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}.", @@ -244,7 +260,6 @@ protected override void BeginProcessing() settingsFilename, directory)); settingFileHasErrors = !ScriptAnalyzer.Instance.ParseProfile(settingsFilepath, this.SessionState.Path, this); - } } @@ -330,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); } } @@ -359,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 From bd0f12a60b7365a158b7de40778ba3f8f58c8e93 Mon Sep 17 00:00:00 2001 From: Kapil Borle Date: Wed, 7 Sep 2016 16:49:18 -0700 Subject: [PATCH 5/6] Add missing file for settings discovery tests --- Tests/Engine/SettingsTest/Project2/RandomNameSettings.psd1 | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Tests/Engine/SettingsTest/Project2/RandomNameSettings.psd1 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 From 20ab622f38386fcfdddc67dfca102adaafafda4f Mon Sep 17 00:00:00 2001 From: Kapil Borle Date: Mon, 12 Sep 2016 13:49:29 -0700 Subject: [PATCH 6/6] Point settings to settings directory in master --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 79d9468ae..0ac2c6db2 100644 --- a/README.md +++ b/README.md @@ -218,7 +218,7 @@ If you place a PSScriptAnayzer settings file named `PSScriptAnalyzerSettings.psd 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/development/Engine/Settings). +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 ================================