From 0922e7d7ed975fe3e291b55ed9b7e78999eb127b Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Sat, 19 Sep 2026 16:04:39 -0700 Subject: [PATCH 1/2] build: add PSScriptAnalyzer clean-pass to build (#487) Implements Phase 3d analysis from #487. - Add PSScriptAnalyzerSettings.psd1 at the repo root: runs the full PSScriptAnalyzer default ruleset (IncludeDefaultRules), with only two repo-wide exclusions (PSAvoidUsingWriteHost for the console UX, PSUseToExportFieldsInManifest for the manifest's VariablesToExport = '*'). Deliberately omits a Severity key, since setting one silently drops ParseError-severity records with no way to see what was suppressed. - Wire the settings file into psakeFile.ps1 via $PSBPreference.Test.ScriptAnalysis.SettingsPath, and add a PrepareAnalysis task that puts the staged module on PSModulePath before analysis runs. Without it, every koan's `using module PSKoans` fails to resolve, which both raises a ModuleNotFoundDuringParse error per file and silently suppresses every other rule that depends on symbol resolution (aliases, output types, etc.) for that file. - Fix ~41 PSUseDeclaredVarsMoreThanAssignments false positives across koan files by scoping BeforeAll/BeforeEach fixture variables with $script:, mirroring the same fix already applied to tests/*.Tests.ps1. These are genuine cross-block Pester usages (BeforeAll -> sibling It) that PSScriptAnalyzer cannot trace across scriptblock boundaries. - Add justified SuppressMessageAttribute annotations (each with a specific comment) for genuine false positives that can't be fixed without altering the exercise: values consumed only by an unsolved `__`/`____` blank, ActiveDirectory -Filter strings expanded by the AD provider rather than PowerShell interpolation, values read only inside nested closures (Mock -MockWith, Select-Object calculated properties, ForEach-Object) that PSScriptAnalyzer doesn't trace, switches used only as parameter-set discriminators, and zero-side-effect factory/example functions flagged for ShouldProcess/SingularNouns based on verb/name alone. - Fix two real bugs found along the way: - AboutQueryingDatabases.Koans.ps1: `Out-File - FilePath ...` (stray space) was breaking positional parameter binding and erroring at runtime; corrected to `-FilePath`. - AddPesterAssertionOperator.ps1: replaced the alias `Add-AssertionOperator` with `Add-ShouldOperator`, and replaced the try/empty-catch registration guard with an explicit Get-ShouldOperator existence check. - Real code fixes in Public/Private module functions: correct [OutputType] declarations on Invoke-Koan and New-KoanRunspace, and wrap Set-PSKoanSetting's body in a process {} block so it actually honors its own already-declared ValueFromPipeline parameters. - Rename the automatic-variable-shadowing $_ scriptblock parameter to $Item in AboutLists.Koans.ps1 (both filters, for consistency); no suppression attribute is discoverable on a bare (non-function) scriptblock's own param block for this rule. - Trim trailing whitespace (42 lines, mechanical) across several koan files. No koan blanks, exercise structure, or pedagogical content were modified. `.\build.ps1 -Task Analyze` now completes with zero findings; full test suite unchanged at 729 passed / 6 pre-existing unrelated failures (Register-Advice, environment-specific $PROFILE binding) / 8 skipped. --- PSKoans/Init/AddPesterAssertionOperator.ps1 | 12 ++- PSKoans/Init/RegisterArgumentCompleters.ps1 | 27 ++++++ .../Cmdlets 1/AboutGroupObject.Koans.ps1 | 2 +- .../Cmdlets 1/AboutMeasureObject.Koans.ps1 | 4 +- .../Koans/Cmdlets 1/AboutModules.Koans.ps1 | 11 ++- .../Koans/Cmdlets 1/AboutNewObject.Koans.ps1 | 5 + .../Cmdlets 1/AboutPSProviders.Koans.ps1 | 9 +- .../Koans/Cmdlets 1/AboutSortObject.Koans.ps1 | 4 +- .../Koans/Cmdlets 2/AboutCsvCmdlets.Koans.ps1 | 11 +-- .../AboutBitwiseOperations.Koans.ps1 | 1 + .../AboutEnumerations.Koans.ps1 | 10 ++ .../AboutErrorHandling.Koans.ps1 | 2 +- .../AboutLists.Koans.ps1 | 11 ++- .../AboutPSObjects.Koans.ps1 | 2 +- .../AboutRedirection.Koans.ps1 | 6 +- .../AboutRegularExpressions.Koans.ps1 | 95 +++++++++++-------- .../AboutSplatting.Koans.ps1 | 2 +- .../AboutStringBuilder.Koans.ps1 | 2 +- .../AboutXml.Koans.ps1 | 4 +- .../Foundations/AboutConditionals.Koans.ps1 | 5 + .../AboutFunctionsAndScriptBlocks.Koans.ps1 | 5 + .../AboutOrderOfOperations.Koans.ps1 | 10 ++ .../Foundations/AboutVariables.Koans.ps1 | 5 + .../Introduction/AboutGetMember.Koans.ps1 | 14 +-- .../Koans/Introduction/AboutNumbers.Koans.ps1 | 4 +- .../Koans/Introduction/AboutStrings.Koans.ps1 | 10 ++ .../Koans/Katas/ProcessingStrings.Koans.ps1 | 9 +- .../Koans/Katas/SortingCharacters.Koans.ps1 | 7 +- .../Introduction/AboutFiltering.Koans.ps1 | 10 ++ .../dbatools/AboutBackupDatabases.Koans.ps1 | 4 +- .../dbatools/AboutNewDatabases.Koans.ps1 | 4 +- .../dbatools/AboutQueryingDatabases.Koans.ps1 | 2 +- PSKoans/Private/Invoke-Koan.ps1 | 2 +- PSKoans/Private/Measure-Koan.ps1 | 5 + PSKoans/Private/New-KoanRunspace.ps1 | 6 ++ PSKoans/Private/New-PSKoanErrorRecord.ps1 | 5 + PSKoans/Public/Get-Blank.ps1 | 15 +++ PSKoans/Public/Get-PSKoan.ps1 | 10 ++ PSKoans/Public/Set-PSKoanSetting.ps1 | 78 ++++++++------- PSKoans/Public/Show-Karma.ps1 | 15 +++ PSScriptAnalyzerSettings.psd1 | 21 ++++ psakeFile.ps1 | 15 +++ .../Private/Assert-UnblockedFile.Tests.ps1 | 2 +- .../Private/Get-KoanAttribute.Tests.ps1 | 2 +- tests/Functions/Private/Get-KoanIt.Tests.ps1 | 2 +- .../Private/New-KoanRunspace.Tests.ps1 | 4 - .../Private/Update-PSKoanFile.Tests.ps1 | 2 +- tests/Functions/Public/Get-Karma.Tests.ps1 | 6 +- .../Public/Get-PSKoanLocation.Tests.ps1 | 2 +- .../Public/Get-PSKoanSetting.Tests.ps1 | 4 +- .../Public/Move-PSKoanLibrary.Tests.ps1 | 2 +- tests/Functions/Public/Reset-PSKoan.Tests.ps1 | 2 +- .../Public/Set-PSKoanSetting.Tests.ps1 | 4 +- tests/Functions/Public/Show-Advice.Tests.ps1 | 8 +- tests/Functions/Public/Show-Karma.Tests.ps1 | 2 +- .../Functions/Public/Update-PSKoan.Tests.ps1 | 2 +- tests/KoanValidation.Tests.ps1 | 2 +- tests/ModuleValidation.Tests.ps1 | 2 +- 58 files changed, 381 insertions(+), 148 deletions(-) create mode 100644 PSScriptAnalyzerSettings.psd1 diff --git a/PSKoans/Init/AddPesterAssertionOperator.ps1 b/PSKoans/Init/AddPesterAssertionOperator.ps1 index 7e32ad56b..234f826cc 100644 --- a/PSKoans/Init/AddPesterAssertionOperator.ps1 +++ b/PSKoans/Init/AddPesterAssertionOperator.ps1 @@ -1,5 +1,12 @@ -try { - Add-AssertionOperator -Name Fail -Test { +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', + 'ActualValue', + Justification = 'Pester always passes the tested value as the first positional argument to a -Test scriptblock; this assertion ignores it because it always fails.' +)] +param() + +if ('Fail' -notin (Get-ShouldOperator).Name) { + Add-ShouldOperator -Name Fail -Test { param ($ActualValue, [switch] $Negate, [string] $Because) if ($Negate) { @@ -23,4 +30,3 @@ try { } } } -catch { } diff --git a/PSKoans/Init/RegisterArgumentCompleters.ps1 b/PSKoans/Init/RegisterArgumentCompleters.ps1 index be804e537..f2098798a 100644 --- a/PSKoans/Init/RegisterArgumentCompleters.ps1 +++ b/PSKoans/Init/RegisterArgumentCompleters.ps1 @@ -1,3 +1,30 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', + 'Command', + Justification = 'false positive' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', + 'Parameter', + Justification = 'false positive' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', + 'CommandAst', + Justification = 'false positive' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', + 'FakeBoundParams', + Justification = 'false positive' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', + 'WordToComplete', + Justification = 'used only inside a nested .Where{} scriptblock, which PSScriptAnalyzer does not trace back to the enclosing param' +)] +param() + $CommandName = @( 'Get-Karma' 'Get-PSKoan' diff --git a/PSKoans/Koans/Cmdlets 1/AboutGroupObject.Koans.ps1 b/PSKoans/Koans/Cmdlets 1/AboutGroupObject.Koans.ps1 index 86f634ec9..f7a8f63ef 100644 --- a/PSKoans/Koans/Cmdlets 1/AboutGroupObject.Koans.ps1 +++ b/PSKoans/Koans/Cmdlets 1/AboutGroupObject.Koans.ps1 @@ -11,7 +11,7 @@ param() #> Describe 'Group-Object' { BeforeAll { - $Files = Get-ChildItem -Path $home -Recurse -Depth 2 -File + $script:Files = Get-ChildItem -Path $home -Recurse -Depth 2 -File } It 'groups items based on specified properties' { diff --git a/PSKoans/Koans/Cmdlets 1/AboutMeasureObject.Koans.ps1 b/PSKoans/Koans/Cmdlets 1/AboutMeasureObject.Koans.ps1 index 26480e8c9..ccda5cf88 100644 --- a/PSKoans/Koans/Cmdlets 1/AboutMeasureObject.Koans.ps1 +++ b/PSKoans/Koans/Cmdlets 1/AboutMeasureObject.Koans.ps1 @@ -9,7 +9,7 @@ param() #> Describe 'Measure-Object' { BeforeAll { - $Numbers = @( + $script:Numbers = @( 839, 339, 763, 663, 238, 427, 577, 613, 284, 453 850, 130, 250, 843, 669, 972, 572, 41, 172, 155 729, 616, 285, 231, 128, 540, 204, 584, 407, 98 @@ -17,7 +17,7 @@ Describe 'Measure-Object' { 665, 154, 943, 35, 391, 816, 420, 229, 3, 938 ) - $Files = Get-ChildItem -Path $HOME -Recurse -Depth 2 + $script:Files = Get-ChildItem -Path $HOME -Recurse -Depth 2 } It 'can count objects' { diff --git a/PSKoans/Koans/Cmdlets 1/AboutModules.Koans.ps1 b/PSKoans/Koans/Cmdlets 1/AboutModules.Koans.ps1 index be94ba40b..65c14bd89 100644 --- a/PSKoans/Koans/Cmdlets 1/AboutModules.Koans.ps1 +++ b/PSKoans/Koans/Cmdlets 1/AboutModules.Koans.ps1 @@ -1,4 +1,9 @@ using module PSKoans +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSProvideCommentHelp', + '', + Justification = 'Test-ModuleFunction is throwaway scaffolding dynamically written to a demo module file for this exercise; formal help is not warranted.' +)] [Koan(Position = 212)] param() <# @@ -90,7 +95,7 @@ Describe 'Find-Module' { }, Description } - $Module = Find-Module -Name 'Pester' | Select-Object -First 1 + $script:Module = Find-Module -Name 'Pester' | Select-Object -First 1 } It 'finds modules that can be installed' { @@ -119,7 +124,7 @@ Describe 'New-Module' { a module in-memory without needing a file on disk. #> BeforeAll { - $Module = New-Module -Name 'PSKoans_TestModule' -ScriptBlock {} + $script:Module = New-Module -Name 'PSKoans_TestModule' -ScriptBlock {} } It 'creates a dynamic module object' { @@ -154,7 +159,7 @@ Describe 'Import-Module' { #> Context 'Importing Installed Modules' { BeforeAll { - $Module = New-Module -Name 'PSKoans_ImportModuleTest' { } + $script:Module = New-Module -Name 'PSKoans_ImportModuleTest' { } } It 'does not produce output' { diff --git a/PSKoans/Koans/Cmdlets 1/AboutNewObject.Koans.ps1 b/PSKoans/Koans/Cmdlets 1/AboutNewObject.Koans.ps1 index da678a26a..2fad7d7ef 100644 --- a/PSKoans/Koans/Cmdlets 1/AboutNewObject.Koans.ps1 +++ b/PSKoans/Koans/Cmdlets 1/AboutNewObject.Koans.ps1 @@ -1,4 +1,9 @@ using module PSKoans +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseLiteralInitializerForHashtable', + '', + Justification = 'Creating a Hashtable via New-Object (vs. the @{} literal) is the exact thing this koan demonstrates.' +)] [Koan(Position = 203)] param() <# diff --git a/PSKoans/Koans/Cmdlets 1/AboutPSProviders.Koans.ps1 b/PSKoans/Koans/Cmdlets 1/AboutPSProviders.Koans.ps1 index bdf46d8a2..1f2853cd5 100644 --- a/PSKoans/Koans/Cmdlets 1/AboutPSProviders.Koans.ps1 +++ b/PSKoans/Koans/Cmdlets 1/AboutPSProviders.Koans.ps1 @@ -1,4 +1,9 @@ using module PSKoans +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', + 'File', + Justification = 'Setup value for the blank the learner fills in below with $File; unused until solved.' +)] [Koan(Position = 202)] param() <# @@ -32,7 +37,7 @@ Describe 'Alias Provider' { #> Context 'Direct Access' { BeforeAll { - $Aliases = Get-ChildItem -Path 'Alias:' + $script:Aliases = Get-ChildItem -Path 'Alias:' } It 'can be queried with generic provider cmdlets' { @@ -181,7 +186,7 @@ especially between Windows, Mac, and Linux, for example. Describe 'Function Provider' { BeforeAll { - $Functions = Get-ChildItem -Path 'Function:' + $script:Functions = Get-ChildItem -Path 'Function:' } It 'allows access to all currently loaded functions' { diff --git a/PSKoans/Koans/Cmdlets 1/AboutSortObject.Koans.ps1 b/PSKoans/Koans/Cmdlets 1/AboutSortObject.Koans.ps1 index 25e7bbe88..265bb72f8 100644 --- a/PSKoans/Koans/Cmdlets 1/AboutSortObject.Koans.ps1 +++ b/PSKoans/Koans/Cmdlets 1/AboutSortObject.Koans.ps1 @@ -15,8 +15,8 @@ param() #> Describe 'Sort-Object' { BeforeAll { - $Numbers = 5, 2, 7, 1, 4, 6, 8, 3, 10, 9 - $Strings = 'hello', 'goodbye', 'who', 'Steve', 'PowerShell' + $script:Numbers = 5, 2, 7, 1, 4, 6, 8, 3, 10, 9 + $script:Strings = 'hello', 'goodbye', 'who', 'Steve', 'PowerShell' } It 'sorts a collection of objects' { diff --git a/PSKoans/Koans/Cmdlets 2/AboutCsvCmdlets.Koans.ps1 b/PSKoans/Koans/Cmdlets 2/AboutCsvCmdlets.Koans.ps1 index 2d5309b9a..77bc865d1 100644 --- a/PSKoans/Koans/Cmdlets 2/AboutCsvCmdlets.Koans.ps1 +++ b/PSKoans/Koans/Cmdlets 2/AboutCsvCmdlets.Koans.ps1 @@ -34,7 +34,7 @@ Describe 'CSV Cmdlets' { to match. #> BeforeAll { - $Objects = foreach ($number in 1..5) { + $script:Objects = foreach ($number in 1..5) { [PSCustomObject]@{ Number = $number Square = $number * $number @@ -105,14 +105,14 @@ Describe 'CSV Cmdlets' { All data will be imported as an array of PSCustomObjects with string properties. #> BeforeAll { - $Objects = foreach ($number in 1..5) { + $script:Objects = foreach ($number in 1..5) { [bigint]$number } $CsvPath = "$TestDrive/Data.csv" $Objects | Export-Csv -Path $CsvPath -NoTypeInformation - $ImportedData = Import-Csv -Path $CsvPath + $script:ImportedData = Import-Csv -Path $CsvPath } It 'imports the stored data as PSCustomObjects' { @@ -196,7 +196,7 @@ Describe 'CSV Cmdlets' { it will output the lines of the CSV as strings instead of storing the data to a file. #> BeforeAll { - $Objects = foreach ($number in 1..5) { + $script:Objects = foreach ($number in 1..5) { [PSCustomObject]@{ Number = $number Square = $number * $number @@ -224,7 +224,7 @@ Describe 'CSV Cmdlets' { Context 'ConvertFrom-Csv' { BeforeAll { - $CsvString = @" + $script:CsvString = @" "Number","Square" "1","1" "2","4" @@ -235,7 +235,6 @@ Describe 'CSV Cmdlets' { } It 'operates on string data rather than directly with files' { - $Objects = $CsvString | ConvertFrom-Csv $CsvString -is [____] | Should -BeTrue } diff --git a/PSKoans/Koans/Constructs and Patterns/AboutBitwiseOperations.Koans.ps1 b/PSKoans/Koans/Constructs and Patterns/AboutBitwiseOperations.Koans.ps1 index 4a5c9b102..2027988ea 100644 --- a/PSKoans/Koans/Constructs and Patterns/AboutBitwiseOperations.Koans.ps1 +++ b/PSKoans/Koans/Constructs and Patterns/AboutBitwiseOperations.Koans.ps1 @@ -37,6 +37,7 @@ Describe 'Binary Operators' { #> function ConvertFrom-Binary { [CmdletBinding()] + [OutputType([string], [byte], [sbyte], [int16], [int32], [int64])] param ( # The binary string to convert. [Parameter(Mandatory, Position = 0, ValueFromPipeline)] diff --git a/PSKoans/Koans/Constructs and Patterns/AboutEnumerations.Koans.ps1 b/PSKoans/Koans/Constructs and Patterns/AboutEnumerations.Koans.ps1 index 662155b59..fd8a470fa 100644 --- a/PSKoans/Koans/Constructs and Patterns/AboutEnumerations.Koans.ps1 +++ b/PSKoans/Koans/Constructs and Patterns/AboutEnumerations.Koans.ps1 @@ -1,4 +1,14 @@ using module PSKoans +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', + '', + Justification = 'New-Enumeration is a minimal in-exercise example defining an enum in a nested scope; it has no real side effects to guard with ShouldProcess.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseUsingScopeModifierInNewRunspaces', + '', + Justification = '$Type is Get-Objects own function parameter declared inside the Start-Job scriptblock, not a captured outer-scope variable; $using: does not apply.' +)] [Koan(Position = 310)] param() <# diff --git a/PSKoans/Koans/Constructs and Patterns/AboutErrorHandling.Koans.ps1 b/PSKoans/Koans/Constructs and Patterns/AboutErrorHandling.Koans.ps1 index 7d98a074b..1bda160df 100644 --- a/PSKoans/Koans/Constructs and Patterns/AboutErrorHandling.Koans.ps1 +++ b/PSKoans/Koans/Constructs and Patterns/AboutErrorHandling.Koans.ps1 @@ -48,7 +48,7 @@ Describe 'ErrorRecord' { } Context 'Error Assignments' { BeforeAll { - $ErrorRecord = try { + $script:ErrorRecord = try { throw "A challenge to the sky!" } catch { diff --git a/PSKoans/Koans/Constructs and Patterns/AboutLists.Koans.ps1 b/PSKoans/Koans/Constructs and Patterns/AboutLists.Koans.ps1 index b70f7e5f3..3ad4d16f4 100644 --- a/PSKoans/Koans/Constructs and Patterns/AboutLists.Koans.ps1 +++ b/PSKoans/Koans/Constructs and Patterns/AboutLists.Koans.ps1 @@ -1,4 +1,9 @@ using module PSKoans +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', + 'Item', + Justification = 'Second $Filter is a blank scriptblock body the learner completes; $Item is unused until solved.' +)] [Koan(Position = 301)] param() <# @@ -125,10 +130,10 @@ Describe 'Lists' { #> $Filter = { # The input variable representing each entry must be named, or use $args[0]. - param($_) + param($Item) # Remove everything that contains the number 9, essentially. - $_ -match '9' + $Item -match '9' # The output must boil down to a $true/$false, or will be coerced to it. } $List.RemoveAll($Filter) | Should -BeTrue @@ -137,7 +142,7 @@ Describe 'Lists' { $List | Should -Be $RemainingEntries $Filter = { - param($_) + param($Item) # Fill in this script block to make the below assertions true! } diff --git a/PSKoans/Koans/Constructs and Patterns/AboutPSObjects.Koans.ps1 b/PSKoans/Koans/Constructs and Patterns/AboutPSObjects.Koans.ps1 index 31cde1623..a8d0f228e 100644 --- a/PSKoans/Koans/Constructs and Patterns/AboutPSObjects.Koans.ps1 +++ b/PSKoans/Koans/Constructs and Patterns/AboutPSObjects.Koans.ps1 @@ -19,7 +19,7 @@ param() Describe 'PSObject' { BeforeAll { - $Object = @('Stand for nothing; fall for everything.') + $script:Object = @('Stand for nothing; fall for everything.') } It "is accessed via a hidden .PSObject property" { diff --git a/PSKoans/Koans/Constructs and Patterns/AboutRedirection.Koans.ps1 b/PSKoans/Koans/Constructs and Patterns/AboutRedirection.Koans.ps1 index 5c8f56502..4981af667 100644 --- a/PSKoans/Koans/Constructs and Patterns/AboutRedirection.Koans.ps1 +++ b/PSKoans/Koans/Constructs and Patterns/AboutRedirection.Koans.ps1 @@ -36,9 +36,9 @@ param() #> Describe 'Redirection Operators' { BeforeAll { - $OriginalDebugPreference = $DebugPreference - $OriginalVerbosePreference = $VerbosePreference - $FilePath = 'TestDrive:\TestFile.txt' + $script:OriginalDebugPreference = $DebugPreference + $script:OriginalVerbosePreference = $VerbosePreference + $script:FilePath = 'TestDrive:\TestFile.txt' $DebugPreference = $VerbosePreference = 'Continue' } diff --git a/PSKoans/Koans/Constructs and Patterns/AboutRegularExpressions.Koans.ps1 b/PSKoans/Koans/Constructs and Patterns/AboutRegularExpressions.Koans.ps1 index b9c4a7ce2..127a20195 100644 --- a/PSKoans/Koans/Constructs and Patterns/AboutRegularExpressions.Koans.ps1 +++ b/PSKoans/Koans/Constructs and Patterns/AboutRegularExpressions.Koans.ps1 @@ -1,4 +1,19 @@ using module PSKoans +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', + 'replaceWith', + Justification = 'Setup value for the blank the learner fills in below with $replaceWith; unused until solved.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', + 'greedy', + Justification = 'Setup value for the blank the learner fills in below with $greedy; unused until solved.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', + 'lazy', + Justification = 'Setup value for the blank the learner fills in below with $lazy; unused until solved.' +)] [Koan(Position = 320)] param() @@ -17,7 +32,7 @@ Describe 'Working With Regular Expressions' { Context 'Using the -match Operator' { It 'returns $true if a match is found' { - <# + <# In PowerShell, the -match operator returns a boolean result based on whether the pattern (regex) on the right side is found within the string on the left side. Yes, in this case 'string' is a regular expression! @@ -25,12 +40,12 @@ Describe 'Working With Regular Expressions' { Regex is case sensitive, but -match, along with many other PowerShell operators, perform case-insensitive matching. #> - + 'a string value' -match 'string' | Should -BeTrue - + $trueValue = '____' $trueValue -match 'climb' | Should -BeTrue - + } It 'returns $false if a match is not found' { @@ -38,7 +53,7 @@ Describe 'Working With Regular Expressions' { $falseValue -match 'climb' | Should -BeFalse } } - + Context 'Using the -replace Operator' { It 'manipulates strings' { @@ -48,7 +63,7 @@ Describe 'Working With Regular Expressions' { the string for, and the second is the string to replace the pattern with if it is found in the string. #> - + $replacePattern = 'simple string' $replaceWith = 'string that got something replaced' $newString = 'Here is a simple string.' -replace $replacePattern, $____ @@ -61,7 +76,7 @@ Describe 'Working With Regular Expressions' { $newString = 'I love that regex is simple' -replace $replacePattern, $replaceWith $newString | Should -Be 'I love that regex is flexible' } - + It 'does not need a second argument' { # If you leave out a "replace with" argument, -replace still works $replacePattern = ' extra bits' @@ -99,7 +114,7 @@ Describe 'Working With Regular Expressions' { The Matches() method takes two arguments. In order, they are the string that is being examined, followed by the regular expression being searched for. #> - + $regexMatch = [Regex]::Matches('Running through a forest', 'through') '____' | Should -Be $regexMatch.GetType().FullName '____' | Should -Be $regexMatch.Value @@ -116,7 +131,7 @@ Describe 'Working With Regular Expressions' { $optionsMatch = [regex]::Matches($overloadString, 'case', $ignoreCase) __ | Should -Be $optionsMatch.Count } - + It 'can split up strings' { <# The [regex] class has a whole lot more in it than just the ::Matches() method. It @@ -153,12 +168,12 @@ Describe 'Quantifiers' { be a single character or a group of characters) that occurs just before the quantifier itself may be repeated or omitted entirely in the target string, depending on the quantifier used. #> - + Context '*' { BeforeEach { - $firstTest = ('pears' -match 'p*ears') - $secondTest = ('shears' -match 'p*ears') + $script:firstTest = ('pears' -match 'p*ears') + $script:secondTest = ('shears' -match 'p*ears') } It 'specifies 0 or more of something' { @@ -174,19 +189,19 @@ Describe 'Quantifiers' { It 'does not need to match the entire string' { <# - What happened in that last example? What part of 'shears' matched the pattern + What happened in that last example? What part of 'shears' matched the pattern 'p*ears'? When you use the -match operator, you can see some interesting information in the automatic variable $matches. $matches is a collection of the match objects that are created when you use the -match operator. This isn't as flexible nor as robust as using [regex]::Matches(), but it's handy in a pinch. - + In this case, you'll see that the "sh" in "shears" weren't part of the match. The pattern "zero or more p's, followed by e, a, r, s," is found in the string, even though there are parts of the string that aren't relevant to that matching effort. - + $matches[0] is always your most recent match. #> - + '____' | Should -Be $matches[0] } @@ -203,7 +218,7 @@ Describe 'Quantifiers' { The + symbol is a lot like the *, but instead of matching zero or more, it matches one or more. #> - + '____' -match 'p+ickles' | Should -BeTrue } @@ -221,7 +236,7 @@ Describe 'Quantifiers' { # The ? matches "zero or one" of something. $____ | Should -Be ('flying through the sky' -match 'f?lying') } - + It 'does not mind matching 0 of something' { $____ | Should -Be ('floating away' -match 'b?oat') } @@ -262,20 +277,20 @@ Describe 'Special Symbols' { In regex, there are far more symbols with unique meanings than just quantifiers. What comes next is not a definitive guide to EVERY SINGLE ONE, but rather an introduction to some of the most common and useful special symbols in regex. - + This is ^\where things st\Art getting \weir\D$ #> - + Context '. (period)' { It 'matches any character' { # The . (period) matches literally any character - + $____ | Should -Be ('Lazy Sunday mornings' -match 'S.nday') } - It 'starts looking kind of weird now' { + It 'starts looking kind of weird now' { '____' -match 'invi.a.ion' | Should -BeTrue } } @@ -289,12 +304,12 @@ Describe 'Special Symbols' { "literally the letter n". When it's preceded by a backslash, however, that "n" takes on a special meaning. In this case, \n matches new lines. Actually, it matches the encoded character that indicates a new line. - + There are many more regex symbols that are single letters preceded by a backslash. In fact, the backslash is probably the single most important character in all of regex. #> - + $multiLine = @" They might look similar and look @@ -349,7 +364,7 @@ a domestic cat Context '^ (caret) and $ (dollar sign)' { - It 'matches the start of lines with ^' { + It 'matches the start of lines with ^' { $caretMatch = '____' $caretMatch -match '^a' | Should -BeTrue } @@ -367,7 +382,7 @@ a domestic cat \s matches whitespace characters (space, tab, etc.) while \S matches anything other than whitespace #> - + # Enter either \s or \S - which do you think will work? 'Room to grow' -match '__' | Should -BeTrue } @@ -391,11 +406,11 @@ a domestic cat can also take special meaning away from characters, even itself. The process of removing the special meaning from a letter in regex is called "escaping". #> - + # Escape the period character to match a literal period instead of "any character" 'This . character means something else in regex' -match '\.' | Should -BeTrue $matches[0] | Should -Be '____' - + $dollarValue = 'The price is $4.99.' $dollarMatch = '____' [regex]::Matches($dollarValue, $dollarMatch).Value | Should -Be '$4.99' @@ -415,7 +430,7 @@ Describe 'Brackets and Braces' { types of brackets and braces all mean different things. Sometimes other regex symbols meanings change when they're found inside some brackets or braces. #> - + Context '{ and } - Curly Braces' { It 'works like a custom quantifier' { @@ -426,7 +441,7 @@ Describe 'Brackets and Braces' { more". Those cover tons of use cases and examples, but there are plenty more situations where you want to be more specific. #> - + <# The portion of this regex in curly braces acts as a quantifier for the symbol that comes immediately before it. It means "exactly 4 digits." @@ -469,7 +484,7 @@ Describe 'Brackets and Braces' { therefore be able to reproduce the function of the standard *, +, and ? quantifiers using curly braces. #> - + $grass = 'Teeny blades of grass' $plusGrassPattern = 'e+' @@ -492,11 +507,11 @@ Describe 'Brackets and Braces' { other ways. This is just the tip of the round bracket/regular expression groups iceberg. #> - + '____' | Should -Be ([regex]::Matches('Bears Beat Bongos', '(B.+){3}').Value) } - It 'does not find just one badger' { + It 'does not find just one badger' { $badgers = 'BadgerBadgerBadger' $groupingPattern = '(____){__}' [regex]::Matches($badgers, $groupingPattern).Value | Should -Be 'BadgerBadgerBadger' @@ -515,11 +530,11 @@ Describe 'Brackets and Braces' { The fancy regex name for this is a "character class". #> - + $____ | Should -Be ('End of the line' -match '[efg]$') } - It 'wants you to end the string a certain way' { + It 'wants you to end the string a certain way' { '____' -match '[abcdefg]$' | Should -BeTrue } @@ -531,7 +546,7 @@ Describe 'Brackets and Braces' { '____' -match '[^abcdefg]$' | Should -BeTrue } - It 'still needs to match a ^ inside a set' { + It 'still needs to match a ^ inside a set' { <# What do you think you might do if you want to match a literal ^ symbol within a set? Use the ^ (caret) symbol in this one. @@ -545,7 +560,7 @@ Describe 'Brackets and Braces' { Describe 'Meditative Examples' { # Here are some challenges for you to put your new skills to work on. - + Context 'Isolate a Username From a domain\username String Using Just One Regex Pattern' { It 'is handy for isolating parts of strings' { @@ -557,13 +572,13 @@ Describe 'Meditative Examples' { } Context 'Validate a Bunch of Phone Numbers' { - + <# Validate a bunch of phone numbers - get rid of the non-numeric characters and check which ones are actually the right length to be dialed. #> BeforeAll { - $phoneNumbers = @( + $script:phoneNumbers = @( '1 425 555 1234' '1-425-555-4321' '1.425.555.6789' @@ -571,7 +586,7 @@ Describe 'Meditative Examples' { '+14255556789' ) - $sanitizedNumbers = @( + $script:sanitizedNumbers = @( '14255551234' '14255554321' '14255556789' diff --git a/PSKoans/Koans/Constructs and Patterns/AboutSplatting.Koans.ps1 b/PSKoans/Koans/Constructs and Patterns/AboutSplatting.Koans.ps1 index cf4a500b8..467710d34 100644 --- a/PSKoans/Koans/Constructs and Patterns/AboutSplatting.Koans.ps1 +++ b/PSKoans/Koans/Constructs and Patterns/AboutSplatting.Koans.ps1 @@ -17,7 +17,7 @@ param() Describe 'Splatting' { BeforeAll { - $PSKoansFolder = Get-PSKoanLocation + $script:PSKoansFolder = Get-PSKoanLocation } Context 'Hashtable Splatting' { diff --git a/PSKoans/Koans/Constructs and Patterns/AboutStringBuilder.Koans.ps1 b/PSKoans/Koans/Constructs and Patterns/AboutStringBuilder.Koans.ps1 index 48eb522af..4b2652988 100644 --- a/PSKoans/Koans/Constructs and Patterns/AboutStringBuilder.Koans.ps1 +++ b/PSKoans/Koans/Constructs and Patterns/AboutStringBuilder.Koans.ps1 @@ -118,7 +118,7 @@ Describe 'System.Text.StringBuilder' { Context 'Other StringBuilder Methods' { BeforeAll { - $StringBuilder = [System.Text.StringBuilder]::new("TEXT") + $script:StringBuilder = [System.Text.StringBuilder]::new("TEXT") } It 'can be cleared' { diff --git a/PSKoans/Koans/Constructs and Patterns/AboutXml.Koans.ps1 b/PSKoans/Koans/Constructs and Patterns/AboutXml.Koans.ps1 index 18cbe6f37..096092be4 100644 --- a/PSKoans/Koans/Constructs and Patterns/AboutXml.Koans.ps1 +++ b/PSKoans/Koans/Constructs and Patterns/AboutXml.Koans.ps1 @@ -12,9 +12,9 @@ param() #> Describe 'About XML' { BeforeAll { - $path = Join-Path $TestDrive -ChildPath document.xml + $script:path = Join-Path $TestDrive -ChildPath document.xml } - + Context 'XML as an object' { It 'can cast a string to an XML document' { diff --git a/PSKoans/Koans/Foundations/AboutConditionals.Koans.ps1 b/PSKoans/Koans/Foundations/AboutConditionals.Koans.ps1 index c62a87c71..2d9ad5180 100644 --- a/PSKoans/Koans/Foundations/AboutConditionals.Koans.ps1 +++ b/PSKoans/Koans/Foundations/AboutConditionals.Koans.ps1 @@ -1,4 +1,9 @@ using module PSKoans +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', + '', + Justification = 'Set-Number is a minimal in-exercise example returning a computed value; it has no real side effects to guard with ShouldProcess.' +)] [Koan(Position = 116)] param() <# diff --git a/PSKoans/Koans/Foundations/AboutFunctionsAndScriptBlocks.Koans.ps1 b/PSKoans/Koans/Foundations/AboutFunctionsAndScriptBlocks.Koans.ps1 index 6e3496b3c..daed46e32 100644 --- a/PSKoans/Koans/Foundations/AboutFunctionsAndScriptBlocks.Koans.ps1 +++ b/PSKoans/Koans/Foundations/AboutFunctionsAndScriptBlocks.Koans.ps1 @@ -1,4 +1,9 @@ using module PSKoans +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseSingularNouns', + '', + Justification = 'Add-Things/Add-Numbers are minimal in-exercise example functions; naming is incidental to the parameter-passing lesson.' +)] [Koan(Position = 117)] param() <# diff --git a/PSKoans/Koans/Foundations/AboutOrderOfOperations.Koans.ps1 b/PSKoans/Koans/Foundations/AboutOrderOfOperations.Koans.ps1 index 0e39354b2..12f8833d8 100644 --- a/PSKoans/Koans/Foundations/AboutOrderOfOperations.Koans.ps1 +++ b/PSKoans/Koans/Foundations/AboutOrderOfOperations.Koans.ps1 @@ -1,4 +1,14 @@ using module PSKoans +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseSingularNouns', + '', + Justification = 'Add-Numbers is a minimal in-exercise example function; naming is incidental to the order-of-operations lesson.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidUsingPositionalParameters', + '', + Justification = 'Positional argument passing is the exact subject of this lesson, and the __ blank is invoked as a bare positional command by design.' +)] [Koan(Position = 118)] param() <# diff --git a/PSKoans/Koans/Foundations/AboutVariables.Koans.ps1 b/PSKoans/Koans/Foundations/AboutVariables.Koans.ps1 index 01360511d..8fbfa2ac5 100644 --- a/PSKoans/Koans/Foundations/AboutVariables.Koans.ps1 +++ b/PSKoans/Koans/Foundations/AboutVariables.Koans.ps1 @@ -1,4 +1,9 @@ using module PSKoans +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', + '____', + Justification = 'The variable name IS the blank; the learner replaces it with the real constant variable name and it becomes used.' +)] [Koan(Position = 111)] param() <# diff --git a/PSKoans/Koans/Introduction/AboutGetMember.Koans.ps1 b/PSKoans/Koans/Introduction/AboutGetMember.Koans.ps1 index 78ad336dd..75b5bdf91 100644 --- a/PSKoans/Koans/Introduction/AboutGetMember.Koans.ps1 +++ b/PSKoans/Koans/Introduction/AboutGetMember.Koans.ps1 @@ -66,9 +66,9 @@ Describe "Get Member" { Get-Command -Verb Get #> BeforeAll { - $Cmdlets = [System.Collections.Generic.HashSet[string]]::new() - $PropertyString = "property '{0}' should be present in output from {1}" - $UniqueString = 'unique cmdlets should be used for each test' + $script:Cmdlets = [System.Collections.Generic.HashSet[string]]::new() + $script:PropertyString = "property '{0}' should be present in output from {1}" + $script:UniqueString = 'unique cmdlets should be used for each test' } It 'lists one of the properties of the first unique command' { @@ -150,7 +150,7 @@ Describe "Get Member" { programming languages treat functions and methods. If for example you want to know if a string ends with a certain character, it is necessary that the method knows which character you want to compare the string against. - + Similar to above, you can inspect the methods available from an object that a cmdlet outputs, by changing the -MemberType value you provide to Get-Member: @@ -165,9 +165,9 @@ Describe "Get Member" { the objects they output! #> BeforeAll { - $Cmdlets = [System.Collections.Generic.HashSet[string]]::new() - $MethodString = "property '{0}' should be present in output from {1}" - $UniqueString = 'unique cmdlets should be used for each test' + $script:Cmdlets = [System.Collections.Generic.HashSet[string]]::new() + $script:MethodString = "property '{0}' should be present in output from {1}" + $script:UniqueString = 'unique cmdlets should be used for each test' } It 'lists one of the methods of the first unique command' { diff --git a/PSKoans/Koans/Introduction/AboutNumbers.Koans.ps1 b/PSKoans/Koans/Introduction/AboutNumbers.Koans.ps1 index e00184d79..b3c61f0a6 100644 --- a/PSKoans/Koans/Introduction/AboutNumbers.Koans.ps1 +++ b/PSKoans/Koans/Introduction/AboutNumbers.Koans.ps1 @@ -27,8 +27,8 @@ Describe 'Basic Number Types' { Context 'Double' { BeforeAll { - $Int = 10 - $Double = 10.0 + $script:Int = 10 + $script:Double = 10.0 } It 'has a specific object type for integers' { diff --git a/PSKoans/Koans/Introduction/AboutStrings.Koans.ps1 b/PSKoans/Koans/Introduction/AboutStrings.Koans.ps1 index 188eb59c8..148c08ed8 100644 --- a/PSKoans/Koans/Introduction/AboutStrings.Koans.ps1 +++ b/PSKoans/Koans/Introduction/AboutStrings.Koans.ps1 @@ -1,4 +1,14 @@ using module PSKoans +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', + 'Number', + Justification = 'Demonstrates that "$Number:text" does not expand $Number (colon starts a drive/scope qualifier); intentionally unused by the koan.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', + 'String2', + Justification = 'Setup value for the blank the learner fills in below with $String2; unused until solved.' +)] [Koan(Position = 104)] param() <# diff --git a/PSKoans/Koans/Katas/ProcessingStrings.Koans.ps1 b/PSKoans/Koans/Katas/ProcessingStrings.Koans.ps1 index 732c2b469..f39d35eb3 100644 --- a/PSKoans/Koans/Katas/ProcessingStrings.Koans.ps1 +++ b/PSKoans/Koans/Katas/ProcessingStrings.Koans.ps1 @@ -1,6 +1,11 @@ using module PSKoans using namespace System.Management.Automation.Language using namespace System.Collections.Generic +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', + 'Data', + Justification = 'Get-GreatestVarianceDate is a solution stub the learner implements; $Data is unused until solved.' +)] [Koan(Position = 150)] param() <# @@ -28,7 +33,7 @@ param() Describe "The Stock Challenge" { BeforeAll { - $StockData = @( + $script:StockData = @( "Date,Open,High,Low,Close,Volume,Adj Close" "2012-03-30,32.40,32.41,32.04,32.26,31749400,32.26" "2012-03-29,32.06,32.19,31.81,32.12,37038500,32.12" @@ -61,7 +66,7 @@ Describe "The Stock Challenge" { the finish line! #> - $Verification = { + $script:Verification = { $Functions = [Hashset[string]]::new([StringComparer]::OrdinalIgnoreCase) $Ast = (Get-Command 'Get-GreatestVarianceDate' -CommandType Function).ScriptBlock.Ast $Ast.FindAll( diff --git a/PSKoans/Koans/Katas/SortingCharacters.Koans.ps1 b/PSKoans/Koans/Katas/SortingCharacters.Koans.ps1 index 08d4bba63..128bd193b 100644 --- a/PSKoans/Koans/Katas/SortingCharacters.Koans.ps1 +++ b/PSKoans/Koans/Katas/SortingCharacters.Koans.ps1 @@ -1,6 +1,11 @@ using module PSKoans using namespace System.Collections.Generic using namespace System.Management.Automation.Language +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', + 'String', + Justification = 'Get-SortedString is a solution stub the learner implements; $String is unused until solved.' +)] [Koan(Position = 151)] param() @@ -18,7 +23,7 @@ param() Describe 'Kata - Sorting Characters' { BeforeAll { - $Verification = { + $script:Verification = { $Functions = [Hashset[string]]::new([StringComparer]::OrdinalIgnoreCase) $Ast = (Get-Command 'Get-SortedString' -CommandType Function).ScriptBlock.Ast $Ast.FindAll( diff --git a/PSKoans/Koans/Modules/ActiveDirectory/Introduction/AboutFiltering.Koans.ps1 b/PSKoans/Koans/Modules/ActiveDirectory/Introduction/AboutFiltering.Koans.ps1 index 8894aecc3..03451a374 100644 --- a/PSKoans/Koans/Modules/ActiveDirectory/Introduction/AboutFiltering.Koans.ps1 +++ b/PSKoans/Koans/Modules/ActiveDirectory/Introduction/AboutFiltering.Koans.ps1 @@ -1,5 +1,15 @@ #Requires -Modules ActiveDirectory using module PSKoans +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', + 'sid', + Justification = 'Consumed by the ActiveDirectory -Filter string, which the AD provider expands by variable name at runtime rather than PowerShell interpolating it.' +)] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', + 'homeDirectory', + Justification = 'Consumed by the ActiveDirectory -Filter string, which the AD provider expands by variable name at runtime rather than PowerShell interpolating it.' +)] [Koan(Position = 101, Module = 'ActiveDirectory')] param() <# diff --git a/PSKoans/Koans/Modules/dbatools/AboutBackupDatabases.Koans.ps1 b/PSKoans/Koans/Modules/dbatools/AboutBackupDatabases.Koans.ps1 index 8baa120ce..0f5e86e1b 100644 --- a/PSKoans/Koans/Modules/dbatools/AboutBackupDatabases.Koans.ps1 +++ b/PSKoans/Koans/Modules/dbatools/AboutBackupDatabases.Koans.ps1 @@ -13,7 +13,7 @@ param() with the Backup-DbaDatabase command. #> Describe "Backup-DbaDatabase" { - + #region Mocks <# Let's setup the environment for you. Unless you want the Koans to nearly always fail, I would @@ -164,7 +164,7 @@ Describe "Backup-DbaDatabase" { It 'takes backups and substitutes keywords such as database name and database backup type' { <# There are different conventions for specifying where the backup file will go to - and what the backup file will be called. There are different keywords that + and what the backup file will be called. There are different keywords that dbatools provides that can be used with the -FilePath and -ReplaceInName. instancename - will be replaced with the instance name servername - will be replaced with the server name diff --git a/PSKoans/Koans/Modules/dbatools/AboutNewDatabases.Koans.ps1 b/PSKoans/Koans/Modules/dbatools/AboutNewDatabases.Koans.ps1 index f243a3ff4..d5202cdb1 100644 --- a/PSKoans/Koans/Modules/dbatools/AboutNewDatabases.Koans.ps1 +++ b/PSKoans/Koans/Modules/dbatools/AboutNewDatabases.Koans.ps1 @@ -14,7 +14,7 @@ Describe 'New-DbaDatabase' { #region Mocks <# Let's set up the environment for you. - + Unless you want the Koans to nearly always fail, I would suggest not messing with this bit. #> BeforeAll { @@ -137,7 +137,7 @@ Describe 'New-DbaDatabase' { $_.SqlInstance -eq 'localhost' -and $_.RecoveryModel -eq 'Full' } - } + } #endregion It 'creates a new database on the specified instance' { diff --git a/PSKoans/Koans/Modules/dbatools/AboutQueryingDatabases.Koans.ps1 b/PSKoans/Koans/Modules/dbatools/AboutQueryingDatabases.Koans.ps1 index b075b0c64..bae15abf4 100644 --- a/PSKoans/Koans/Modules/dbatools/AboutQueryingDatabases.Koans.ps1 +++ b/PSKoans/Koans/Modules/dbatools/AboutQueryingDatabases.Koans.ps1 @@ -75,7 +75,7 @@ Describe "Invoke-DbaQuery" { We've created a file called SimpleTSQL.sql that contains a T-SQL statement, which we want to run against the tempdb database on the localhost instance. #> - Out-File - FilePath TestDrive:\SimpleTSQL.sql -InputObject "SELECT 'From a File' AS Origin;" + Out-File -FilePath TestDrive:\SimpleTSQL.sql -InputObject "SELECT 'From a File' AS Origin;" $InvokeDbaQueryParams = @{ SqlInstance = 'localhost' Database = 'tempdb' diff --git a/PSKoans/Private/Invoke-Koan.ps1 b/PSKoans/Private/Invoke-Koan.ps1 index 560c0b5c8..a6bd80902 100644 --- a/PSKoans/Private/Invoke-Koan.ps1 +++ b/PSKoans/Private/Invoke-Koan.ps1 @@ -18,7 +18,7 @@ #> [CmdletBinding()] - [OutputType([PSObject])] + [OutputType([System.Management.Automation.PSDataCollection[psobject]])] param( [Parameter(Position = 0, Mandatory)] [Alias('Params')] diff --git a/PSKoans/Private/Measure-Koan.ps1 b/PSKoans/Private/Measure-Koan.ps1 index 3fbc7169b..d67726312 100644 --- a/PSKoans/Private/Measure-Koan.ps1 +++ b/PSKoans/Private/Measure-Koan.ps1 @@ -21,6 +21,11 @@ .LINK https://github.com/vexx32/PSKoans #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', + 'invokedViaInvokePester', + Justification = 'Read by Pester internals via $ExecutionContext.SessionState.PSVariable.Get(''invokedViaInvokePester''), a dynamic-scope convention invisible to static analysis.' + )] [CmdletBinding()] [OutputType([int])] param( diff --git a/PSKoans/Private/New-KoanRunspace.ps1 b/PSKoans/Private/New-KoanRunspace.ps1 index d564fd449..c344efe5f 100644 --- a/PSKoans/Private/New-KoanRunspace.ps1 +++ b/PSKoans/Private/New-KoanRunspace.ps1 @@ -16,7 +16,13 @@ function New-KoanRunspace { .NOTES Run scripts in a new scope to avoid scope bleed wherever possible, with the `$ps.AddScript($script, $true) overload. #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', + '', + Justification = 'Constructs and returns an in-memory Runspace object; no external state is mutated.' + )] [CmdletBinding()] + [OutputType([System.Management.Automation.Runspaces.Runspace])] param() $runspace = [runspacefactory]::CreateRunspace() diff --git a/PSKoans/Private/New-PSKoanErrorRecord.ps1 b/PSKoans/Private/New-PSKoanErrorRecord.ps1 index e2b7fd9a8..6f083f04f 100644 --- a/PSKoans/Private/New-PSKoanErrorRecord.ps1 +++ b/PSKoans/Private/New-PSKoanErrorRecord.ps1 @@ -39,6 +39,11 @@ function New-PSKoanErrorRecord { Constructs an error record out of disparate parts and throws it to terminate the current command. #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', + '', + Justification = 'Constructs and returns an in-memory ErrorRecord object; no external state is mutated.' + )] [CmdletBinding(DefaultParameterSetName = 'Default')] [OutputType([System.Management.Automation.ErrorRecord])] param( diff --git a/PSKoans/Public/Get-Blank.ps1 b/PSKoans/Public/Get-Blank.ps1 index 7dfba8e9f..744fb8992 100644 --- a/PSKoans/Public/Get-Blank.ps1 +++ b/PSKoans/Public/Get-Blank.ps1 @@ -38,6 +38,21 @@ .LINK https://github.com/vexx32/PSKoans/tree/main/docs/PSKoans.md #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', + '|PipeInput', + Justification = 'Exists only to silently absorb accidental pipeline input without erroring; the value is never meant to be read.' + )] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', + '|ParameterInput', + Justification = 'Exists only to silently absorb accidental extra arguments without erroring; the value is never meant to be read.' + )] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseProcessBlockForPipelineCommand', + '', + Justification = 'Piped input is intentionally discarded, not processed per-item; a process block would add no behavior.' + )] [CmdletBinding(HelpUri = 'https://github.com/vexx32/PSKoans/tree/main/docs/Get-Blank.md')] [OutputType('Blank')] [Alias('__', '____', 'FILL_ME_IN')] diff --git a/PSKoans/Public/Get-PSKoan.ps1 b/PSKoans/Public/Get-PSKoan.ps1 index f763d5575..8ef365e41 100644 --- a/PSKoans/Public/Get-PSKoan.ps1 +++ b/PSKoans/Public/Get-PSKoan.ps1 @@ -65,6 +65,16 @@ function Get-PSKoan { .LINK https://github.com/vexx32/PSKoans/tree/main/docs/PSKoans.md #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', + 'SkipAttributeParsing', + Justification = 'Referenced inside a nested ForEach-Object scriptblock, which PSScriptAnalyzer does not always trace back to the enclosing param.' + )] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', + 'ListModules', + Justification = 'Used only to select the ListModules parameter set; dispatch reads $PSCmdlet.ParameterSetName, not the switch value.' + )] [CmdletBinding(DefaultParameterSetName = 'IncludeModule', HelpUri = 'https://github.com/vexx32/PSKoans/tree/main/docs/Get-PSKoan.md')] [OutputType('PSKoans.KoanInfo')] diff --git a/PSKoans/Public/Set-PSKoanSetting.ps1 b/PSKoans/Public/Set-PSKoanSetting.ps1 index c96e81392..2feeb9e6a 100644 --- a/PSKoans/Public/Set-PSKoanSetting.ps1 +++ b/PSKoans/Public/Set-PSKoanSetting.ps1 @@ -39,6 +39,16 @@ function Set-PSKoanSetting { .LINK https://github.com/vexx32/PSKoans/tree/main/docs/PSKoans.md #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', + 'Value', + Justification = 'Referenced inside a nested Select-Object calculated-property scriptblock, which PSScriptAnalyzer does not trace back to the enclosing param.' + )] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', + 'Reset', + Justification = 'Used only to select the Reset parameter set; dispatch reads $PSCmdlet.ParameterSetName, not the switch value.' + )] [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium', DefaultParameterSetName = 'Single', HelpUri = 'https://github.com/vexx32/PSKoans/tree/main/docs/Set-PSKoanSetting.md')] [OutputType([void])] @@ -60,45 +70,47 @@ function Set-PSKoanSetting { $Reset ) - if ($PSCmdlet.ShouldProcess($script:ConfigPath, "Update configuration file")) { - $CurrentSettings = if (Test-Path $script:ConfigPath) { - Get-Content -Path $script:ConfigPath | ConvertFrom-Json - } - else { - $ConfigRoot = $script:ConfigPath | Split-Path -Parent - - if (-not (Test-Path $ConfigRoot)) { - New-Item -ItemType Directory -Path $ConfigRoot > $null + process { + if ($PSCmdlet.ShouldProcess($script:ConfigPath, "Update configuration file")) { + $CurrentSettings = if (Test-Path $script:ConfigPath) { + Get-Content -Path $script:ConfigPath | ConvertFrom-Json } + else { + $ConfigRoot = $script:ConfigPath | Split-Path -Parent - [PSCustomObject]$script:DefaultSettings - } + if (-not (Test-Path $ConfigRoot)) { + New-Item -ItemType Directory -Path $ConfigRoot > $null + } - $NewSettings = switch ($PSCmdlet.ParameterSetName) { - 'Single' { - $CurrentSettings | - Select-Object -Property *, @{ Name = $Name; Expression = { $Value } } -ExcludeProperty $Name + [PSCustomObject]$script:DefaultSettings } - 'Multiple' { - $Properties = @( - '*' - foreach ($key in $Settings.Keys) { - @{ - Name = $key - Expression = { $Settings[$key] }.GetNewClosure() + + $NewSettings = switch ($PSCmdlet.ParameterSetName) { + 'Single' { + $CurrentSettings | + Select-Object -Property *, @{ Name = $Name; Expression = { $Value } } -ExcludeProperty $Name + } + 'Multiple' { + $Properties = @( + '*' + foreach ($key in $Settings.Keys) { + @{ + Name = $key + Expression = { $Settings[$key] }.GetNewClosure() + } } - } - ) - $CurrentSettings | - Select-Object -Property $Properties -ExcludeProperty $Settings.Keys.ForEach{ $_ } + ) + $CurrentSettings | + Select-Object -Property $Properties -ExcludeProperty $Settings.Keys.ForEach{ $_ } + } + 'Reset' { + $CurrentSettings + } } - 'Reset' { - $CurrentSettings - } - } - $NewSettings | - ConvertTo-Json | - Set-Content -Path $script:ConfigPath + $NewSettings | + ConvertTo-Json | + Set-Content -Path $script:ConfigPath + } } } diff --git a/PSKoans/Public/Show-Karma.ps1 b/PSKoans/Public/Show-Karma.ps1 index 98224d33f..ec6e12b1d 100644 --- a/PSKoans/Public/Show-Karma.ps1 +++ b/PSKoans/Public/Show-Karma.ps1 @@ -71,6 +71,21 @@ function Show-Karma { .LINK https://github.com/vexx32/PSKoans/tree/main/docs/PSKoans.md #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', + 'List', + Justification = 'Used only to select the ListKoans parameter sets; dispatch reads $PSCmdlet.ParameterSetName, not the switch value.' + )] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', + 'Contemplate', + Justification = 'Used only to select the OpenFile parameter sets; dispatch reads $PSCmdlet.ParameterSetName, not the switch value.' + )] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSReviewUnusedParameter', + 'Library', + Justification = 'Used only to select the OpenFolder parameter set; dispatch reads $PSCmdlet.ParameterSetName, not the switch value.' + )] [CmdletBinding(DefaultParameterSetName = 'Default', HelpUri = 'https://github.com/vexx32/PSKoans/tree/main/docs/Show-Karma.md')] [OutputType([void])] diff --git a/PSScriptAnalyzerSettings.psd1 b/PSScriptAnalyzerSettings.psd1 new file mode 100644 index 000000000..bc3c3212d --- /dev/null +++ b/PSScriptAnalyzerSettings.psd1 @@ -0,0 +1,21 @@ +@{ + # PSKoans organization default PSScriptAnalyzer ruleset. + # Run every rule PSScriptAnalyzer ships as part of its default (non-formatting) set, at every + # severity, so nothing is silently hidden from `Invoke-psake Analyze`. Only exclude a rule here + # when it is fundamentally inapplicable to this codebase; every exclusion carries a justification. + # + # NOTE: deliberately no `Severity` key -- setting one filters diagnostics by their own Severity + # value, which silently drops ParseError-severity records (e.g. `using module` resolution + # failures) with no way to see or document what was suppressed. + IncludeDefaultRules = $true + + ExcludeRules = @( + # PSKoans is an interactive console teaching tool -- Write-ConsoleLine (and the koan output + # it powers) uses Write-Host by design to render colored console text, not as ad-hoc logging. + 'PSAvoidUsingWriteHost', + + # PSKoans.psd1 sets VariablesToExport = '*' deliberately: koan/library-generated variables + # aren't statically enumerable at manifest-authoring time. + 'PSUseToExportFieldsInManifest' + ) +} diff --git a/psakeFile.ps1 b/psakeFile.ps1 index c33fa5aed..51a4125c3 100644 --- a/psakeFile.ps1 +++ b/psakeFile.ps1 @@ -12,6 +12,7 @@ Properties { $PSBPreference.Test.OutputFile = 'out/testResults.xml' $PSBPreference.Test.OutputFormat = 'JUnitXml' $PSBPreference.Test.ScriptAnalysis.Enabled = $true + $PSBPreference.Test.ScriptAnalysis.SettingsPath = Join-Path $PSScriptRoot 'PSScriptAnalyzerSettings.psd1' $PSBPreference.Test.ScriptAnalysis.FailBuildOnSeverityLevel = 'Error' $PSBPreference.Test.CodeCoverage.Enabled = $false @@ -25,6 +26,20 @@ Task GenerateFormatData -Depends Clean { & (Join-Path $PSBPreference.General.ProjectRoot 'PSKoans.ezformat.ps1') } -Description 'Generates PSKoans.format.ps1xml from ./formatting' +# Koan files start with `using module PSKoans`. PSScriptAnalyzer can only resolve that if the +# staged module is discoverable on PSModulePath -- without it, every koan file surfaces a +# `ModuleNotFoundDuringParse` ParseError, and (more importantly) PSScriptAnalyzer skips every +# other rule that depends on symbol resolution (aliases, output types, etc.) for that file. +Task PrepareAnalysis -Depends Build { + $moduleParentDir = Split-Path -Path $PSBPreference.Build.ModuleOutDir -Parent + $currentEntries = $env:PSModulePath -split [IO.Path]::PathSeparator + if ($moduleParentDir -notin $currentEntries) { + $env:PSModulePath = $moduleParentDir + [IO.Path]::PathSeparator + $env:PSModulePath + } +} -Description 'Makes the staged PSKoans module resolvable so koan files'' `using module` statements parse cleanly' + +$PSBAnalyzeDependency = @('PrepareAnalysis') + $PSBStageFilesDependency = @('Clean', 'GenerateFormatData') $PSBBuildDependency = @('StageFiles') diff --git a/tests/Functions/Private/Assert-UnblockedFile.Tests.ps1 b/tests/Functions/Private/Assert-UnblockedFile.Tests.ps1 index 67bf2744c..0ff5811d0 100644 --- a/tests/Functions/Private/Assert-UnblockedFile.Tests.ps1 +++ b/tests/Functions/Private/Assert-UnblockedFile.Tests.ps1 @@ -29,7 +29,7 @@ $SkipTests = $PSVersionTable.PSEdition -ne 'Desktop' -or $PSVersionTable.Platfor Describe 'Assert-UnblockedFile' -Skip:$SkipTests { BeforeAll { - $defaultParams = @{ + $script:defaultParams = @{ FileInfo = [System.IO.FileInfo](Join-Path -Path $TestDrive -ChildPath 'AboutSomething.Koans.ps1') PassThru = $true } diff --git a/tests/Functions/Private/Get-KoanAttribute.Tests.ps1 b/tests/Functions/Private/Get-KoanAttribute.Tests.ps1 index 5e84dde68..3f1bf9938 100644 --- a/tests/Functions/Private/Get-KoanAttribute.Tests.ps1 +++ b/tests/Functions/Private/Get-KoanAttribute.Tests.ps1 @@ -25,7 +25,7 @@ BeforeDiscovery { Describe 'Get-KoanAttribute' { BeforeAll { - $filePath = @{ + $script:filePath = @{ Path = Join-Path $TestDrive -ChildPath 'AboutSomething.Koans.ps1' } } diff --git a/tests/Functions/Private/Get-KoanIt.Tests.ps1 b/tests/Functions/Private/Get-KoanIt.Tests.ps1 index 56d3089cf..519cbf729 100644 --- a/tests/Functions/Private/Get-KoanIt.Tests.ps1 +++ b/tests/Functions/Private/Get-KoanIt.Tests.ps1 @@ -25,7 +25,7 @@ BeforeDiscovery { Describe 'Get-KoanIt' { BeforeAll { - $defaultParams = @{ + $script:defaultParams = @{ Path = Join-Path $TestDrive 'AboutSomething.Koans.ps1' } } diff --git a/tests/Functions/Private/New-KoanRunspace.Tests.ps1 b/tests/Functions/Private/New-KoanRunspace.Tests.ps1 index b1cc8101d..736305c91 100644 --- a/tests/Functions/Private/New-KoanRunspace.Tests.ps1 +++ b/tests/Functions/Private/New-KoanRunspace.Tests.ps1 @@ -24,10 +24,6 @@ BeforeDiscovery { Describe 'New-KoanRunspace' { - BeforeAll { - [runspace]$runspace = $null - } - AfterEach { $runspace.Dispose() } diff --git a/tests/Functions/Private/Update-PSKoanFile.Tests.ps1 b/tests/Functions/Private/Update-PSKoanFile.Tests.ps1 index 6688efc08..878e7046c 100644 --- a/tests/Functions/Private/Update-PSKoanFile.Tests.ps1 +++ b/tests/Functions/Private/Update-PSKoanFile.Tests.ps1 @@ -65,7 +65,7 @@ Describe 'Update-PSKoanFile' { } '@ - $userFilePath = Join-Path -Path $koanLocation -ChildPath $koanRelativePath + $script:userFilePath = Join-Path -Path $koanLocation -ChildPath $koanRelativePath } BeforeEach { diff --git a/tests/Functions/Public/Get-Karma.Tests.ps1 b/tests/Functions/Public/Get-Karma.Tests.ps1 index f04982297..cbfd09bdd 100644 --- a/tests/Functions/Public/Get-Karma.Tests.ps1 +++ b/tests/Functions/Public/Get-Karma.Tests.ps1 @@ -25,7 +25,7 @@ BeforeDiscovery { Describe 'Get-Karma' { BeforeAll { - $originalLocation = Get-PSKoanLocation + $script:originalLocation = Get-PSKoanLocation Set-PSKoanLocation -Path (Join-Path $TestDrive -ChildPath 'PSKoans') Update-PSKoan -Confirm:$false } @@ -46,7 +46,7 @@ Describe 'Get-Karma' { } } - $Result = Get-Karma + $script:Result = Get-Karma } It 'produces a hashtable with data' { @@ -151,7 +151,7 @@ Describe 'Get-Karma' { '@ try { - $Result = Get-Karma -Topic SelectedTopicTest + $script:Result = Get-Karma -Topic SelectedTopicTest } catch { # Ignore this. Error tests follow. diff --git a/tests/Functions/Public/Get-PSKoanLocation.Tests.ps1 b/tests/Functions/Public/Get-PSKoanLocation.Tests.ps1 index b9a9730a5..4c5fbe776 100644 --- a/tests/Functions/Public/Get-PSKoanLocation.Tests.ps1 +++ b/tests/Functions/Public/Get-PSKoanLocation.Tests.ps1 @@ -31,7 +31,7 @@ Describe 'Get-PSKoanLocation' { '~/PSKoans' } - $Result = Get-PSKoanLocation + $script:Result = Get-PSKoanLocation } It 'retrieves the koan library location' { diff --git a/tests/Functions/Public/Get-PSKoanSetting.Tests.ps1 b/tests/Functions/Public/Get-PSKoanSetting.Tests.ps1 index 4c54224e3..7d02b18f7 100644 --- a/tests/Functions/Public/Get-PSKoanSetting.Tests.ps1 +++ b/tests/Functions/Public/Get-PSKoanSetting.Tests.ps1 @@ -32,7 +32,7 @@ Describe 'Get-PSKoanSetting' { $script:ConfigPath = $Path } - ${/} = [IO.Path]::DirectorySeparatorChar + ${script:/} = [IO.Path]::DirectorySeparatorChar } AfterAll { @@ -51,7 +51,7 @@ Describe 'Get-PSKoanSetting' { It 'returns the default settings' { $Result = Get-PSKoanSetting $Result | Should -BeOfType [PSCustomObject] - $Result.KoanLocation | Should -BeExactly "$HOME${/}PSKoans" + $Result.KoanLocation | Should -BeExactly "$HOME${script:/}PSKoans" $Result.Editor | Should -BeExactly 'code' } diff --git a/tests/Functions/Public/Move-PSKoanLibrary.Tests.ps1 b/tests/Functions/Public/Move-PSKoanLibrary.Tests.ps1 index 4e9623568..5c92f7e8e 100644 --- a/tests/Functions/Public/Move-PSKoanLibrary.Tests.ps1 +++ b/tests/Functions/Public/Move-PSKoanLibrary.Tests.ps1 @@ -56,7 +56,7 @@ Describe 'Move-PSKoanLibrary' { Context 'Integration Tests' { BeforeAll { - $OldLocation = Get-PSKoanLocation + $script:OldLocation = Get-PSKoanLocation Set-PSKoanLocation -Path 'TestDrive:/PSKoans' Update-PSKoan -Confirm:$false diff --git a/tests/Functions/Public/Reset-PSKoan.Tests.ps1 b/tests/Functions/Public/Reset-PSKoan.Tests.ps1 index 3b80cecd6..35c19cb51 100644 --- a/tests/Functions/Public/Reset-PSKoan.Tests.ps1 +++ b/tests/Functions/Public/Reset-PSKoan.Tests.ps1 @@ -25,7 +25,7 @@ BeforeDiscovery { Describe Reset-PSKoan { BeforeAll { - $defaultParams = @{ + $script:defaultParams = @{ Confirm = $false } diff --git a/tests/Functions/Public/Set-PSKoanSetting.Tests.ps1 b/tests/Functions/Public/Set-PSKoanSetting.Tests.ps1 index 0b3645959..82eab7630 100644 --- a/tests/Functions/Public/Set-PSKoanSetting.Tests.ps1 +++ b/tests/Functions/Public/Set-PSKoanSetting.Tests.ps1 @@ -120,7 +120,7 @@ Describe 'Set-PSKoanSetting' { Describe 'Setting values with -Name and -Value' { BeforeAll { - $NewConfigPath = InModuleScope 'PSKoans' { + $script:NewConfigPath = InModuleScope 'PSKoans' { ($script:ConfigPath = "$TestDrive/config.json") } @@ -155,7 +155,7 @@ Describe 'Set-PSKoanSetting' { Context 'Setting values with -Settings Hashtable' { BeforeAll { - $NewConfigPath = InModuleScope 'PSKoans' { + $script:NewConfigPath = InModuleScope 'PSKoans' { ($script:ConfigPath = "$TestDrive/config.json") } diff --git a/tests/Functions/Public/Show-Advice.Tests.ps1 b/tests/Functions/Public/Show-Advice.Tests.ps1 index aa4713352..ec0ad33ab 100644 --- a/tests/Functions/Public/Show-Advice.Tests.ps1 +++ b/tests/Functions/Public/Show-Advice.Tests.ps1 @@ -31,7 +31,7 @@ Describe "Show-Advice" { Context "Behaviour of Parameter-less Calls" { BeforeAll { - $result = Show-Advice + $script:result = Show-Advice } It "calls Write-ConsoleLine with Parameter -Title" { @@ -70,9 +70,9 @@ Describe "Show-Advice" { Context 'Behaviour with malformed advice files' { BeforeAll { - $GetContentResult = [string]::Empty + $script:GetContentResult = [string]::Empty - Mock Get-Content -MockWith { $GetContentResult } -Verifiable -ModuleName 'PSKoans' + Mock Get-Content -MockWith { $script:GetContentResult } -Verifiable -ModuleName 'PSKoans' Mock Get-ChildItem -MockWith { [PSCustomObject]@{ PSPath = "DummyPath" } } -Verifiable -ModuleName 'PSKoans' } @@ -94,7 +94,7 @@ Describe "Show-Advice" { } | ConvertTo-Json } ) { - $GetContentResult = $Json + $script:GetContentResult = $Json $AdviceName = "TestAdvice" $Message = "Could not find Title and/or Content elements for Advice file: {0}" -f $AdviceName { Show-Advice -name $AdviceName -ErrorAction Stop } | Should -Throw -ExpectedMessage $Message diff --git a/tests/Functions/Public/Show-Karma.Tests.ps1 b/tests/Functions/Public/Show-Karma.Tests.ps1 index 6de7409c6..d2fd3ae13 100644 --- a/tests/Functions/Public/Show-Karma.Tests.ps1 +++ b/tests/Functions/Public/Show-Karma.Tests.ps1 @@ -28,7 +28,7 @@ Describe 'Show-Karma' { $koanLocation = "$TestDrive/Koans" Mock 'Get-PSKoanLocation' -ModuleName 'PSKoans' { $koanLocation } - $EditorSetting = Get-PSKoanSetting -Name Editor + $script:EditorSetting = Get-PSKoanSetting -Name Editor Reset-PSKoan -Confirm:$false } diff --git a/tests/Functions/Public/Update-PSKoan.Tests.ps1 b/tests/Functions/Public/Update-PSKoan.Tests.ps1 index 0d6819b74..c4d19ffd2 100644 --- a/tests/Functions/Public/Update-PSKoan.Tests.ps1 +++ b/tests/Functions/Public/Update-PSKoan.Tests.ps1 @@ -94,7 +94,7 @@ Describe 'Update-PSKoan' { New-Item -Path $koanLocation -ItemType Directory Update-PSKoan -Confirm:$false - $file = Get-ChildItem -Path $koanLocation -Filter *.koans.ps1 -File -Recurse | + $script:file = Get-ChildItem -Path $koanLocation -Filter *.koans.ps1 -File -Recurse | Select-Object -First 1 } diff --git a/tests/KoanValidation.Tests.ps1 b/tests/KoanValidation.Tests.ps1 index 44f80ce18..e2483742e 100644 --- a/tests/KoanValidation.Tests.ps1 +++ b/tests/KoanValidation.Tests.ps1 @@ -116,7 +116,7 @@ Describe 'Static Analysis: Koan Topics' { Context 'Library Cleanliness' { BeforeAll { - $KoanFolder = Resolve-Path "$PSScriptRoot/../PSKoans/Koans" + $script:KoanFolder = Resolve-Path "$PSScriptRoot/../PSKoans/Koans" } It 'does not have topics with duplicate Koan positions' { diff --git a/tests/ModuleValidation.Tests.ps1 b/tests/ModuleValidation.Tests.ps1 index bc26f8d1c..5b4bbfee8 100644 --- a/tests/ModuleValidation.Tests.ps1 +++ b/tests/ModuleValidation.Tests.ps1 @@ -57,7 +57,7 @@ Describe 'Static Analysis: Module & Repository Files' { BeforeAll { $ModuleName = 'PSKoans' - $ModuleRoot = (Get-Module -Name $ModuleName).ModuleBase + $script:ModuleRoot = (Get-Module -Name $ModuleName).ModuleBase } It 'cleanly imports the module' { From 4668ab745cbd5fb05a24c5150400224fa4e4bf86 Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Sat, 19 Sep 2026 20:38:04 -0700 Subject: [PATCH 2/2] Address PR review comments on #503 - RegisterArgumentCompleters.ps1: replace generic "false positive" suppression justifications with the actual reason -- Command, Parameter, CommandAst, and FakeBoundParams are required positional slots in the host-supplied Register-ArgumentCompleter callback signature that these completers don't use. - Set-PSKoanSetting.ps1: add regression tests piping supported input forms (PSCustomObject with Name/Value, hashtable for Settings) as requested. Writing the hashtable-pipe test surfaced a real binding bug: DefaultParameterSetName 'Single' made pipeline binding prefer the ValueFromPipelineByPropertyName 'Single' set as a tiebreaker, which a bare hashtable can never satisfy, so piped hashtables silently failed to bind. Fixed by changing the default parameter set to 'Multiple'. --- PSKoans/Init/RegisterArgumentCompleters.ps1 | 8 +++--- PSKoans/Public/Set-PSKoanSetting.ps1 | 2 +- .../Public/Set-PSKoanSetting.Tests.ps1 | 28 +++++++++++++++++++ 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/PSKoans/Init/RegisterArgumentCompleters.ps1 b/PSKoans/Init/RegisterArgumentCompleters.ps1 index f2098798a..a686c8a21 100644 --- a/PSKoans/Init/RegisterArgumentCompleters.ps1 +++ b/PSKoans/Init/RegisterArgumentCompleters.ps1 @@ -1,22 +1,22 @@ [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSReviewUnusedParameter', 'Command', - Justification = 'false positive' + Justification = 'Required positional slot in the host-supplied Register-ArgumentCompleter callback signature; this completer does not use its value.' )] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSReviewUnusedParameter', 'Parameter', - Justification = 'false positive' + Justification = 'Required positional slot in the host-supplied Register-ArgumentCompleter callback signature; this completer does not use its value.' )] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSReviewUnusedParameter', 'CommandAst', - Justification = 'false positive' + Justification = 'Required positional slot in the host-supplied Register-ArgumentCompleter callback signature; this completer does not use its value.' )] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSReviewUnusedParameter', 'FakeBoundParams', - Justification = 'false positive' + Justification = 'Required positional slot in the host-supplied Register-ArgumentCompleter callback signature; this completer does not use its value.' )] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSReviewUnusedParameter', diff --git a/PSKoans/Public/Set-PSKoanSetting.ps1 b/PSKoans/Public/Set-PSKoanSetting.ps1 index 2feeb9e6a..48cc06597 100644 --- a/PSKoans/Public/Set-PSKoanSetting.ps1 +++ b/PSKoans/Public/Set-PSKoanSetting.ps1 @@ -49,7 +49,7 @@ function Set-PSKoanSetting { 'Reset', Justification = 'Used only to select the Reset parameter set; dispatch reads $PSCmdlet.ParameterSetName, not the switch value.' )] - [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium', DefaultParameterSetName = 'Single', + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium', DefaultParameterSetName = 'Multiple', HelpUri = 'https://github.com/vexx32/PSKoans/tree/main/docs/Set-PSKoanSetting.md')] [OutputType([void])] param( diff --git a/tests/Functions/Public/Set-PSKoanSetting.Tests.ps1 b/tests/Functions/Public/Set-PSKoanSetting.Tests.ps1 index 82eab7630..18c09dd81 100644 --- a/tests/Functions/Public/Set-PSKoanSetting.Tests.ps1 +++ b/tests/Functions/Public/Set-PSKoanSetting.Tests.ps1 @@ -74,6 +74,20 @@ Describe 'Set-PSKoanSetting' { Select-Object -ExpandProperty $Name | Should -BeExactly $Value } + + It 'should add a new setting via pipeline (PSCustomObject with Name/Value): = ' -TestCases @( + @{ Name = 'TestSetting1'; Value = 'TestValue1' } + @{ Name = 'LibraryFolder'; Value = "TestDrive:/PSKoans" } + @{ Name = 'Editor'; Value = 'code-insiders' } + ) { + [PSCustomObject]@{ Name = $Name; Value = $Value } | Set-PSKoanSetting + + Get-PSKoanSetting -Name $Name | Should -BeExactly $Value + Get-Content -Path $NewConfigPath | + ConvertFrom-Json | + Select-Object -ExpandProperty $Name | + Should -BeExactly $Value + } } Context 'Setting values with -Settings Hashtable' { @@ -112,6 +126,20 @@ Describe 'Set-PSKoanSetting' { $Settings.Editor | Should -BeExactly 'code' $Settings.LibraryFolder | Should -BeExactly $NewSettings.LibraryFolder } + + It 'should add or overwrite multiple new settings via pipeline (hashtable)' { + $NewSettings = @{ + TestSetting2 = "TestValue2" + LibraryFolder = "$TestDrive/PSKoans" + } + + $NewSettings | Set-PSKoanSetting + $Settings = Get-PSKoanSetting + + $Settings.TestSetting2 | Should -BeExactly $NewSettings.TestSetting2 + $Settings.Editor | Should -BeExactly 'code' + $Settings.LibraryFolder | Should -BeExactly $NewSettings.LibraryFolder + } } }