diff --git a/.editorconfig b/.editorconfig index 87230b0..997abde 100644 --- a/.editorconfig +++ b/.editorconfig @@ -18,8 +18,4 @@ end_of_line = crlf [*.{yml, yaml}] indent_size = 2 - -# Makefiles require tab indentation -[{{M,m,GNU}akefile{,.*},*.mak,*.mk}] -indent_style = tab end_of_line = lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..57e64cd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +name: CI + +on: + push: + pull_request: + +permissions: {} + +jobs: + test: + name: Tests + runs-on: windows-latest + defaults: + run: + shell: pwsh + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Install dependencies + uses: potatoqualitee/psmodulecache@ee5e9494714abf56f6efbfa51527b2aec5c761b8 # v6.2.1 + with: + modules-to-cache: PSScriptAnalyzer, Pester + shell: pwsh + - name: Run tests + run: ./tests/run.ps1 diff --git a/.psformatrules.psd1 b/.psformatrules.psd1 new file mode 100644 index 0000000..305b1b9 --- /dev/null +++ b/.psformatrules.psd1 @@ -0,0 +1,67 @@ +@{ + # OTBS + IncludeRules = @( + 'PSPlaceOpenBrace', + 'PSPlaceCloseBrace', + 'PSUseConsistentWhitespace', + 'PSUseConsistentIndentation', + 'PSAlignAssignmentStatement', + 'PSAvoidSemicolonsAsLineTerminators', + 'PSAvoidUsingDoubleQuotesForConstantString', + 'PSUseCorrectCasing' + ) + + Rules = @{ + PSPlaceOpenBrace = @{ + Enable = $true + OnSameLine = $true + NewLineAfter = $true + IgnoreOneLineBlock = $true + } + + PSPlaceCloseBrace = @{ + Enable = $true + NewLineAfter = $false + IgnoreOneLineBlock = $true + NoEmptyLineBefore = $false + } + + PSUseConsistentIndentation = @{ + Enable = $true + Kind = 'space' + PipelineIndentation = 'IncreaseIndentationForFirstPipeline' + IndentationSize = 4 + } + + PSUseConsistentWhitespace = @{ + Enable = $true + CheckInnerBrace = $true + CheckOpenBrace = $true + CheckOpenParen = $true + CheckOperator = $true + CheckPipe = $true + CheckPipeForRedundantWhitespace = $false + CheckSeparator = $true + CheckParameter = $false + IgnoreAssignmentOperatorInsideHashTable = $true + } + + PSAlignAssignmentStatement = @{ + Enable = $true + CheckHashtable = $true + } + + PSAvoidSemicolonsAsLineTerminators = @{ + Enable = $true + } + + PSAvoidUsingDoubleQuotesForConstantString = @{ + Enable = $true + } + + PSUseCorrectCasing = @{ + Enable = $true + } + } +} + diff --git a/.pslintrules.psd1 b/.pslintrules.psd1 new file mode 100644 index 0000000..80095b4 --- /dev/null +++ b/.pslintrules.psd1 @@ -0,0 +1,31 @@ +@{ + # Only diagnostic records of the specified severity will be generated. + # Uncomment the following line if you only want Errors and Warnings but + # not Information diagnostic records. + Severity = @('Error', 'Warning') + + # Analyze **only** the following rules. Use IncludeRules when you want + # to invoke only a small subset of the default rules. + # IncludeRules = @('PSAvoidDefaultValueSwitchParameter', + # 'PSMisleadingBacktick', + # 'PSMissingModuleManifestField', + # 'PSReservedCmdletChar', + # 'PSReservedParams', + # 'PSShouldProcess', + # 'PSUseApprovedVerbs', + # 'PSAvoidUsingCmdletAliases', + # 'PSUseDeclaredVarsMoreThanAssignments') + + # Do not analyze the following rules. Use ExcludeRules when you have + # commented out the IncludeRules settings above and want to include all + # the default rules except for those you exclude below. + # Note: if a rule is in both IncludeRules and ExcludeRules, the rule + # will be excluded. + ExcludeRules = @( + # PSUseDeclaredVarsMoreThanAssignments doesn't currently work due to: + # https://github.com/PowerShell/PSScriptAnalyzer/issues/636 + 'PSUseDeclaredVarsMoreThanAssignments', + # `Write-Log` uses `Write-Host` currently. + 'PSAvoidUsingWriteHost' + ) +} diff --git a/.vscode/settings.json b/.vscode/settings.json index b250385..520963d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,18 +1,9 @@ -// Configure PSScriptAnalyzer settings { - "powershell.scriptAnalysis.settingsPath": "PSScriptAnalyzerSettings.psd1", + "[powershell]": { + "editor.formatOnSave": true, + }, + "powershell.scriptAnalysis.settingsPath": ".pslintrules.psd1", "powershell.codeFormatting.preset": "OTBS", - "powershell.codeFormatting.alignPropertyValuePairs": true, - "powershell.codeFormatting.ignoreOneLineBlock": true, "powershell.codeFormatting.useConstantStrings": true, "powershell.codeFormatting.useCorrectCasing": true, - "powershell.codeFormatting.whitespaceBetweenParameters": true, - "files.exclude": { - "**/.git": true, - "**/.svn": true, - "**/.hg": true, - "**/CVS": true, - "**/.DS_Store": true, - "**/tmp": true - } } diff --git a/tests/Linter.Tests.ps1 b/tests/Linter.Tests.ps1 new file mode 100644 index 0000000..1bba961 --- /dev/null +++ b/tests/Linter.Tests.ps1 @@ -0,0 +1,75 @@ +Describe 'PowerShell Code Style' -Tag 'PSScriptAnalyzer' { + BeforeAll { + $formatSettings = "$PSScriptRoot\..\.psformatrules.psd1" + $lintSettings = "$PSScriptRoot\..\.pslintrules.psd1" + } + + It 'PSScriptAnalyzer rules files should exist' { + $formatSettings | Should -Exist + $lintSettings | Should -Exist + } + + Context 'PowerShell code formatting' { + BeforeAll { + $records = Invoke-ScriptAnalyzer -Path "$PSScriptRoot\..\" ` + -Recurse -Settings $formatSettings + } + It 'Code should be formatted' { + $records.Count | Should -Be 0 + } + AfterAll { + if ($records) { + foreach ($r in $records) { + $type = 'Unknown' + switch -wildCard ($r.ScriptName) { + '*.psm1' { $type = 'Module' } + '*.ps1' { $type = 'Script' } + '*.psd1' { $type = 'Manifest' } + default { $type = 'Unknown' } + } + $scriptPath = Resolve-Path -Relative $r.ScriptPath + $color = switch ($r.Severity) { + 'Error' { 'Red' } + 'Warning' { 'Yellow' } + 'Information' { 'White' } + default { 'White' } + } + Write-Host -f $color " [!] $($r.Severity): $($r.Message)" + Write-Host -f $color " $($r.RuleName) in $type`: $($scriptPath):$($r.Line)" + } + } + } + } + + Context 'PowerShell code linting' { + BeforeAll { + $records = Invoke-ScriptAnalyzer -Path "$PSScriptRoot\..\" ` + -Recurse -Settings $lintSettings + } + It 'Code should be linted' { + $records.Count | Should -Be 0 + } + AfterAll { + if ($records) { + foreach ($r in $records) { + $type = 'Unknown' + switch -wildCard ($r.ScriptName) { + '*.psm1' { $type = 'Module' } + '*.ps1' { $type = 'Script' } + '*.psd1' { $type = 'Manifest' } + default { $type = 'Unknown' } + } + $scriptPath = Resolve-Path -Relative $r.ScriptPath + $color = switch ($r.Severity) { + 'Error' { 'Red' } + 'Warning' { 'Yellow' } + 'Information' { 'White' } + default { 'White' } + } + Write-Host -f $color " [!] $($r.Severity): $($r.Message)" + Write-Host -f $color " $($r.RuleName) in $type`: $($scriptPath):$($r.Line)" + } + } + } + } +} diff --git a/tests/run.ps1 b/tests/run.ps1 new file mode 100644 index 0000000..adaa6e8 --- /dev/null +++ b/tests/run.ps1 @@ -0,0 +1,22 @@ +#Requires -Version 5.1 +#Requires -Modules Pester +#Requires -Modules PSScriptAnalyzer + +$pesterConfig = New-PesterConfiguration -Hashtable @{ + Run = @{ + Path = "$PSScriptRoot" + PassThru = $true + } + Should = @{ + # Continue running tests even if some assertions fail. This allows + # Pester to collect and report all failures at the end of a test + # instead of stopping at the first failing assertion. + ErrorAction = 'Continue' + } + Output = @{ + StackTraceVerbosity = 'None' + Verbosity = 'Detailed' + } +} +$result = Invoke-Pester -Configuration $pesterConfig +exit $result.FailedCount