GeezerVison is showing github.com — PSUseConsistentIndentation double-indents attribute bodies that open a scriptblock (`[Attr({ … })]`) · Issue #2216 · PowerShell/PSScriptAnalyzer
PSUseConsistentIndentation double-indents attribute bodies that open a scriptblock ([Attr({ … })]) #2216DescriptionPrerequisites
Summary
PSUseConsistentIndentation (and therefore Invoke-Formatter) double-indents the body of an attribute that opens a scriptblock, e.g. [ArgumentCompleter({...})]. The body gets IndentationSize * 2 and the closing })] gets IndentationSize * 1.
This looks like the same root cause as #2159 (hashtable inside a method call: the LParen and the AtCurly each add an indentation level). Here the two adjacent openers are ( and { from a type/attribute literal instead of @{, so [AttributeName({ hits it too. #2159 was fixed on main by #2173 ("only the last unclosed opener on a line affects indentation"), but neither that fix nor #2159 covers the attribute form, and it is still broken in the latest release 1.25.0.
Steps to reproduce
$code = @'
[ArgumentCompleter({
Param($commandName, $parameterName)
$validKeys = @('a', 'b')
$validKeys | Where-Object { $_ } | ForEach-Object { "$_=" }
})]
'@
$settings = @{
IncludeRules = @('PSUseConsistentIndentation')
Rules = @{
PSUseConsistentIndentation = @{
Enable = $true
IndentationSize = 4
Kind = 'tab'
}
}
}
Invoke-Formatter -ScriptDefinition $code -Settings $settings
Expected behavior
One level of indentation for the body, matching the opener line, and the closing })] at the opener's level:
[ArgumentCompleter({
Param($commandName, $parameterName)
$validKeys = @('a', 'b')
$validKeys | Where-Object { $_ } | ForEach-Object { "$_=" }
})]
Actual behavior
Two levels for the body and one level for the closing })]:
[ArgumentCompleter({
Param($commandName, $parameterName)
$validKeys = @('a', 'b')
$validKeys | Where-Object { $_ } | ForEach-Object { "$_=" }
})]
The same happens with spaces (Kind = 'space', IndentationSize = 4) and with NewLineAfterOpenBrace/PSPlaceOpenBrace not involved at all — PSUseConsistentIndentation alone is enough.
It also affects other attribute forms that open a scriptblock, e.g.
[ValidateScript({
$_ -gt 0
})]
and the buggy output is not idempotent in a harmless way: because the opener line itself is not re-indented, re-running the formatter keeps the body at the wrong level, and any tool that re-applies formatting sees a permanent diff.
Environment
PSVersion: 7.6.6
PSEdition: Core
OS: Windows 11 (26100)
PSScriptAnalyzer: 1.25.0
Also reproduced with PSScriptAnalyzer 1.24.0 (bundled with ms-vscode.powershell 2025.4.0) and with Windows PowerShell 5.1 / 26100 (Desktop edition), so it is not host- or edition-specific.
Related
Reacted by sharpchen Reacted by Copilot Activitysteve02081504 commented on Sep 18, 2026 Follow-up with the exact repro outputs, to make the fix easy to verify.
1.25.0 (latest release, pwsh 7.6.6, Kind = 'tab', IndentationSize = 4):
>[ArgumentCompleter({
><TAB><TAB>Param($commandName, $parameterName)
><TAB><TAB>$validKeys = @('a', 'b')
><TAB><TAB>$validKeys | Where-Object { $_ } | ForEach-Object { "$_=" }
><TAB>})]
1.24.0 (bundled with ms-vscode.powershell 2025.4.0, pwsh 7.6.6): identical output.
Windows PowerShell 5.1 (Desktop, 26100) + PSScriptAnalyzer 1.25.0: identical output.
So: not host-specific, not edition-specific, present in both the current release and the previous one.
Why this matters / workaround note for other tools
Because the opener line itself is not re-indented, the output is stable on re-format but permanently offset, so any tool that runs Invoke-Formatter and then expects the repository's canonical style will produce a diff on every format. In ps12exe we had to post-process exactly this signature (body at opener+2 levels, closing at opener+1) to restore the intended indentation — that is a workaround, not a fix; the right place is the rule's LParen/XCurly accounting.
Status relative to #2159 / #2173
#2173's approach ("only the last unclosed opener on a line affects indentation") is exactly what should fix the attribute form too, since here ( is also followed by { on the same line. It is merged on main, but the attribute case still reproduces, so the fix either doesn't cover the type/attribute literal path or was never exercised by a test with an attribute opening a scriptblock. Adding a regression test with [ArgumentCompleter({ / [ValidateScript({ would have caught it. steve02081504 commented on Sep 18, 2026 Additional reproductions showing the same LParen + scriptblock opener double count outside attributes, on the latest release (PSScriptAnalyzer 1.25.0, pwsh 7.6.6, Windows 11; identical with Kind = 'tab' and Kind = 'space'):
$x = (1..3 | ForEach-Object {
$_
})
$list.Add([PSCustomObject]@{
Name = "x"
})
$button.add_Click({
$x
})
Actual (the body and the closing line are each pushed down by the number of parentheses still open before the opener):
$x = (1..3 | ForEach-Object {
$_
})
$list.Add([PSCustomObject]@{
Name = "x"
})
$button.add_Click({
$x
})
Expected:
$x = (1..3 | ForEach-Object {
$_
})
$list.Add([PSCustomObject]@{
Name = "x"
})
$button.add_Click({
$x
})
The number of extra levels equals the number of unclosed ( before the opener, so the double-parenthesized form shifts by two levels:
$str += (($obj.GetEnumerator() | ForEach-Object {
$_
}) -join '')
A backtick continuation line that starts while a parenthesis is still open is affected the same way, so this is not limited to scriptblock/hashtable openers:
Write-Host ("{0}: {1}" -f `
$Name, $Value)
#2173's "only the last unclosed opener on a line affects indentation" rule would cover all of these, because the scriptblock {/@{ (or the backtick continuation) is the last unclosed opener on the line.
Workaround / what to delete after the fix
ps12exe post-processes exactly this signature (restoreParenIndentation in
src/.subrepo/vscode-plug/ps12exe/lib/preprocessor.mjs): when the first body line
is at opener + N + 1 levels and the closing line at opener + N levels (N =
unclosed parentheses before the opener), it pulls the region back N levels. It
is a workaround, not a fix — once this is fixed upstream that function, its test
and the AGENTS.md row can be deleted.
bergmeister commented on Oct 28, 2026 As I asked Copilot to fix it, it turns out #2173 has already fixed this issue so you can expect it to be fixed in next 1.26.0 release, which I am keen to do sooner if Microsoft has time once we've merged most of the newer PRs
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|