PSUseConsistentIndentation double-indents attribute bodies that open a scriptblock (`[Attr({ … })]`)

Open
#2,216 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
70/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
csharp, powershell
Domain
tooling

Research direction

Start by running the supplied Invoke-Formatter reproduction with PSUseConsistentIndentation enabled, then trace the indentation handling for the PSUseConsistentIndentation rule when an attribute or type literal opens a scriptblock. Add a regression test covering [ArgumentCompleter({ ... })] and [ValidateScript({ ... })], verifying one body indentation level and the closing })] at the opener's level.

Written by the indexing model from the issue text.

Description

Prerequisites
  • I have read the documentation and my issue is not covered there.
  • I have searched the existing issues and my issue is not already reported.
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
  • #2159 — PSUseConsistentIndentation: Hashtable inside method call gets double-indented (same LParen+XCurly double count; fixed by #2173)
  • #2173 — the fix that pops the level when an opener is not the last unclosed opener on the line; it does not catch the attribute opening ({
  • #1168 — "Formatting .where and .foreach methods is incorrect" (adjacent-opener family)
Dominant language
C#
Stars
2.2k
Forks
415
Avg merge
13h 1m
Merged PRs (30d)
2

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from PowerShell/PSScriptAnalyzer

All issues in PowerShell/PSScriptAnalyzer

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.