From d946410a7b9fd4cd425a7ad87b432684ce21ff98 Mon Sep 17 00:00:00 2001 From: Justin Beeson Date: Wed, 29 Jul 2026 08:25:33 -0700 Subject: [PATCH] Accept settings as a hashtable, not only a file path Invoke-ScriptAnalyzer types its -Settings as object and has always taken a hashtable; this module narrowed it to [String]. A caller assembling settings at run time therefore had to serialize them to a file first, which meant writing to a temp path, cleaning it up, and keeping the write collision-free between processes -- all to satisfy a constraint that was never PSScriptAnalyzer's. The workaround already passed hashtables to Invoke-ScriptAnalyzer internally, so only the parameter types and the one Import-PowerShellDataFile stood in the way. - Widen -Settings to [Object] on Invoke-PSModuleAnalyzer and the private casing workaround - Import the data file only when the value is a string - Copy the levels the split-casing run edits, so a caller's hashtable is left alone - Bump to 4.1.0; a string still binds, so nothing existing changes --- src/PSModuleUtils.psd1 | 2 +- ...nvoke-PSModuleAnalyzerCasingWorkaround.ps1 | 22 ++++++++++---- src/Public/Invoke-PSModuleAnalyzer.ps1 | 12 ++++++-- tests/Invoke-PSModuleAnalyzer.Tests.ps1 | 15 ++++++++++ ...PSModuleAnalyzerCasingWorkaround.Tests.ps1 | 29 +++++++++++++++++++ 5 files changed, 71 insertions(+), 9 deletions(-) diff --git a/src/PSModuleUtils.psd1 b/src/PSModuleUtils.psd1 index d2946a0..263f3f9 100644 --- a/src/PSModuleUtils.psd1 +++ b/src/PSModuleUtils.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'PSModuleUtils.psm1' - ModuleVersion = '4.0.0' + ModuleVersion = '4.1.0' GUID = '3c63c38f-c32c-4837-a6fa-0b456f4099ce' Author = '' CompanyName = '' diff --git a/src/Private/Invoke-PSModuleAnalyzerCasingWorkaround.ps1 b/src/Private/Invoke-PSModuleAnalyzerCasingWorkaround.ps1 index 1dddb97..40abdb6 100644 --- a/src/Private/Invoke-PSModuleAnalyzerCasingWorkaround.ps1 +++ b/src/Private/Invoke-PSModuleAnalyzerCasingWorkaround.ps1 @@ -11,7 +11,8 @@ command-casing analysis. The file or directory to analyze. .PARAMETER Settings -The PSScriptAnalyzer settings file to use. +A settings file path or a settings hashtable, as Invoke-ScriptAnalyzer itself accepts. A hashtable is +not modified; the split run gets a copy with command casing disabled. .PARAMETER Recurse Analyzes files recursively. @@ -39,7 +40,7 @@ function Invoke-PSModuleAnalyzerCasingWorkaround { [String]$Path, [Parameter(Mandatory)] - [String]$Settings, + [Object]$Settings, [Switch]$Recurse, @@ -57,7 +58,12 @@ function Invoke-PSModuleAnalyzerCasingWorkaround { $recursiveAnalyzerArguments[$argument.Key] = $argument.Value } - $settingsData = Import-PowerShellDataFile -Path $Settings -ErrorAction Stop + $settingsData = if ($Settings -is [Collections.IDictionary]) { + $Settings + } + else { + Import-PowerShellDataFile -Path $Settings -ErrorAction Stop + } $correctCasingRule = $settingsData.Rules.PSUseCorrectCasing $includeRules = @($settingsData.IncludeRules | Where-Object { $_ }) $excludeRules = @($settingsData.ExcludeRules | Where-Object { $_ }) @@ -76,8 +82,14 @@ function Invoke-PSModuleAnalyzerCasingWorkaround { ) if ($splitCommandCasing) { - $settingsData.Rules.PSUseCorrectCasing.Enable = $false - $recursiveAnalyzerArguments.Settings = $settingsData + # Copied rather than edited in place, because a caller that passed a hashtable still owns it. + $recursiveCasingRule = @{} + $correctCasingRule + $recursiveCasingRule.Enable = $false + $recursiveRules = @{} + $settingsData.Rules + $recursiveRules.PSUseCorrectCasing = $recursiveCasingRule + $recursiveSettings = @{} + $settingsData + $recursiveSettings.Rules = $recursiveRules + $recursiveAnalyzerArguments.Settings = $recursiveSettings $casingSettings = @{ IncludeRules = @('PSUseCorrectCasing') Rules = @{ diff --git a/src/Public/Invoke-PSModuleAnalyzer.ps1 b/src/Public/Invoke-PSModuleAnalyzer.ps1 index dc4c7d7..96e40c9 100644 --- a/src/Public/Invoke-PSModuleAnalyzer.ps1 +++ b/src/Public/Invoke-PSModuleAnalyzer.ps1 @@ -15,8 +15,9 @@ The directory to analyze. Also accepts a file path or a wildcard such as scripts with -NoRecurse to analyze a directory's own scripts without descending into folders beneath it. .PARAMETER Settings -The settings file to use. Defaults to the bundled PSScriptAnalyzerSettings.psd1, resolved for both a -source checkout and a built module layout. +A settings file path or a settings hashtable, as Invoke-ScriptAnalyzer itself accepts. Defaults to the +bundled PSScriptAnalyzerSettings.psd1, resolved for both a source checkout and a built module layout. +Pass a hashtable when the settings are assembled at run time, so nothing has to be written to disk. .PARAMETER Fix Whether to fix the issues found. @@ -48,6 +49,11 @@ if ($findings) { .EXAMPLE Invoke-PSModuleAnalyzer -SourceDirectory $PWD/src -EnableExit +.EXAMPLE +$settings = Import-PowerShellDataFile -Path ./PSScriptAnalyzerSettings.psd1 +$settings.ExcludeRules += 'PSUseShouldProcessForStateChangingFunctions' +Invoke-PSModuleAnalyzer -SourceDirectory $PWD/tests -Settings $settings + .NOTES N/A #> @@ -56,7 +62,7 @@ function Invoke-PSModuleAnalyzer { [OutputType('Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord')] param ( [String]$SourceDirectory = "$PWD/src", - [String]$Settings = (Get-PSModuleAnalyzerSettingsPath -CallerScriptRoot $PSScriptRoot), + [Object]$Settings = (Get-PSModuleAnalyzerSettingsPath -CallerScriptRoot $PSScriptRoot), [Switch]$Fix, [Switch]$NoRecurse, [Switch]$EnableExit diff --git a/tests/Invoke-PSModuleAnalyzer.Tests.ps1 b/tests/Invoke-PSModuleAnalyzer.Tests.ps1 index 8304701..c6922d5 100644 --- a/tests/Invoke-PSModuleAnalyzer.Tests.ps1 +++ b/tests/Invoke-PSModuleAnalyzer.Tests.ps1 @@ -112,6 +112,21 @@ Describe 'Unit Tests' -Tag 'Unit' { $topOnlyFindings.ScriptName | Should -Not -Contain 'Alias.ps1' } + It 'should analyze against a settings hashtable, with nothing written to disk' { + $fixtureDir = Join-Path -Path $TestDrive -ChildPath 'HashtableSettingsRun' + New-Item -ItemType Directory -Path $fixtureDir -Force | Out-Null + 'function Get-Aliased { gci }' | Set-Content -Path "$fixtureDir/Aliased.ps1" -Encoding utf8 + $settings = Import-PowerShellDataFile -Path ( + Resolve-Path -Path $PSScriptRoot/../src/Settings/PSScriptAnalyzerSettings.psd1 + ).Path + $settings.ExcludeRules = @($settings.ExcludeRules) + 'PSProvideCommentHelp' + + $findings = Invoke-PSModuleAnalyzer -SourceDirectory $fixtureDir -Settings $settings + + $findings.RuleName | Should -Contain 'PSAvoidUsingCmdletAliases' + $findings.RuleName | Should -Not -Contain 'PSProvideCommentHelp' + } + It 'should hand a finding back to the caller rather than exiting on it' { $fixtureDir = Join-Path -Path $TestDrive -ChildPath 'DefaultReturnFixture' New-Item -ItemType Directory -Path $fixtureDir -Force | Out-Null diff --git a/tests/Invoke-PSModuleAnalyzerCasingWorkaround.Tests.ps1 b/tests/Invoke-PSModuleAnalyzerCasingWorkaround.Tests.ps1 index 7510d75..e6178e4 100644 --- a/tests/Invoke-PSModuleAnalyzerCasingWorkaround.Tests.ps1 +++ b/tests/Invoke-PSModuleAnalyzerCasingWorkaround.Tests.ps1 @@ -50,6 +50,35 @@ Describe 'Invoke-PSModuleAnalyzerCasingWorkaround' -Tag 'Unit' { $scriptAnalyzerArguments.Settings | Should -BeExactly $script:defaultSettingsPath } + It 'should accept settings as a hashtable and leave the caller''s copy untouched' { + $fixtureDir = Join-Path -Path $TestDrive -ChildPath 'HashtableSettingsFixture' + $null = New-Item -ItemType Directory -Path $fixtureDir -Force + "function Get-Only { 'only' }" | Set-Content -Path "$fixtureDir/Only.ps1" + $callerSettings = Import-PowerShellDataFile -Path $script:defaultSettingsPath + $scriptAnalyzerArguments = @{ + Path = $fixtureDir + Settings = $callerSettings + Recurse = $true + Severity = 'Information' + EnableExit = $false + ReportSummary = $true + ErrorAction = 'Stop' + } + Mock Invoke-ScriptAnalyzer {} + + Invoke-PSModuleAnalyzerCasingWorkaround @scriptAnalyzerArguments + + Should -Invoke Invoke-ScriptAnalyzer -Exactly -Times 1 -ParameterFilter { + $Settings.IncludeRules -contains 'PSUseCorrectCasing' -and + $Path -like '*Only.ps1' + } + Should -Invoke Invoke-ScriptAnalyzer -Exactly -Times 1 -ParameterFilter { + $Settings.Rules.PSUseCorrectCasing.Enable -eq $false -and + $Recurse -eq $true + } + $callerSettings.Rules.PSUseCorrectCasing.Enable | Should -BeTrue + } + It 'should enumerate only the top level for the casing pass when Recurse is false' { $fixtureDir = Join-Path -Path $TestDrive -ChildPath 'NonRecursiveCasingFixture' $nestedDir = Join-Path -Path $fixtureDir -ChildPath 'Nested'