diff --git a/.build.ps1 b/.build.ps1 index dc525f13c..a712753bf 100644 --- a/.build.ps1 +++ b/.build.ps1 @@ -113,7 +113,12 @@ function Get-ResourceTaskParam($project) { @{ Inputs = "$project/Strings.resx" Outputs = "$project/Strings.cs" - Jobs = {& "$resourceScript $project"} + Data = $project + Jobs = { + Push-Location $BuildRoot + & $resourceScript $Task.Data + Pop-Location + } Before = "$project/build" } } diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..a97addede --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "editor.tabSize": 4, + "powershell.codeFormatting.preset": "Allman" +} diff --git a/Engine/Commands/InvokeScriptAnalyzerCommand.cs b/Engine/Commands/InvokeScriptAnalyzerCommand.cs index c230c1071..ad25c5e0e 100644 --- a/Engine/Commands/InvokeScriptAnalyzerCommand.cs +++ b/Engine/Commands/InvokeScriptAnalyzerCommand.cs @@ -265,6 +265,9 @@ protected override void BeginProcessing() ProcessPath(); } + string[] combRulePaths = null; + var combRecurseCustomRulePath = RecurseCustomRulePath.IsPresent; + var combIncludeDefaultRules = IncludeDefaultRules.IsPresent; try { var settingsObj = PSSASettings.Create( @@ -274,7 +277,30 @@ protected override void BeginProcessing() if (settingsObj != null) { ScriptAnalyzer.Instance.UpdateSettings(settingsObj); + + // For includeDefaultRules and RecurseCustomRulePath we override the value in the settings file by + // command line argument. + combRecurseCustomRulePath = OverrideSwitchParam( + settingsObj.RecurseCustomRulePath, + "RecurseCustomRulePath"); + combIncludeDefaultRules = OverrideSwitchParam( + settingsObj.IncludeDefaultRules, + "IncludeDefaultRules"); } + + // Ideally we should not allow the parameter to be set from settings and command line + // simultaneously. But since, this was done before with IncludeRules, ExcludeRules and Severity, + // we use the same strategy for CustomRulePath. So, we take the union of CustomRulePath provided in + // the settings file and if provided on command line. + var settingsCustomRulePath = Helper.ProcessCustomRulePaths( + settingsObj?.CustomRulePath?.ToArray(), + this.SessionState, + combRecurseCustomRulePath); + combRulePaths = rulePaths == null + ? settingsCustomRulePath + : settingsCustomRulePath == null + ? rulePaths + : rulePaths.Concat(settingsCustomRulePath).ToArray(); } catch { @@ -285,11 +311,11 @@ protected override void BeginProcessing() ScriptAnalyzer.Instance.Initialize( this, - rulePaths, + combRulePaths, this.includeRule, this.excludeRule, this.severity, - null == rulePaths ? true : this.includeDefaultRules, + combRulePaths == null || combIncludeDefaultRules, this.suppressedOnly); } @@ -388,6 +414,13 @@ private bool IsFileParameterSet() return String.Equals(this.ParameterSetName, "File", StringComparison.OrdinalIgnoreCase); } + private bool OverrideSwitchParam(bool paramValue, string paramName) + { + return MyInvocation.BoundParameters.ContainsKey(paramName) + ? ((SwitchParameter)MyInvocation.BoundParameters[paramName]).ToBool() + : paramValue; + } + #endregion // Private Methods } } diff --git a/Engine/Settings.cs b/Engine/Settings.cs index 83cc12f69..c34631183 100644 --- a/Engine/Settings.cs +++ b/Engine/Settings.cs @@ -28,17 +28,23 @@ internal enum SettingsMode { None = 0, Auto, File, Hashtable, Preset }; /// public class Settings { + private bool recurseCustomRulePath = false; + private bool includeDefaultRules = false; private string filePath; private List includeRules; private List excludeRules; private List severities; + private List customRulePath; private Dictionary> ruleArguments; - public string FilePath { get { return filePath; } } - public IEnumerable IncludeRules { get { return includeRules; } } - public IEnumerable ExcludeRules { get { return excludeRules; } } - public IEnumerable Severities { get { return severities; } } - public Dictionary> RuleArguments { get { return ruleArguments; } } + public bool RecurseCustomRulePath => recurseCustomRulePath; + public bool IncludeDefaultRules => includeDefaultRules; + public string FilePath => filePath; + public IEnumerable IncludeRules => includeRules; + public IEnumerable ExcludeRules => excludeRules; + public IEnumerable Severities => severities; + public IEnumerable CustomRulePath => customRulePath; + public Dictionary> RuleArguments => ruleArguments; /// /// Create a settings object from the input object. @@ -400,6 +406,27 @@ private void parseSettingsHashtable(Hashtable settingsHashtable) excludeRules = GetData(val, key); break; + case "customrulepath": + customRulePath = GetData(val, key); + break; + + case "includedefaultrules": + case "recursecustomrulepath": + if (!(val is bool)) + { + throw new InvalidDataException(string.Format( + CultureInfo.CurrentCulture, + Strings.SettingsValueTypeMustBeBool, + settingKey)); + } + + var booleanVal = (bool)val; + var field = this.GetType().GetField( + key, + BindingFlags.Instance | BindingFlags.IgnoreCase | BindingFlags.NonPublic); + field.SetValue(this, booleanVal); + break; + case "rules": try { diff --git a/Engine/Strings.resx b/Engine/Strings.resx index 729cd67fb..f9bb6f8a3 100644 --- a/Engine/Strings.resx +++ b/Engine/Strings.resx @@ -273,6 +273,9 @@ Cannot parse settings. Will abort the invocation. + + {0} property must be of type bool. + Temporary module location: {0}. diff --git a/New-StronglyTypedCsFileForResx.ps1 b/New-StronglyTypedCsFileForResx.ps1 index 0cc65723f..c18896f66 100644 --- a/New-StronglyTypedCsFileForResx.ps1 +++ b/New-StronglyTypedCsFileForResx.ps1 @@ -113,7 +113,7 @@ internal class {0} {{ $entry -f $name,$val } } | Out-String - + $bodyCode = $body -f $shortClassName,$ModuleName,$entries,$ClassName if ($NamespaceName) @@ -126,10 +126,10 @@ internal class {0} {{ return $resultCode -replace "`r`n?|`n","`r`n" } -$projectRoot = Split-Path $MyInvocation.InvocationName +$projectRoot = $PWD if (-not (Test-Path "$projectRoot/global.json")) { - throw "Not in solution root" + throw "Not in solution root: $projectRoot" } $inputFilePath = Join-Path $projectRoot "$project/Strings.resx" $outputFilePath = Join-Path $projectRoot "$project/Strings.cs" @@ -141,4 +141,4 @@ if ($project -eq "Rules") $className += ".Strings" $xml = [xml](Get-Content -raw $inputFilePath) $genSource = Get-StronglyTypeCsFileForResx -xml $xml -ModuleName Foo -ClassName $className -Set-Content -Encoding Ascii -Path $outputFilePath -Value $genSource \ No newline at end of file +Set-Content -Encoding Ascii -Path $outputFilePath -Value $genSource diff --git a/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 b/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 index 77c5be507..f1989fae4 100644 --- a/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 +++ b/Tests/Engine/InvokeScriptAnalyzer.tests.ps1 @@ -380,9 +380,74 @@ Describe "Test CustomizedRulePath" { $customizedRulePath = Invoke-ScriptAnalyzer $directory\TestScript.ps1 -CustomRulePath ("$directory\CommunityAnalyzerRules", "$directory\samplerule", "$directory\samplerule\samplerule2") $customizedRulePath.Count | Should Be 3 } - } + if (!$testingLibraryUsage) + { + Context "When used from settings file" { + It "Should use the CustomRulePath parameter" { + $settings = @{ + CustomRulePath = "$directory\CommunityAnalyzerRules" + IncludeDefaultRules = $false + RecurseCustomRulePath = $false + } + + $v = Invoke-ScriptAnalyzer -Path $directory\TestScript.ps1 -Settings $settings + $v.Count | Should Be 1 + } + + It "Should use the IncludeDefaultRulePath parameter" { + $settings = @{ + CustomRulePath = "$directory\CommunityAnalyzerRules" + IncludeDefaultRules = $true + RecurseCustomRulePath = $false + } + + $v = Invoke-ScriptAnalyzer -Path $directory\TestScript.ps1 -Settings $settings + $v.Count | Should Be 2 + } + + It "Should use the RecurseCustomRulePath parameter" { + $settings = @{ + CustomRulePath = "$directory\samplerule" + IncludeDefaultRules = $false + RecurseCustomRulePath = $true + } + + $v = Invoke-ScriptAnalyzer -Path $directory\TestScript.ps1 -Settings $settings + $v.Count | Should Be 3 + } + } + + Context "When used from settings file and command line simulataneusly" { + BeforeAll { + $settings = @{ + CustomRulePath = "$directory\samplerule" + IncludeDefaultRules = $false + RecurseCustomRulePath = $false + } + $isaParams = @{ + Path = "$directory\TestScript.ps1" + Settings = $settings + } + } + + It "Should combine CustomRulePaths" { + $v = Invoke-ScriptAnalyzer @isaParams -CustomRulePath "$directory\CommunityAnalyzerRules" + $v.Count | Should Be 2 + } + + It "Should override the settings IncludeDefaultRules parameter" { + $v = Invoke-ScriptAnalyzer @isaParams -IncludeDefaultRules + $v.Count | Should Be 2 + } + + It "Should override the settings RecurseCustomRulePath parameter" { + $v = Invoke-ScriptAnalyzer @isaParams -RecurseCustomRulePath + $v.Count | Should Be 3 + } + } + } Context "When used incorrectly" { It "file cannot be found" { diff --git a/Tests/Engine/Settings.tests.ps1 b/Tests/Engine/Settings.tests.ps1 index f4a634ad4..3f2825690 100644 --- a/Tests/Engine/Settings.tests.ps1 +++ b/Tests/Engine/Settings.tests.ps1 @@ -114,4 +114,69 @@ Describe "Settings Class" { $settings.RuleArguments["PSProvideCommentHelp"]["Placement"] | Should Be 'end' } } + + Context "When CustomRulePath parameter is provided" { + It "Should return an array of 1 item when only 1 path is given in a hashtable" { + $rulePath = "C:\rules\module1" + $settingsHashtable = @{ + CustomRulePath = $rulePath + } + + $settings = New-Object -TypeName $settingsTypeName -ArgumentList $settingsHashtable + $settings.CustomRulePath.Count | Should Be 1 + $settings.CustomRulePath[0] | Should be $rulePath + } + + It "Should return an array of n items when n items are given in a hashtable" { + $rulePaths = @("C:\rules\module1", "C:\rules\module2") + $settingsHashtable = @{ + CustomRulePath = $rulePaths + } + + $settings = New-Object -TypeName $settingsTypeName -ArgumentList $settingsHashtable + $settings.CustomRulePath.Count | Should Be $rulePaths.Count + 0..($rulePaths.Count - 1) | ForEach-Object { $settings.CustomRulePath[$_] | Should be $rulePaths[$_] } + + } + + It "Should detect the parameter in a settings file" { + $settings = New-Object -TypeName $settingsTypeName ` + -ArgumentList ([System.IO.Path]::Combine($project1Root, "CustomRulePathSettings.psd1")) + $settings.CustomRulePath.Count | Should Be 2 + } + } + + @("IncludeDefaultRules", "RecurseCustomRulePath") | ForEach-Object { + $paramName = $_ + Context "When $paramName parameter is provided" { + It "Should correctly set the value if a boolean is given - true" { + $settingsHashtable = @{} + $settingsHashtable.Add($paramName, $true) + + $settings = New-Object -TypeName $settingsTypeName -ArgumentList $settingsHashtable + $settings."$paramName" | Should Be $true + } + + It "Should correctly set the value if a boolean is given - false" { + $settingsHashtable = @{} + $settingsHashtable.Add($paramName, $false) + + $settings = New-Object -TypeName $settingsTypeName -ArgumentList $settingsHashtable + $settings."$paramName" | Should Be $false + } + + It "Should throw if a non-boolean value is given" { + $settingsHashtable = @{} + $settingsHashtable.Add($paramName, "some random string") + + { New-Object -TypeName $settingsTypeName -ArgumentList $settingsHashtable } | Should Throw + } + + It "Should detect the parameter in a settings file" { + $settings = New-Object -TypeName $settingsTypeName ` + -ArgumentList ([System.IO.Path]::Combine($project1Root, "CustomRulePathSettings.psd1")) + $settings."$paramName" | Should Be $true + } + } + } } diff --git a/Tests/Engine/SettingsTest/Project1/CustomRulePathSettings.psd1 b/Tests/Engine/SettingsTest/Project1/CustomRulePathSettings.psd1 new file mode 100644 index 000000000..7f7aabdf4 --- /dev/null +++ b/Tests/Engine/SettingsTest/Project1/CustomRulePathSettings.psd1 @@ -0,0 +1,5 @@ +@{ + "CustomRulePath" = @("C:\rules\module1", "C:\rules\module2") + "IncludeDefaultRules" = $true + "RecurseCustomRulePath" = $true +}