From ba86e55a02cf24f6a52809c63a414ee6365bc633 Mon Sep 17 00:00:00 2001 From: Justin Beeson Date: Tue, 28 Jul 2026 12:53:34 -0700 Subject: [PATCH] Add -NoRecurse to Invoke-PSModuleAnalyzer Callers that want to analyze a single directory's own scripts had no way to say so: Recurse was hardcoded on, so a wildcard like scripts/*.ps1 still pulled in a nested tests folder that needs different rules. Routing such a target through direct Invoke-ScriptAnalyzer instead is not a workaround, since that path hits the PSScriptAnalyzer 1.25.0 casing crash this module exists to avoid. - Add -NoRecurse, which flips the previously hardcoded Recurse argument - Thread Recurse into the casing workaround's Get-ChildItem so its per-file pass matches the scope - Document the wildcard form on SourceDirectory and add an example - Cover default-recursive vs -NoRecurse dispatch, wildcard scoping, and non-recursive casing - Bump the module to 3.1.0 --- src/PSModuleUtils.psd1 | 2 +- ...nvoke-PSModuleAnalyzerCasingWorkaround.ps1 | 2 +- src/Public/Invoke-PSModuleAnalyzer.ps1 | 15 +++++++-- tests/Invoke-PSModuleAnalyzer.Tests.ps1 | 29 +++++++++++++++++ ...PSModuleAnalyzerCasingWorkaround.Tests.ps1 | 32 +++++++++++++++++++ 5 files changed, 75 insertions(+), 5 deletions(-) diff --git a/src/PSModuleUtils.psd1 b/src/PSModuleUtils.psd1 index 59aeca2..87e4826 100644 --- a/src/PSModuleUtils.psd1 +++ b/src/PSModuleUtils.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'PSModuleUtils.psm1' - ModuleVersion = '3.0.0' + ModuleVersion = '3.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 d935d27..1dddb97 100644 --- a/src/Private/Invoke-PSModuleAnalyzerCasingWorkaround.ps1 +++ b/src/Private/Invoke-PSModuleAnalyzerCasingWorkaround.ps1 @@ -102,7 +102,7 @@ function Invoke-PSModuleAnalyzerCasingWorkaround { } $casingResultCount = 0 - Get-ChildItem -Path $Path -Recurse -File -ErrorAction Stop | + Get-ChildItem -Path $Path -Recurse:$Recurse -File -ErrorAction Stop | Where-Object { $_.Extension -in '.ps1', '.psm1', '.psd1' } | ForEach-Object { $casingArguments.Path = $_.FullName diff --git a/src/Public/Invoke-PSModuleAnalyzer.ps1 b/src/Public/Invoke-PSModuleAnalyzer.ps1 index ee325f6..952d484 100644 --- a/src/Public/Invoke-PSModuleAnalyzer.ps1 +++ b/src/Public/Invoke-PSModuleAnalyzer.ps1 @@ -6,7 +6,8 @@ Invokes PSScriptAnalyzer on a directory using a more strict set of rules than de Invokes PSScriptAnalyzer on a directory using a more strict set of rules than default. .PARAMETER SourceDirectory -The directory to analyze. +The directory to analyze. Also accepts a file path or a wildcard such as scripts/*.ps1, which pairs +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 @@ -19,12 +20,19 @@ Whether to fix the issues found. Returns analyzer diagnostics without exiting the caller when violations are found. Use this when another command needs to process the diagnostics, such as converting them to SARIF. +.PARAMETER NoRecurse +Analyzes only what SourceDirectory itself matches instead of descending into subdirectories. Use with +a wildcard to keep a subtree with different rules, such as a tests folder, out of the run. + .OUTPUTS Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord .EXAMPLE Invoke-PSModuleAnalyzer -SourceDirectory $PWD/src -Fix +.EXAMPLE +Invoke-PSModuleAnalyzer -SourceDirectory $PWD/scripts/*.ps1 -NoRecurse + .NOTES N/A #> @@ -35,13 +43,14 @@ function Invoke-PSModuleAnalyzer { [String]$SourceDirectory = "$PWD/src", [String]$Settings = (Get-PSModuleAnalyzerSettingsPath -CallerScriptRoot $PSScriptRoot), [Switch]$Fix, - [Switch]$NoExit + [Switch]$NoExit, + [Switch]$NoRecurse ) $scriptAnalyzerArgs = @{ Path = $SourceDirectory Settings = $Settings - Recurse = $true + Recurse = (-not $NoRecurse) Severity = 'Error', 'Warning', 'Information' EnableExit = (-not $Fix -and -not $NoExit) ReportSummary = $true diff --git a/tests/Invoke-PSModuleAnalyzer.Tests.ps1 b/tests/Invoke-PSModuleAnalyzer.Tests.ps1 index 427e816..71317e0 100644 --- a/tests/Invoke-PSModuleAnalyzer.Tests.ps1 +++ b/tests/Invoke-PSModuleAnalyzer.Tests.ps1 @@ -83,6 +83,35 @@ Describe 'Unit Tests' -Tag 'Unit' { } } + It 'should analyze recursively by default and honor NoRecurse' { + Mock Invoke-PSModuleAnalyzerCasingWorkaround {} + + Invoke-PSModuleAnalyzer -SourceDirectory $TestDrive + Should -Invoke Invoke-PSModuleAnalyzerCasingWorkaround -Exactly -Times 1 -ParameterFilter { + $Recurse -eq $true + } + + Invoke-PSModuleAnalyzer -SourceDirectory $TestDrive -NoRecurse + Should -Invoke Invoke-PSModuleAnalyzerCasingWorkaround -Exactly -Times 1 -ParameterFilter { + $Recurse -eq $false + } + } + + It 'should keep a nested directory out of a NoRecurse wildcard run' { + $fixtureDir = Join-Path -Path $TestDrive -ChildPath 'NoRecurseFixture' + $nestedDir = Join-Path -Path $fixtureDir -ChildPath 'Nested' + New-Item -ItemType Directory -Path $nestedDir -Force | Out-Null + "function Get-Top { 'ok' }" | Set-Content -Path "$fixtureDir/Top.ps1" -Encoding utf8 + 'function Get-Nested { gci }' | Set-Content -Path "$nestedDir/Alias.ps1" -Encoding utf8 + + $recursiveFindings = Invoke-PSModuleAnalyzer -SourceDirectory $fixtureDir -NoExit + $topOnlyFindings = Invoke-PSModuleAnalyzer -SourceDirectory "$fixtureDir/*.ps1" -NoRecurse -NoExit + + $recursiveFindings.ScriptName | Should -Contain 'Alias.ps1' + $topOnlyFindings.ScriptName | Should -Contain 'Top.ps1' + $topOnlyFindings.ScriptName | Should -Not -Contain 'Alias.ps1' + } + It 'should preserve nested parentheses containing function definitions in fix mode' { $fixtureDir = Join-Path -Path $TestDrive -ChildPath 'IndentationFixture' New-Item -ItemType Directory -Path $fixtureDir -Force | Out-Null diff --git a/tests/Invoke-PSModuleAnalyzerCasingWorkaround.Tests.ps1 b/tests/Invoke-PSModuleAnalyzerCasingWorkaround.Tests.ps1 index 5052ffc..7510d75 100644 --- a/tests/Invoke-PSModuleAnalyzerCasingWorkaround.Tests.ps1 +++ b/tests/Invoke-PSModuleAnalyzerCasingWorkaround.Tests.ps1 @@ -50,6 +50,38 @@ Describe 'Invoke-PSModuleAnalyzerCasingWorkaround' -Tag 'Unit' { $scriptAnalyzerArguments.Settings | Should -BeExactly $script:defaultSettingsPath } + 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' + $null = New-Item -ItemType Directory -Path $nestedDir -Force + "function Get-First { 'first' }" | Set-Content -Path "$fixtureDir/First.ps1" + "function Get-Second { 'second' }" | Set-Content -Path "$nestedDir/Second.ps1" + $scriptAnalyzerArguments = @{ + Path = $fixtureDir + Settings = $script:defaultSettingsPath + Recurse = $false + 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 '*First.ps1' + } + Should -Invoke Invoke-ScriptAnalyzer -Exactly -Times 0 -ParameterFilter { + $Path -like '*Second.ps1' + } + Should -Invoke Invoke-ScriptAnalyzer -Exactly -Times 1 -ParameterFilter { + $Settings.Rules.PSUseCorrectCasing.Enable -eq $false -and + $Recurse -eq $false + } + } + It 'should invoke PSScriptAnalyzer unchanged when command casing does not need the workaround' { $settingsPath = Join-Path -Path $TestDrive -ChildPath 'DisabledCasingSettings.psd1' @'