diff --git a/.vscode/cSpell.json b/.vscode/cSpell.json index 5516882a..3ae42629 100644 --- a/.vscode/cSpell.json +++ b/.vscode/cSpell.json @@ -19,7 +19,8 @@ "subfolders", "PSPKI", "gitignore", - "Nuget" + "Nuget", + "Hashtable" ], "ignoreRegExpList": [ "AppVeyor", diff --git a/.vscode/settings.json b/.vscode/settings.json index 1f35d4f6..5f4f8fa7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,7 @@ "powershell.codeFormatting.whitespaceBeforeOpenParen": true, "powershell.codeFormatting.whitespaceAroundOperator": true, "powershell.codeFormatting.whitespaceAfterSeparator": true, + "powershell.codeFormatting.whitespaceInsideBrace": false, "powershell.codeFormatting.ignoreOneLineBlock": false, "powershell.codeFormatting.preset": "Custom", "powershell.scriptAnalysis.settingsPath": ".vscode\\analyzersettings.psd1", diff --git a/CHANGELOG.md b/CHANGELOG.md index 17ffc4ec..bdb09981 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,7 @@ - Fixed broken links and formatting in the `README.md` file. - Added Measure-Keyword function to check if all keywords are in lower case. - If a keyword is followed by parentheses,there should be a single space between them. +- Added Measure-Hashtable function to check if a hashtable is correctly formatted. - Turn on the Custom Script Analyzer Rules meta test. ## 0.3.0.0 diff --git a/DscResource.AnalyzerRules/DscResource.AnalyzerRules.psm1 b/DscResource.AnalyzerRules/DscResource.AnalyzerRules.psm1 index cf87a816..9cfed06a 100644 --- a/DscResource.AnalyzerRules/DscResource.AnalyzerRules.psm1 +++ b/DscResource.AnalyzerRules/DscResource.AnalyzerRules.psm1 @@ -1061,4 +1061,80 @@ function Measure-Keyword } } +<# + .SYNOPSIS + Validates all hashtables. + + .DESCRIPTION + Hashtables should have the correct format + + .EXAMPLE + PS C:\> Measure-Hashtable -HashtableAst $HashtableAst + + .INPUTS + [System.Management.Automation.Language.HashtableAst] + + .OUTPUTS + [Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]] + + .NOTES + None +#> +function Measure-Hashtable +{ + [CmdletBinding()] + [OutputType([Microsoft.Windows.Powershell.ScriptAnalyzer.Generic.DiagnosticRecord[]])] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.Language.HashtableAst[]] + $HashtableAst + ) + + try + { + foreach ($hashtable in $HashtableAst) + { + # Empty hashtables should be ignored + if ($hashtable.extent.Text -eq '@{}') + { + continue + } + + $script:diagnosticRecord['RuleName'] = $PSCmdlet.MyInvocation.InvocationName + + $hashtableLines = $hashtable.Extent.Text -split '\n' + + # Hashtable should start with '@{' and end with '}' + if ($hashtableLines[0] -notmatch '@{\r' -or $hashtableLines[-1] -notmatch '\s*}') + { + $script:diagnosticRecord['Extent'] = $hashtable.Extent + $script:diagnosticRecord['Message'] = $localizedData.HashtableShouldHaveCorrectFormat + $script:diagnosticRecord -as $diagnosticRecordType + } + else + { + # We alredy checked that the first line is correctly formatted. Getting the starting indentation here + $initialIndent = ([regex]::Match($hashtable.Extent.StartScriptPosition.Line, '(\s*)')).Length + $expectedLineIndent = $initialIndent + 5 + + foreach ($keyValuePair in $hashtable.KeyValuePairs) + { + if ($keyValuePair.Item1.Extent.StartColumnNumber -ne $expectedLineIndent) + { + $script:diagnosticRecord['Extent'] = $hashtable.Extent + $script:diagnosticRecord['Message'] = $localizedData.HashtableShouldHaveCorrectFormat + $script:diagnosticRecord -as $diagnosticRecordType + break + } + } + } + } + } + catch + { + $PSCmdlet.ThrowTerminatingError($PSItem) + } +} + Export-ModuleMember -Function Measure-* diff --git a/DscResource.AnalyzerRules/en-US/DscResource.AnalyzerRules.psd1 b/DscResource.AnalyzerRules/en-US/DscResource.AnalyzerRules.psd1 index 7f84f2d9..b1fae08b 100644 --- a/DscResource.AnalyzerRules/en-US/DscResource.AnalyzerRules.psd1 +++ b/DscResource.AnalyzerRules/en-US/DscResource.AnalyzerRules.psd1 @@ -43,4 +43,5 @@ ClassOpeningBraceNotOnSameLine = Class should not have the open brace on the sam ClassOpeningBraceShouldBeFollowedByNewLine = Opening brace on Class should be followed by a new line. See https://github.com/PowerShell/DscResources/blob/master/StyleGuidelines.md#one-newline-after-opening-brace ClassOpeningBraceShouldBeFollowedByOnlyOneNewLine = Opening brace on Class should only be followed by one new line. See https://github.com/PowerShell/DscResources/blob/master/StyleGuidelines.md#one-newline-after-opening-brace OneSpaceBetweenKeywordAndParenthesis = If a keyword is followed by a parenthesis, there should be single space between the keyword and the parenthesis. See https://github.com/PowerShell/DscResources/blob/master/StyleGuidelines.md#one-newline-after-opening-brace +HashtableShouldHaveCorrectFormat = Hashtable is not correctly formatted. See https://github.com/PowerShell/DscResources/blob/master/StyleGuidelines.md#correct-format-for-hashtables-or-objects '@ diff --git a/DscResource.CodeCoverage/CodeCovIo.psm1 b/DscResource.CodeCoverage/CodeCovIo.psm1 index 880ee260..30d990e8 100644 --- a/DscResource.CodeCoverage/CodeCovIo.psm1 +++ b/DscResource.CodeCoverage/CodeCovIo.psm1 @@ -71,12 +71,17 @@ function Add-UniqueFileLineToTable if (!$FileLine.ContainsKey($fileKey)) { - $FileLine.add($fileKey, @{ $TableName = @{ } }) + $FileLine.add( + $fileKey, + @{ + $TableName = @{} + } + ) } if (!$FileLine.$fileKey.ContainsKey($TableName)) { - $FileLine.$fileKey.Add($TableName, @{ }) + $FileLine.$fileKey.Add($TableName, @{}) } $lines = $FileLine.($fileKey).$TableName @@ -176,7 +181,7 @@ function Export-CodeCovIoJson } # A table of the file key then a sub-tables of `misses` and `hits` lines. - $FileLine = @{ } + $FileLine = @{} # define common parameters $addUniqueFileLineParams = @{ @@ -194,8 +199,8 @@ function Export-CodeCovIoJson Add-UniqueFileLineToTable -Command $CodeCoverage.HitCommands -TableName 'hits' @addUniqueFileLineParams # Create the results structure - $resultLineData = @{ } - $resultMessages = @{ } + $resultLineData = @{} + $resultMessages = @{} $result = @{ coverage = $resultLineData messages = $resultMessages @@ -209,14 +214,14 @@ function Export-CodeCovIoJson Write-Verbose -Message "summarizing for file: $file" # Get the hits, if they exist - $hits = @{ } + $hits = @{} if ($FileLine.$file.ContainsKey('hits')) { $hits = $FileLine.$file.hits } # Get the misses, if they exist - $misses = @{ } + $misses = @{} if ($FileLine.$file.ContainsKey('misses')) { $misses = $FileLine.$file.misses @@ -236,7 +241,7 @@ function Export-CodeCovIoJson } $lineData = @() - $messages = @{ } + $messages = @{} <# produce the results diff --git a/DscResource.Container/DscResource.Container.psm1 b/DscResource.Container/DscResource.Container.psm1 index c188dd98..d196d6b3 100644 --- a/DscResource.Container/DscResource.Container.psm1 +++ b/DscResource.Container/DscResource.Container.psm1 @@ -724,7 +724,8 @@ function Out-MissedCommand [PSCustomObject[]] $MissedCommand = $MissedCommand | Select-Object -Property @{ - Name = 'File'; Expression = { + Name = 'File' + Expression = { $_.File -replace ("$env:APPVEYOR_BUILD_FOLDER\" -replace '\\', '\\') } }, Function, Line, Command diff --git a/Tests/Unit/DscResource.AnalyzerRules.Tests.ps1 b/Tests/Unit/DscResource.AnalyzerRules.Tests.ps1 index 5c319bb0..1c6e9ad8 100644 --- a/Tests/Unit/DscResource.AnalyzerRules.Tests.ps1 +++ b/Tests/Unit/DscResource.AnalyzerRules.Tests.ps1 @@ -3434,3 +3434,212 @@ Describe 'Measure-Keyword' { } } } + +Describe 'Measure-Hashtable' { + Context 'When calling the function directly' { + BeforeAll { + $ruleName = 'Measure-Hashtable' + $astType = 'System.Management.Automation.Language.HashtableAst' + } + + Context 'When hashtable is not correctly formatted' { + It 'Hashtable defined on a single line' { + $definition = ' + $hashtable = @{Key1 = "Value1";Key2 = 2;Key3 = "3"} + ' + + $mockAst = Get-AstFromDefinition -ScriptDefinition $definition -AstType $astType + $record = Measure-Hashtable -HashtableAst $mockAst + ($record | Measure-Object).Count | Should -Be 1 + $record.Message | Should -Be $localizedData.HashtableShouldHaveCorrectFormat + $record.RuleName | Should -Be $ruleName + } + + It 'Hashtable partially correct formatted' { + $definition = ' + $hashtable = @{ Key1 = "Value1" + Key2 = 2 + Key3 = "3" } + ' + + $mockAst = Get-AstFromDefinition -ScriptDefinition $definition -AstType $astType + $record = Measure-Hashtable -HashtableAst $mockAst + ($record | Measure-Object).Count | Should -Be 1 + $record.Message | Should -Be $localizedData.HashtableShouldHaveCorrectFormat + $record.RuleName | Should -Be $ruleName + } + + It 'Hashtable indentation not correct' { + $definition = ' + $hashtable = @{ + Key1 = "Value1" + Key2 = 2 + Key3 = "3" + } + ' + + $mockAst = Get-AstFromDefinition -ScriptDefinition $definition -AstType $astType + $record = Measure-Hashtable -HashtableAst $mockAst + ($record | Measure-Object).Count | Should -Be 1 + $record.Message | Should -Be $localizedData.HashtableShouldHaveCorrectFormat + $record.RuleName | Should -Be $ruleName + } + + It 'Correctly formatted empty hashtable' { + $definition = ' + $hashtable = @{ } + ' + + $mockAst = Get-AstFromDefinition -ScriptDefinition $definition -AstType $astType + $record = Measure-Hashtable -HashtableAst $mockAst + ($record | Measure-Object).Count | Should -Be 1 + $record.Message | Should -Be $localizedData.HashtableShouldHaveCorrectFormat + $record.RuleName | Should -Be $ruleName + + } + } + + Context 'When hashtable is correctly formatted' { + It "Correctly formatted non-nested hashtable" { + $definition = ' + $hashtable = @{ + Key1 = "Value1" + Key2 = 2 + Key3 = "3" + } + ' + + $mockAst = Get-AstFromDefinition -ScriptDefinition $definition -AstType $astType + $record = Measure-Hashtable -HashtableAst $mockAst + ($record | Measure-Object).Count | Should -Be 0 + } + + It 'Correctly formatted nested hashtable' { + $definition = ' + $hashtable = @{ + Key1 = "Value1" + Key2 = 2 + Key3 = @{ + Key3Key1 = "ExampleText" + Key3Key2 = 42 + } + } + ' + + $mockAst = Get-AstFromDefinition -ScriptDefinition $definition -AstType $astType + $record = Measure-Hashtable -HashtableAst $mockAst + ($record | Measure-Object).Count | Should -Be 0 + } + + It 'Correctly formatted empty hashtable' { + $definition = ' + $hashtable = @{} + ' + + $mockAst = Get-AstFromDefinition -ScriptDefinition $definition -AstType $astType + $record = Measure-Hashtable -HashtableAst $mockAst + ($record | Measure-Object).Count | Should -Be 0 + } + } + } + + Context 'When calling PSScriptAnalyzer' { + BeforeAll { + $invokeScriptAnalyzerParameters = @{ + CustomRulePath = $modulePath + } + $ruleName = "$($script:ModuleName)\Measure-Hashtable" + } + + Context 'When hashtable is not correctly formatted' { + It 'Hashtable defined on a single line' { + $invokeScriptAnalyzerParameters['ScriptDefinition'] = ' + $hashtable = @{Key1 = "Value1";Key2 = 2;Key3 = "3"} + ' + + $record = Invoke-ScriptAnalyzer @invokeScriptAnalyzerParameters + ($record | Measure-Object).Count | Should -Be 1 + $record.Message | Should -Be $localizedData.HashtableShouldHaveCorrectFormat + $record.RuleName | Should -Be $ruleName + } + + It 'Hashtable partially correct formatted' { + $invokeScriptAnalyzerParameters['ScriptDefinition'] = ' + $hashtable = @{ Key1 = "Value1" + Key2 = 2 + Key3 = "3" } + ' + + $record = Invoke-ScriptAnalyzer @invokeScriptAnalyzerParameters + ($record | Measure-Object).Count | Should -Be 1 + $record.Message | Should -Be $localizedData.HashtableShouldHaveCorrectFormat + $record.RuleName | Should -Be $ruleName + } + + It 'Hashtable indentation not correct' { + $invokeScriptAnalyzerParameters['ScriptDefinition'] = ' + $hashtable = @{ + Key1 = "Value1" + Key2 = 2 + Key3 = "3" + } + ' + + $record = Invoke-ScriptAnalyzer @invokeScriptAnalyzerParameters + ($record | Measure-Object).Count | Should -Be 1 + $record.Message | Should -Be $localizedData.HashtableShouldHaveCorrectFormat + $record.RuleName | Should -Be $ruleName + } + + It 'Incorrectly formatted empty hashtable' { + $invokeScriptAnalyzerParameters['ScriptDefinition'] = ' + $hashtable = @{ } + ' + + $record = Invoke-ScriptAnalyzer @invokeScriptAnalyzerParameters + $record.Message | Should -Be $localizedData.HashtableShouldHaveCorrectFormat + $record.RuleName | Should -Be $ruleName + } + } + + Context 'When hashtable is correctly formatted' { + It 'Correctly formatted non-nested hashtable' { + $invokeScriptAnalyzerParameters['ScriptDefinition'] = ' + $hashtable = @{ + Key1 = "Value1" + Key2 = 2 + Key3 = "3" + } + ' + + $record = Invoke-ScriptAnalyzer @invokeScriptAnalyzerParameters + ($record | Measure-Object).Count | Should -Be 0 + } + + It 'Correctly formatted nested hashtable' { + $invokeScriptAnalyzerParameters['ScriptDefinition'] = ' + $hashtable = @{ + Key1 = "Value1" + Key2 = 2 + Key3 = @{ + Key3Key1 = "ExampleText" + Key3Key2 = 42 + } + } + ' + + $record = Invoke-ScriptAnalyzer @invokeScriptAnalyzerParameters + ($record | Measure-Object).Count | Should -Be 0 + } + + It 'Correctly formatted empty hashtable' { + $invokeScriptAnalyzerParameters['ScriptDefinition'] = ' + $hashtable = @{} + ' + + $record = Invoke-ScriptAnalyzer @invokeScriptAnalyzerParameters + ($record | Measure-Object).Count | Should -Be 0 + } + } + } +}