|
1 | 1 | Describe "Validate rule documentation files" { |
2 | 2 | BeforeAll { |
3 | | - $ruleDocDirectory = Join-Path $PSScriptRoot '../../docs/Rules' |
4 | | - $docs = Get-ChildItem $ruleDocDirectory/*.md -Exclude README.md | |
5 | | - ForEach-Object { "PS" + $_.BaseName} | Sort-Object |
| 3 | + $ruleDocDirectory = 'C:\Git\PS-Src\PSScriptAnalyzer\docs\Rules' #Join-Path $PSScriptRoot '../../docs/Rules' |
| 4 | + $docInfoList = @{} |
| 5 | + Get-ChildItem $ruleDocDirectory/*.md -Exclude README.md | |
| 6 | + ForEach-Object { |
| 7 | + $sev = Select-String -Path $_ -Pattern '\*\*Severity Level: (?<sev>\w+)\*\*' |
| 8 | + $def = Select-String -Path $_ -Pattern '\*\*Default state: (?<def>\w+\s?\w+)\*\*' |
| 9 | + $docInfoList.Add(('PS' + $_.BaseName), [pscustomobject]@{ |
| 10 | + FileName = $_.Name |
| 11 | + Severity = $sev.Matches.Groups.Where({$_.Name -eq 'sev'}).Value |
| 12 | + DefState = $def.Matches.Groups.Where({$_.Name -eq 'def'}).Value |
| 13 | + }) |
| 14 | + } |
| 15 | + #$docInfoList |
6 | 16 |
|
7 | | - $rules = Get-ScriptAnalyzerRule | ForEach-Object RuleName | Sort-Object |
| 17 | + $ruleList = Get-ScriptAnalyzerRule | |
| 18 | + Sort-Object RuleName | |
| 19 | + Select-Object -Property RuleName, Severity |
| 20 | + #$ruleList |
8 | 21 |
|
9 | | - $readmeLinks = @{} |
10 | | - $readmeRules = Get-Content -LiteralPath $ruleDocDirectory/README.md | |
11 | | - Foreach-Object { if ($_ -match '^\s*\|\s*\[([^]]+)\]\(([^)]+)\)\s*\|') { |
12 | | - $ruleName = $matches[1] -replace '<sup>.</sup>$', '' |
13 | | - $readmeLinks["$ruleName"] = $matches[2] |
14 | | - "PS${ruleName}" |
15 | | - }} | |
16 | | - Sort-Object |
| 22 | + $ruleTable = @() |
| 23 | + $linkDefs = @{} |
| 24 | + $linkDefLine = @{} |
| 25 | + $usedRefs = @{} |
| 26 | + # Regex patterns. |
| 27 | + # Table rule cell: | [RuleName][ref] | Severity | Default state |... | -> capture name, ref, severity, defstate. |
| 28 | + $ruleRowRegex = '^\|\s*\[(?<name>[^\]]+)\]\[(?<ref>[^\]]+)\]\s*\|\s*(?<severity>[^|]+?)\s*\|(?<defstate>[^|]+?)\s*\|' |
| 29 | + # Link definition: [ref]: target (target may include an #anchor). |
| 30 | + $linkDefRegex = '^\[(?<ref>[^\]]+)\]:\s*(?<target>\S+)' |
| 31 | + # Any reference-style usage anywhere: ...][ref]... |
| 32 | + $refUsageRegex = '\]\[(?<ref>[^\]]+)\]' |
| 33 | + $lines = Get-Content 'C:\Git\PS-Src\PSScriptAnalyzer\docs\Rules\README.md' |
| 34 | + $lineNumber = 0 |
| 35 | + foreach ($line in $lines) { |
| 36 | + $lineNumber++ |
| 37 | + if ($line -match $ruleRowRegex) { |
| 38 | + $ruleTable += [pscustomobject]@{ |
| 39 | + RowName = $Matches['name'].Trim() |
| 40 | + RuleName = 'PS' + $Matches['name'].Trim() |
| 41 | + Ref = $Matches['ref'].Trim() |
| 42 | + Severity = $Matches['severity'].Trim() |
| 43 | + DefState = $Matches['defstate'].Trim() |
| 44 | + Line = $lineNumber |
| 45 | + } |
| 46 | + } |
17 | 47 |
|
18 | | - $rulesDocsDiff = Compare-Object -ReferenceObject $rules -DifferenceObject $docs -SyncWindow 25 |
19 | | - $rulesReadmeDiff = Compare-Object -ReferenceObject $rules -DifferenceObject $readmeRules -SyncWindow 25 |
| 48 | + if ($line -match $linkDefRegex) { |
| 49 | + $ref = $Matches['ref'].Trim() |
| 50 | + $linkDefs[$ref] = $Matches['target'].Trim() |
| 51 | + $linkDefLine[$ref] = $lineNumber |
| 52 | + } |
| 53 | + |
| 54 | + # Collect every reference usage (table rows and prose) for orphan detection. |
| 55 | + foreach ($match in [regex]::matches($line, $refUsageRegex)) { |
| 56 | + $usedRefs[$match.Groups['ref'].Value] = 1 |
| 57 | + } |
| 58 | + } |
20 | 59 | } |
21 | 60 |
|
22 | | - It "Every rule must have a rule documentation file" { |
23 | | - $rulesDocsDiff | Where-Object SideIndicator -eq "<=" | Foreach-Object InputObject | Should -BeNullOrEmpty |
| 61 | + ######################################################################################### |
| 62 | + |
| 63 | + It 'Every rule documentation file must be a defined rule' { |
| 64 | + $result = $true |
| 65 | + foreach ($rule in $docInfoList.Keys) { |
| 66 | + if ($rule -notin $ruleList.RuleName) { |
| 67 | + Write-Host "Rule not defined for file: $($docInfoList[$rule].FileName)" |
| 68 | + $result = $false |
| 69 | + } |
| 70 | + } |
| 71 | + $result | Should -Be $true |
| 72 | + } |
| 73 | + |
| 74 | + It 'Every defined rule must have a documentation file' { |
| 75 | + $result = $true |
| 76 | + foreach ($rule in $ruleList.RuleName) { |
| 77 | + if ($rule -notin $docInfoList.Keys) { |
| 78 | + Write-Host "Missing documentation file for rule: $($rule)" |
| 79 | + $result = $false |
| 80 | + } |
| 81 | + } |
| 82 | + $result | Should -Be $true |
24 | 83 | } |
25 | | - It "Every rule documentation file must have a corresponding rule" { |
26 | | - $rulesDocsDiff | Where-Object SideIndicator -eq "=>" | Foreach-Object InputObject | Should -BeNullOrEmpty |
| 84 | + |
| 85 | + It 'Every rule doc must have the correct defined severity' { |
| 86 | + $result = $true |
| 87 | + foreach ($rule in $ruleList) { |
| 88 | + if ($rule.Severity -ne $docInfoList[$rule.RuleName].Severity) { |
| 89 | + Write-Host "Severity mismatch for rule: $($rule.RuleName). Defined: $($rule.Severity), Doc: $($docInfoList[$rule.RuleName].Severity)" |
| 90 | + $result = $false |
| 91 | + } |
| 92 | + } |
| 93 | + $result | Should -Be $true |
27 | 94 | } |
28 | 95 |
|
29 | | - It "Every rule must have an entry in the rule documentation README.md file" { |
30 | | - $rulesReadmeDiff | Where-Object SideIndicator -eq "<=" | Foreach-Object InputObject | Should -BeNullOrEmpty |
| 96 | + It 'Every rule in the table must have the correct severity and default state' { |
| 97 | + $result = $true |
| 98 | + foreach ($ruleRow in $ruleTable) { |
| 99 | + $definedRule = $ruleList | Where-Object { $_.RuleName -eq $ruleRow.RuleName } |
| 100 | + if ($null -eq $definedRule) { |
| 101 | + Write-Host "Rule in table not found in defined rules: $($ruleRow.RowName)" |
| 102 | + $result = $false |
| 103 | + continue |
| 104 | + } |
| 105 | + if ($ruleRow.RuleName -notin $docInfoList.Keys) { |
| 106 | + Write-Host "Rule in table not found in documentation: $($ruleRow.RowName)" |
| 107 | + $result = $false |
| 108 | + continue |
| 109 | + } |
| 110 | + $docInfo = $docInfoList[$ruleRow.RuleName] |
| 111 | + if ($ruleRow.Severity -ne $docInfo.Severity) { |
| 112 | + Write-Host "Severity mismatch for rule: $($ruleRow.RowName). Table: $($ruleRow.Severity), Doc: $($docInfo.Severity)" |
| 113 | + $result = $false |
| 114 | + } |
| 115 | + if ($ruleRow.DefState -ne $docInfo.DefState) { |
| 116 | + Write-Host "Default state mismatch for rule: $($ruleRow.RowName). Table: $($ruleRow.DefState), Doc: $($docInfo.DefState)" |
| 117 | + $result = $false |
| 118 | + } |
| 119 | + } |
| 120 | + $result | Should -Be $true |
31 | 121 | } |
32 | | - It "Every entry in the rule documentation README.md file must correspond to a rule" { |
33 | | - $rulesReadmeDiff | Where-Object SideIndicator -eq "=>" | Foreach-Object InputObject | Should -BeNullOrEmpty |
| 122 | + |
| 123 | + It 'Every link definition must be used at least once' { |
| 124 | + $result = $true |
| 125 | + foreach ($ref in $linkDefs.Keys) { |
| 126 | + if ($ref -notin $usedRefs.Keys) { |
| 127 | + Write-Host "Unused link definition: $ref (defined at line $($linkDefLine[$ref]))" |
| 128 | + $result = $false |
| 129 | + } |
| 130 | + } |
| 131 | + $result | Should -Be $true |
34 | 132 | } |
35 | 133 |
|
36 | | - It "Every entry in the rule documentation README.md file must have a valid link to the documentation file" { |
37 | | - foreach ($key in $readmeLinks.Keys) { |
38 | | - $link = $readmeLinks[$key] |
39 | | - $filePath = Join-Path $ruleDocDirectory $link |
40 | | - $filePath | Should -Exist |
| 134 | + It 'Every link definition that points to a rule must have a matching rule in the table' { |
| 135 | + $result = $true |
| 136 | + foreach ($ref in $linkDefs.Keys) { |
| 137 | + $target = $linkDefs[$ref] |
| 138 | + $isRuleFile = $targetFile -match '\.md$' -and $targetFile -notmatch '/' |
| 139 | + |
| 140 | + if ($isRuleFile) { |
| 141 | + # A rule-page link definition with no matching table row. |
| 142 | + if ($ref -notin $ruleTable.Ref) { |
| 143 | + Write-Host "Orphan rule target: Link definition [$ref] -> '$target' has no matching rule in the table (defined at line $($linkDefLine[$ref]))." |
| 144 | + $result = $false |
| 145 | + } |
| 146 | + } |
41 | 147 | } |
| 148 | + $result | Should -Be $true |
42 | 149 | } |
43 | 150 |
|
44 | | - It "Every rule name in the rule documentation README.md file must match the documentation file's basename" { |
45 | | - foreach ($key in $readmeLinks.Keys) { |
46 | | - $link = $readmeLinks[$key] |
47 | | - $filePath = Join-Path $ruleDocDirectory $link |
48 | | - $fileName = Split-Path $filePath -Leaf |
49 | | - $fileName | Should -BeExactly "${key}.md" |
| 151 | + It 'Every link definition target must have a valid file path' { |
| 152 | + $result = $true |
| 153 | + foreach ($ref in $linkDefs.Keys) { |
| 154 | + $target = $linkDefs[$ref] |
| 155 | + $isRuleFile = $targetFile -match '\.md$' -and $targetFile -notmatch '/' |
| 156 | + |
| 157 | + if ($isRuleFile) { |
| 158 | + # A rule-page link definition with no matching table row. |
| 159 | + if ($target -notin $ruleTable.FileName) { |
| 160 | + Write-Host "Orphan rule target: Link definition [$ref] -> '$target' has no matching rule file." |
| 161 | + $result = $false |
| 162 | + } |
| 163 | + } |
50 | 164 | } |
| 165 | + $result | Should -Be $true |
51 | 166 | } |
52 | 167 | } |
0 commit comments