Skip to content

Commit 411c3d0

Browse files
sdwheelerCopilot
andauthored
Sync docs changes from docs repo (#2196)
* Sync docs changes from docs repo * Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Add missing configuration parameter * Update docs test * fix $ruleDocDirectory * fix path to README * Fix severity in rule.md * fix severity in table * Address Copilot feedback * Fix pester test for docs * Restore files for new rules --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 168dcbd commit 411c3d0

86 files changed

Lines changed: 3108 additions & 1889 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 154 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,174 @@
11
Describe "Validate rule documentation files" {
22
BeforeAll {
33
$ruleDocDirectory = Join-Path $PSScriptRoot '../../docs/Rules'
4-
$docs = Get-ChildItem $ruleDocDirectory/*.md -Exclude README.md |
5-
ForEach-Object { "PS" + $_.BaseName} | Sort-Object
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
616

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
821

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 (Join-Path $ruleDocDirectory '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+
}
1747

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+
}
2059
}
2160

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
2472
}
25-
It "Every rule documentation file must have a corresponding rule" {
26-
$rulesDocsDiff | Where-Object SideIndicator -eq "=>" | Foreach-Object InputObject | Should -BeNullOrEmpty
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
2783
}
2884

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
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
3194
}
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
95+
96+
It 'Every defined rule must have a matching table entry with the correct severity and default state' {
97+
$result = $true
98+
foreach ($rule in $ruleList.RuleName) {
99+
if ($rule -notin $ruleTable.RuleName) {
100+
Write-Host "Missing README table entry for rule: $rule"
101+
$result = $false
102+
}
103+
}
104+
foreach ($ruleRow in $ruleTable) {
105+
$definedRule = $ruleList | Where-Object { $_.RuleName -eq $ruleRow.RuleName }
106+
if ($null -eq $definedRule) {
107+
Write-Host "Rule in table not found in defined rules: $($ruleRow.RowName)"
108+
$result = $false
109+
continue
110+
}
111+
if ($ruleRow.RuleName -notin $docInfoList.Keys) {
112+
Write-Host "Rule in table not found in documentation: $($ruleRow.RowName)"
113+
$result = $false
114+
continue
115+
}
116+
$docInfo = $docInfoList[$ruleRow.RuleName]
117+
if ($ruleRow.Severity -ne $docInfo.Severity) {
118+
Write-Host "Severity mismatch for rule: $($ruleRow.RowName). Table: $($ruleRow.Severity), Doc: $($docInfo.Severity)"
119+
$result = $false
120+
}
121+
if ($ruleRow.DefState -ne $docInfo.DefState) {
122+
Write-Host "Default state mismatch for rule: $($ruleRow.RowName). Table: $($ruleRow.DefState), Doc: $($docInfo.DefState)"
123+
$result = $false
124+
}
125+
}
126+
$result | Should -Be $true
34127
}
35128

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
129+
It 'Every link definition must be used at least once' {
130+
$result = $true
131+
foreach ($ref in $linkDefs.Keys) {
132+
if ($ref -notin $usedRefs.Keys) {
133+
Write-Host "Unused link definition: $ref (defined at line $($linkDefLine[$ref]))"
134+
$result = $false
135+
}
41136
}
137+
$result | Should -Be $true
42138
}
43139

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"
140+
It 'Every link definition that points to a rule must have a matching rule in the table' {
141+
$result = $true
142+
foreach ($ref in $linkDefs.Keys) {
143+
$targetFile = $linkDefs[$ref]
144+
$isRuleFile = $targetFile -match '\.md$' -and $targetFile -notmatch '/'
145+
146+
if ($isRuleFile) {
147+
# A rule-page link definition with no matching table row.
148+
if ($ref -notin $ruleTable.Ref) {
149+
Write-Host "Orphan rule target: Link definition [$ref] -> '$targetFile' has no matching rule in the table (defined at line $($linkDefLine[$ref]))."
150+
$result = $false
151+
}
152+
}
153+
}
154+
$result | Should -Be $true
155+
}
156+
157+
It 'Every link definition target must have a valid file path' {
158+
$result = $true
159+
foreach ($ref in $linkDefs.Keys) {
160+
$targetFile = $linkDefs[$ref]
161+
$isRuleFile = $targetFile -match '\.md$' -and $targetFile -notmatch '/'
162+
$targetRule = $targetFile -replace '\.md$', ''
163+
164+
if ($isRuleFile) {
165+
# A rule-page link definition with no matching table row.
166+
if ($targetRule -notin $ruleTable.RowName) {
167+
Write-Host "Orphan rule target: Link definition [$ref] -> '$targetFile' has no matching rule file."
168+
$result = $false
169+
}
170+
}
50171
}
172+
$result | Should -Be $true
51173
}
52174
}

‎docs/Cmdlets/Invoke-Formatter.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ schema: 2.0.0
99
# Invoke-Formatter
1010

1111
## SYNOPSIS
12+
1213
Formats a script text based on the input settings or default settings.
1314

1415
## SYNTAX
@@ -76,7 +77,7 @@ function foo
7677
}
7778
```
7879

79-
### EXAMPLE 3 - Format the input script text using the settings defined a `.psd1` file
80+
### EXAMPLE 3 - Format the input script text using the settings defined in a `.psd1` file
8081

8182
```powershell
8283
Invoke-Formatter -ScriptDefinition $scriptDefinition -Settings /path/to/settings.psd1

‎docs/Cmdlets/Invoke-ScriptAnalyzer.md‎

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
external help file: Microsoft.Windows.PowerShell.ScriptAnalyzer.dll-Help.xml
33
Module Name: PSScriptAnalyzer
4-
ms.date: 10/07/2021
4+
ms.date: 07/23/2026
55
online version: https://learn.microsoft.com/powershell/module/psscriptanalyzer/invoke-scriptanalyzer?view=ps-modules&wt.mc_id=ps-gethelp
66
schema: 2.0.0
77
---
@@ -107,7 +107,12 @@ This example runs all rules except for **PSAvoidUsingCmdletAliases** and
107107
subdirectories.
108108

109109
```powershell
110-
Invoke-ScriptAnalyzer -Path C:\ps-test\MyModule -Recurse -ExcludeRule PSAvoidUsingCmdletAliases, PSAvoidUsingInternalURLs
110+
$invokeScriptAnalyzerSplat = @{
111+
Path = 'C:\ps-test\MyModule'
112+
Recurse = $true
113+
ExcludeRule = 'PSAvoidUsingCmdletAliases', 'PSAvoidUsingInternalURLs'
114+
}
115+
Invoke-ScriptAnalyzer @invokeScriptAnalyzerSplat
111116
```
112117

113118
### EXAMPLE 5 - Run Script Analyzer with custom rules
@@ -116,13 +121,19 @@ This example runs Script Analyzer on `Test-Script.ps1` with the standard rules a
116121
`C:\CommunityAnalyzerRules` path.
117122

118123
```powershell
119-
Invoke-ScriptAnalyzer -Path D:\test_scripts\Test-Script.ps1 -CustomRulePath C:\CommunityAnalyzerRules -IncludeDefaultRules
124+
$invokeScriptAnalyzerSplat = @{
125+
Path = 'D:\test_scripts\Test-Script.ps1'
126+
CustomRulePath = 'C:\CommunityAnalyzerRules'
127+
IncludeDefaultRules = $true
128+
}
129+
Invoke-ScriptAnalyzer @invokeScriptAnalyzerSplat
120130
```
121131

122132
### EXAMPLE 6 - Run only the rules that are Error severity and have the PSDSC source name
123133

124134
```powershell
125-
$DSCError = Get-ScriptAnalyzerRule -Severity Error | Where SourceName -eq PSDSC
135+
$DSCError = Get-ScriptAnalyzerRule -Severity Error |
136+
Where-Object SourceName -eq PSDSC
126137
$Path = "$home\Documents\WindowsPowerShell\Modules\MyDSCModule"
127138
Invoke-ScriptAnalyzerRule -Path $Path -IncludeRule $DSCError -Recurse
128139
```
@@ -145,34 +156,32 @@ function Get-Widgets
145156
{
146157
[CmdletBinding()]
147158
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
148-
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingCmdletAliases", "", Justification="Resolution in progress.")]
159+
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingCmdletAliases", "",
160+
Justification="Resolution in progress.")]
149161
Param()
150162
151-
dir $pshome
163+
dir $PSHOME
152164
...
153165
}
154166
155167
Invoke-ScriptAnalyzer -Path .\Get-Widgets.ps1
156168
```
157169

158170
```Output
159-
RuleName Severity FileName Line Message
160-
-------- -------- -------- ---- -------
161-
PSProvideCommentHelp Information ManageProf 14 The cmdlet 'Get-Widget' does not have a help comment.
162-
iles.psm1
171+
RuleName Severity FileName Line Message
172+
-------- -------- -------- ---- -------
173+
PSProvideCommentHelp Information ManageProfiles.psm1 14 The cmdlet 'Get-Widget' does not have a help comment.
163174
```
164175

165176
```powershell
166177
Invoke-ScriptAnalyzer -Path .\Get-Widgets.ps1 -SuppressedOnly
167178
```
168179

169180
```Output
170-
Rule Name Severity File Name Line Justification
171-
--------- -------- --------- ---- -------------
172-
PSAvoidUsingCmdletAliases Warning ManageProf 21 Resolution in progress.
173-
iles.psm1
174-
PSUseSingularNouns Warning ManageProf 14
175-
iles.psm1
181+
Rule Name Severity File Name Line Justification
182+
--------- -------- --------- ---- -------------
183+
PSAvoidUsingCmdletAliases Warning ManageProfiles.psm1 21 Resolution in progress.
184+
PSUseSingularNouns Warning ManageProfiles.psm1 14
176185
```
177186

178187
The second command uses the **SuppressedOnly** parameter to report violations of the rules that are
@@ -192,7 +201,7 @@ value of the **Profile** parameter is the path to the Script Analyzer profile.
192201
ExcludeRules = '*WriteHost'
193202
}
194203
195-
Invoke-ScriptAnalyzer -Path $pshome\Modules\BitLocker -Settings .\ScriptAnalyzerProfile.txt
204+
Invoke-ScriptAnalyzer -Path $PSHOME\Modules\BitLocker -Settings .\ScriptAnalyzerProfile.txt
196205
```
197206

198207
If you include a conflicting parameter in the `Invoke-ScriptAnalyzer` command, such as
@@ -208,15 +217,16 @@ Invoke-ScriptAnalyzer -ScriptDefinition "function Get-Widgets {Write-Host 'Hello
208217
```
209218

210219
```Output
211-
RuleName Severity FileName Line Message
212-
-------- -------- -------- ---- -------
213-
PSAvoidUsingWriteHost Warning 1 Script
214-
because
215-
there i
216-
suppres
217-
Write-O
218-
PSUseSingularNouns Warning 1 The cmd
219-
noun sh
220+
RuleName Severity FileName Line Message
221+
-------- -------- -------- ---- -------
222+
PSAvoidUsingWriteHost Warning 1 Script definition uses Write-Host. Avoid using
223+
Write-Host because it might not work in all hosts,
224+
does not work when there is no host, and (prior
225+
to PS 5.0) cannot be suppressed, captured, or
226+
redirected. Instead, use Write-Output, Write-Verbose,
227+
or Write-Information.
228+
PSUseSingularNouns Warning 1 The cmdlet 'Get-Widgets' uses a plural noun. A
229+
singular noun should be used instead.
220230
```
221231

222232
When you use the **ScriptDefinition** parameter, the **FileName** property of the
@@ -513,7 +523,7 @@ following keys:
513523

514524
The keys and values in the profile are interpreted as if they were standard parameters and values of
515525
`Invoke-ScriptAnalyzer`, similar to splatting. For more information, see
516-
[about_Splatting](https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_splatting).
526+
[about_Splatting](/powershell/module/microsoft.powershell.core/about/about_splatting).
517527

518528
```yaml
519529
Type: Object
@@ -541,11 +551,9 @@ Valid values are:
541551

542552
You can specify one or more severity values.
543553

544-
The parameter filters the rules violations only after running all rules. To filter rules
545-
efficiently, use `Get-ScriptAnalyzerRule` to select the rules you want to run.
546-
547-
The **Severity** parameter takes precedence over **IncludeRule**. For example, if **Severity** is
548-
`Error`, you cannot use **IncludeRule** to include a `Warning` rule.
554+
The parameter filters the rule violation output only after running all rules. It doesn't filter
555+
which rules are run. To filter rules efficiently, use `Get-ScriptAnalyzerRule` to select the rules
556+
you want to run.
549557

550558
```yaml
551559
Type: String[]

0 commit comments

Comments
 (0)