From d53f794b38b280f452ec5e4f5993151b1ae7942e Mon Sep 17 00:00:00 2001 From: Trent Blackburn Date: Wed, 6 May 2026 15:47:33 -0400 Subject: [PATCH 1/3] style: enforce quoted literals, named parameters, and parameter validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The repo's own powershell.instructions.md mandates three rules for in-tree code: - literal string parameter values are single-quoted - cmdlet calls use named parameters (when more than one arg is passed) - every function parameter has an appropriate validator This commit applies those rules to the template's first-party files — the example public/private functions, the test scaffolding, the meta helper modules, and build.ps1. Notable changes: - build.ps1 gains [ValidateNotNullOrEmpty()] on $Task - tests/MetaFixers.psm1 + tests/ManifestHelpers.psm1 gain validators on every parameter that didn't already have one - tests/Help.tests.ps1, tests/Manifest.tests.ps1, tests/Meta.tests.ps1 rename positional Split-Path / Join-Path / Get-Module / Get-Content calls to use named -Path / -ChildPath / -Name / -Pattern parameters - the example Get-{{Prefix}}Example.ps1 / its tests get the same treatment so newly-scaffolded modules start out compliant No behavioural changes. CI on the un-initialized template still skips build/test (the {{GUID}} placeholder is unparseable until init runs); parse-checks confirm every edited file is syntactically valid. Co-Authored-By: Claude Opus 4.7 (1M context) --- build.ps1 | 1 + tests/Help.tests.ps1 | 12 ++++++------ tests/Manifest.tests.ps1 | 16 ++++++++-------- tests/ManifestHelpers.psm1 | 17 ++++++++++++----- tests/Meta.tests.ps1 | 6 +++--- tests/MetaFixers.psm1 | 6 +++++- .../Private/Invoke-{{Prefix}}Helper.tests.ps1 | 10 +++++----- .../Unit/Public/Get-{{Prefix}}Example.tests.ps1 | 8 ++++---- {{ModuleName}}/Public/Get-{{Prefix}}Example.ps1 | 4 ++-- 9 files changed, 46 insertions(+), 34 deletions(-) diff --git a/build.ps1 b/build.ps1 index 8887f8f..c76be82 100644 --- a/build.ps1 +++ b/build.ps1 @@ -40,6 +40,7 @@ param( @() } })] + [ValidateNotNullOrEmpty()] [string[]]$Task = 'default', # Bootstrap dependencies diff --git a/tests/Help.tests.ps1 b/tests/Help.tests.ps1 index 4f1a16c..ea99c57 100644 --- a/tests/Help.tests.ps1 +++ b/tests/Help.tests.ps1 @@ -57,10 +57,10 @@ BeforeDiscovery { } # PowerShellBuild outputs to Output///, override BHBuildOutput - $projectRoot = Split-Path -Parent $PSScriptRoot - $sourceManifest = Join-Path $projectRoot "$Env:BHProjectName/$Env:BHProjectName.psd1" + $projectRoot = Split-Path -Path $PSScriptRoot -Parent + $sourceManifest = Join-Path -Path $projectRoot -ChildPath "$Env:BHProjectName/$Env:BHProjectName.psd1" $moduleVersion = (Import-PowerShellDataFile -Path $sourceManifest).ModuleVersion - $Env:BHBuildOutput = Join-Path $projectRoot "Output/$Env:BHProjectName/$moduleVersion" + $Env:BHBuildOutput = Join-Path -Path $projectRoot -ChildPath "Output/$Env:BHProjectName/$moduleVersion" # Define the path to the module manifest $moduleManifestFilename = $Env:BHProjectName + '.psd1' @@ -72,18 +72,18 @@ BeforeDiscovery { 'Classes' ) | ForEach-Object { $path = Join-Path -Path $Env:BHBuildOutput -ChildPath $_ - if (Test-Path $path) { + if (Test-Path -Path $path) { $global:CustomTypes += (Get-ChildItem -Path $path -Recurse -ErrorAction 'SilentlyContinue').BaseName } } # Remove all versions of the module from the session. Pester can't handle multiple versions. - Get-Module $Env:BHProjectName | Remove-Module -Force -ErrorAction 'Ignore' + Get-Module -Name $Env:BHProjectName | Remove-Module -Force -ErrorAction 'Ignore' Import-Module -Name $moduleManifestPath -Verbose:$false -ErrorAction 'Stop' # Get module commands $getCommandParameters = @{ - Module = (Get-Module $Env:BHProjectName) + Module = (Get-Module -Name $Env:BHProjectName) CommandType = [System.Management.Automation.CommandTypes[]]'Cmdlet, Function' # Not alias } if ($PSVersionTable.PSVersion.Major -lt 6) { diff --git a/tests/Manifest.tests.ps1 b/tests/Manifest.tests.ps1 index fe6e3f6..0762051 100644 --- a/tests/Manifest.tests.ps1 +++ b/tests/Manifest.tests.ps1 @@ -69,10 +69,10 @@ BeforeDiscovery { } # PowerShellBuild outputs to Output///, override BHBuildOutput - $projectRoot = Split-Path -Parent $PSScriptRoot - $sourceManifest = Join-Path $projectRoot "$Env:BHProjectName/$Env:BHProjectName.psd1" + $projectRoot = Split-Path -Path $PSScriptRoot -Parent + $sourceManifest = Join-Path -Path $projectRoot -ChildPath "$Env:BHProjectName/$Env:BHProjectName.psd1" $moduleVersion = (Import-PowerShellDataFile -Path $sourceManifest).ModuleVersion - $Env:BHBuildOutput = Join-Path $projectRoot "Output/$Env:BHProjectName/$moduleVersion" + $Env:BHBuildOutput = Join-Path -Path $projectRoot -ChildPath "Output/$Env:BHProjectName/$moduleVersion" # Define the path to the module manifest $moduleManifestFilename = $Env:BHProjectName + '.psd1' @@ -108,10 +108,10 @@ BeforeAll { } # PowerShellBuild outputs to Output///, override BHBuildOutput - $projectRoot = Split-Path -Parent $PSScriptRoot - $sourceManifest = Join-Path $projectRoot "$Env:BHProjectName/$Env:BHProjectName.psd1" + $projectRoot = Split-Path -Path $PSScriptRoot -Parent + $sourceManifest = Join-Path -Path $projectRoot -ChildPath "$Env:BHProjectName/$Env:BHProjectName.psd1" $moduleVersion = (Import-PowerShellDataFile -Path $sourceManifest).ModuleVersion - $Env:BHBuildOutput = Join-Path $projectRoot "Output/$Env:BHProjectName/$moduleVersion" + $Env:BHBuildOutput = Join-Path -Path $projectRoot -ChildPath "Output/$Env:BHProjectName/$moduleVersion" # Define the path to the module manifest $moduleManifestFilename = $Env:BHProjectName + '.psd1' @@ -135,12 +135,12 @@ BeforeAll { Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'ManifestHelpers.psm1') -Verbose:$false -Force $requirementsPath = Join-Path -Path $env:BHProjectPath -ChildPath 'requirements.psd1' - $requirements = Import-PowerShellDataFile -Path $requirementsPath -ErrorAction Stop + $requirements = Import-PowerShellDataFile -Path $requirementsPath -ErrorAction 'Stop' # Parse the version from the changelog $changelogPath = Join-Path -Path $Env:BHProjectPath -ChildPath 'CHANGELOG.md' $changelogVersionPattern = '^##\s\\?\[(?(\d+\.){1,3}\d+)\\?\]' # Matches on a line that starts with '## [Version]' or '## \[Version\]' - $changelogVersion = Get-Content $changelogPath | ForEach-Object { + $changelogVersion = Get-Content -Path $changelogPath | ForEach-Object { if ($_ -match $changelogVersionPattern) { $changelogVersion = $matches.Version break diff --git a/tests/ManifestHelpers.psm1 b/tests/ManifestHelpers.psm1 index b797866..6470812 100644 --- a/tests/ManifestHelpers.psm1 +++ b/tests/ManifestHelpers.psm1 @@ -36,11 +36,12 @@ function Split-SemVerString { [OutputType([hashtable])] param( [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] [string]$VersionString ) if ([string]::IsNullOrEmpty($VersionString)) { - throw "VersionString cannot be empty or null" + throw 'VersionString cannot be empty or null' } # Strip build metadata per SemVer 2.0.0 — it does not affect precedence and is @@ -92,9 +93,11 @@ function Compare-SemVerPrerelease { [OutputType([int])] param( [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] [string]$FirstPrerelease, [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] [string]$SecondPrerelease ) @@ -208,6 +211,7 @@ function Test-VersionComparison { [OutputType([bool])] param( [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] [string]$FirstVersion, [Parameter(Mandatory = $false)] @@ -215,6 +219,7 @@ function Test-VersionComparison { [string]$FirstPrerelease, [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] [string]$SecondVersion, [Parameter(Mandatory = $false)] @@ -325,9 +330,11 @@ function Test-VersionConstraint { [OutputType([bool])] param( [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] [string]$ManifestVersion, [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] [string]$RequirementsVersion, [Parameter(Mandatory = $true)] @@ -337,14 +344,14 @@ function Test-VersionConstraint { # Validate input versions are not empty if ([string]::IsNullOrWhiteSpace($ManifestVersion)) { - throw "ManifestVersion cannot be empty or whitespace" + throw 'ManifestVersion cannot be empty or whitespace' } if ([string]::IsNullOrWhiteSpace($RequirementsVersion)) { - throw "RequirementsVersion cannot be empty or whitespace" + throw 'RequirementsVersion cannot be empty or whitespace' } - $manifestParts = Split-SemVerString $ManifestVersion - $requirementsParts = Split-SemVerString $RequirementsVersion + $manifestParts = Split-SemVerString -VersionString $ManifestVersion + $requirementsParts = Split-SemVerString -VersionString $RequirementsVersion $comparisonParameters = @{ FirstVersion = $requirementsParts.Version diff --git a/tests/Meta.tests.ps1 b/tests/Meta.tests.ps1 index 68298f0..c1813ec 100644 --- a/tests/Meta.tests.ps1 +++ b/tests/Meta.tests.ps1 @@ -9,11 +9,11 @@ BeforeAll { $projectRoot = $PSScriptRoot } - $allTextFiles = Get-TextFilesList $projectRoot + $allTextFiles = Get-TextFilesList -Root $projectRoot $unicodeFilesCount = 0 $totalTabsCount = 0 foreach ($textFile in $allTextFiles) { - if (Test-FileUnicode $textFile) { + if (Test-FileUnicode -FileInfo $textFile) { $unicodeFilesCount++ Write-Warning ( "File $($textFile.FullName) contains 0x00 bytes." + @@ -24,7 +24,7 @@ BeforeAll { $unicodeFilesCount | Should -Be 0 $fileName = $textFile.FullName - (Get-Content $fileName -Raw) | Select-String "`t" | Foreach-Object { + (Get-Content -Path $fileName -Raw) | Select-String -Pattern "`t" | Foreach-Object { Write-Warning ( "There are tabs in $fileName." + ' Use Fixer "Get-TextFilesList `$pwd | ConvertTo-SpaceIndentation".' diff --git a/tests/MetaFixers.psm1 b/tests/MetaFixers.psm1 index 7dec0c8..43da225 100644 --- a/tests/MetaFixers.psm1 +++ b/tests/MetaFixers.psm1 @@ -27,6 +27,7 @@ function ConvertTo-UTF8 { [OutputType([void])] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [ValidateNotNull()] [System.IO.FileInfo]$FileInfo ) @@ -56,6 +57,7 @@ function ConvertTo-SpaceIndentation { [OutputType([void])] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [ValidateNotNull()] [System.IO.FileInfo]$FileInfo ) @@ -85,6 +87,7 @@ function Get-TextFilesList { [OutputType([System.IO.FileInfo])] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [ValidateNotNullOrEmpty()] [string]$Root ) @@ -159,10 +162,11 @@ function Get-UnicodeFilesList { [OutputType([System.IO.FileInfo])] param( [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] [string]$Root ) $root | Get-TextFilesList | Where-Object { - Test-FileUnicode $_ + Test-FileUnicode -FileInfo $_ } } diff --git a/tests/Unit/Private/Invoke-{{Prefix}}Helper.tests.ps1 b/tests/Unit/Private/Invoke-{{Prefix}}Helper.tests.ps1 index e4879ca..250662c 100644 --- a/tests/Unit/Private/Invoke-{{Prefix}}Helper.tests.ps1 +++ b/tests/Unit/Private/Invoke-{{Prefix}}Helper.tests.ps1 @@ -17,20 +17,20 @@ BeforeDiscovery { } # PowerShellBuild outputs to Output/// - $projectRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) - $sourceManifest = Join-Path $projectRoot "$Env:BHProjectName/$Env:BHProjectName.psd1" + $projectRoot = Split-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -Parent + $sourceManifest = Join-Path -Path $projectRoot -ChildPath "$Env:BHProjectName/$Env:BHProjectName.psd1" $moduleVersion = (Import-PowerShellDataFile -Path $sourceManifest).ModuleVersion - $Env:BHBuildOutput = Join-Path $projectRoot "Output/$Env:BHProjectName/$moduleVersion" + $Env:BHBuildOutput = Join-Path -Path $projectRoot -ChildPath "Output/$Env:BHProjectName/$moduleVersion" } BeforeAll { # Import the module from the build output $moduleManifestPath = Join-Path -Path $Env:BHBuildOutput -ChildPath "$Env:BHProjectName.psd1" - Get-Module $Env:BHProjectName | Remove-Module -Force -ErrorAction 'Ignore' + Get-Module -Name $Env:BHProjectName | Remove-Module -Force -ErrorAction 'Ignore' Import-Module -Name $moduleManifestPath -Force -ErrorAction 'Stop' } -InModuleScope $Env:BHProjectName { +InModuleScope -ModuleName $Env:BHProjectName -ScriptBlock { Describe 'Invoke-{{Prefix}}Helper' { Context 'Basic functionality' { diff --git a/tests/Unit/Public/Get-{{Prefix}}Example.tests.ps1 b/tests/Unit/Public/Get-{{Prefix}}Example.tests.ps1 index 47fdbc0..051affa 100644 --- a/tests/Unit/Public/Get-{{Prefix}}Example.tests.ps1 +++ b/tests/Unit/Public/Get-{{Prefix}}Example.tests.ps1 @@ -17,16 +17,16 @@ BeforeDiscovery { } # PowerShellBuild outputs to Output/// - $projectRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) - $sourceManifest = Join-Path $projectRoot "$Env:BHProjectName/$Env:BHProjectName.psd1" + $projectRoot = Split-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -Parent + $sourceManifest = Join-Path -Path $projectRoot -ChildPath "$Env:BHProjectName/$Env:BHProjectName.psd1" $moduleVersion = (Import-PowerShellDataFile -Path $sourceManifest).ModuleVersion - $Env:BHBuildOutput = Join-Path $projectRoot "Output/$Env:BHProjectName/$moduleVersion" + $Env:BHBuildOutput = Join-Path -Path $projectRoot -ChildPath "Output/$Env:BHProjectName/$moduleVersion" } BeforeAll { # Import the module from the build output $moduleManifestPath = Join-Path -Path $Env:BHBuildOutput -ChildPath "$Env:BHProjectName.psd1" - Get-Module $Env:BHProjectName | Remove-Module -Force -ErrorAction 'Ignore' + Get-Module -Name $Env:BHProjectName | Remove-Module -Force -ErrorAction 'Ignore' Import-Module -Name $moduleManifestPath -Force -ErrorAction 'Stop' } diff --git a/{{ModuleName}}/Public/Get-{{Prefix}}Example.ps1 b/{{ModuleName}}/Public/Get-{{Prefix}}Example.ps1 index ea095b9..e3879fd 100644 --- a/{{ModuleName}}/Public/Get-{{Prefix}}Example.ps1 +++ b/{{ModuleName}}/Public/Get-{{Prefix}}Example.ps1 @@ -37,7 +37,7 @@ function Get-{{Prefix}}Example { ) begin { - Write-Verbose "Starting Get-{{Prefix}}Example" + Write-Verbose 'Starting Get-{{Prefix}}Example' } process { @@ -51,6 +51,6 @@ function Get-{{Prefix}}Example { } end { - Write-Verbose "Completed Get-{{Prefix}}Example" + Write-Verbose 'Completed Get-{{Prefix}}Example' } } From 703c2a83cc032fb41c6ca1e1db7fc529cd463c3a Mon Sep 17 00:00:00 2001 From: Trent Blackburn Date: Wed, 6 May 2026 18:25:13 -0400 Subject: [PATCH 2/3] chore: pin line endings to LF via .gitattributes The repo already stores every text file as LF in the index, but Windows clones with global core.autocrlf=true rewrite the working tree to CRLF on checkout. Subsequent edits made through tooling that writes LF (most editors and AI agents do) trigger 'LF will be replaced by CRLF the next time Git touches it' warnings on every commit. Pinning eol=lf in .gitattributes overrides the contributor's global autocrlf setting for this repo and silences the warning. PowerShell on Windows handles LF fine, and the devcontainer already runs Linux. NOTE: GitHub template repos are one-time copies at creation, so this fix does NOT propagate to repositories already created from this template. Existing downstream repos must apply the same .gitattributes change separately. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitattributes | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.gitattributes b/.gitattributes index df492b0..5a3d63b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,10 @@ +# Normalize line endings to LF in both the repo and the working tree, regardless of +# the contributor's global core.autocrlf setting. PowerShell on Windows handles LF +# fine, and the devcontainer runs Linux. Without this, Windows clones with +# core.autocrlf=true emit "LF will be replaced by CRLF the next time Git touches it" +# warnings on every commit when files are edited by tooling that writes LF (most +# editors and AI agents do). +* text=auto eol=lf + # The docs are generated by the build script and should be considered artifacts docs/en-US/* linguist-generated From abf5d46c331176ce5a178e1d7c9baa4c2f046496 Mon Sep 17 00:00:00 2001 From: Trent Blackburn Date: Sun, 10 May 2026 02:09:47 -0400 Subject: [PATCH 3/3] style: remove unreachable IsNullOrEmpty guard in Split-SemVerString MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit added [ValidateNotNullOrEmpty()] to $VersionString, which makes the in-body IsNullOrEmpty check unreachable under normal parameter binding — both Copilot and CodeRabbit flagged it as dead code. Removing the redundant guard. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/ManifestHelpers.psm1 | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/ManifestHelpers.psm1 b/tests/ManifestHelpers.psm1 index 6470812..533824b 100644 --- a/tests/ManifestHelpers.psm1 +++ b/tests/ManifestHelpers.psm1 @@ -40,10 +40,6 @@ function Split-SemVerString { [string]$VersionString ) - if ([string]::IsNullOrEmpty($VersionString)) { - throw 'VersionString cannot be empty or null' - } - # Strip build metadata per SemVer 2.0.0 — it does not affect precedence and is # not valid for [System.Version], so it must be removed before further parsing. $coreVersion = ($VersionString -split '\+', 2)[0]