Skip to content

PSUseConsistentIndentation: Hashtable inside method call gets double-indented #2159

Description

@quasarea

Summary

PSUseConsistentIndentation incorrectly indents hashtable body content when the hashtable is inside a method call parenthesis (e.g., .Add(@{ ... })). The formatter treats the @{ opening as an additional indentation level relative to the method call position, producing excessive indentation instead of indenting consistently from the line start.

This was originally reported as PowerShell/vscode-powershell#5397, where a maintainer confirmed it belongs in PSScriptAnalyzer. That issue was auto-closed awaiting author feedback.

Steps to Reproduce

# PSScriptAnalyzer 1.24.0 / PowerShell 7.5.4

# Minimal repro - only PSUseConsistentIndentation enabled
$code = @'
$list.Add([PSCustomObject]@{
    Name = "Test"
    Value = 123
})
'@

$settings = @{
    IncludeRules = @("PSUseConsistentIndentation")
    Rules = @{
        PSUseConsistentIndentation = @{
            Enable = $true
            IndentationSize = 4
            Kind = "space"
        }
    }
}

Invoke-Formatter -ScriptDefinition $code -Settings $settings

Expected Behavior

Hashtable properties should be indented by one level (4 spaces) from the line start, regardless of where @{ appears on the line:

$list.Add([PSCustomObject]@{
    Name = "Test"
    Value = 123
})

Actual Behavior

Properties are indented to the @{ column position (8 spaces), and the closing }) gets its own indentation level too:

$list.Add([PSCustomObject]@{
        Name = "Test"
        Value = 123
    })

Additional Test Cases

The problem compounds with deeper nesting:

# Input:
foreach ($item in $items) {
    $results.Add([PSCustomObject]@{
        Name = $item.Name
        Value = $item.Value
        Status = "OK"
    })
}

# Formatted (actual) - 12 spaces for hashtable body:
foreach ($item in $items) {
    $results.Add([PSCustomObject]@{
            Name = $item.Name
            Value = $item.Value
            Status = "OK"
        })
}

# Expected - 8 spaces (one level deeper than the foreach body):
foreach ($item in $items) {
    $results.Add([PSCustomObject]@{
        Name = $item.Name
        Value = $item.Value
        Status = "OK"
    })
}

Standalone hashtables (not inside method calls) format correctly:

# This formats correctly with 4-space indent:
$obj = [PSCustomObject]@{
    Name = "Test"
    Value = 123
}

Root Cause Analysis

PSUseConsistentIndentation appears to count ( as an indentation-increasing token, then also counts @{ as another level. For standalone hashtables like $x = @{, the = doesn't increase indentation so only @{ counts — producing 4 spaces correctly. But inside .Add(@{, the ( adds one level and @{ adds another, producing 8 spaces (2 × 4).

Impact

This is a common PowerShell pattern — building lists of [PSCustomObject] via .Add() is idiomatic. The excessive indentation forces a choice between:

  1. Disabling PSUseConsistentIndentation entirely
  2. Accepting unintuitively deep indentation
  3. Disabling powershell.codeFormatting.alignPropertyValuePairs (which also disables desirable alignment on standalone hashtables)
  4. Disabling format-on-save for PowerShell files

When combined with PSAlignAssignmentStatement.CheckHashtable, the problem is amplified because value alignment is calculated from the already-wrong base indentation.

Workaround

Setting powershell.codeFormatting.alignPropertyValuePairs: false in VS Code mitigates the worst visual impact but also disables alignment on standalone hashtables where it's desirable.

Environment

  • PSScriptAnalyzer: 1.24.0
  • PowerShell: 7.5.4
  • OS: Linux (also reported on Windows 11 in vscode-powershell#5397)

Activity

  1. liamjpeters commented on Mar 5, 2026

    @liamjpeters
    Contributor

    You're correct in your findings that that indentation level in this case is incremented twice. Once because of the LParen, and once again because of the AtCurly.

    This is a really tricky one to get right. "Fixing" it to one person's preference will almost certainly break it for others.

    Consider your example.

    $list.Add([PSCustomObject]@{
            Name = "Test"
            Value = 123
        })

    We could add in a check when considering an LParen to make sure it's the last "Indent-affecting" token on that line. If it isn't then don't increase the indent level (and don't decrease the indent level for it's corresponding RParen).

    Something like this.

    Doing so formats your example to your (and my) preference:

    $list.Add([PSCustomObject]@{
        Name = "Test"
        Value = 123
    })

    However, consider if you'd formatted your example like so (LParen and AtCurly on the same line, closing on different lines):

    $list.Add([PSCustomObject]@{
            Name = "Test"
            Value = 123
        }
    )

    Current formatting leaves that alone, however with the above change this would become:

    $list.Add([PSCustomObject]@{
        Name = "Test"
        Value = 123
    }
    )

    Which looks odd, to say the least.

    I'm not sure that there's a "fix" for this that pleases everyone.

  2. bergmeister commented on Mar 31, 2026

    @bergmeister
    Collaborator

    yes, it's a difficult one to get logic right in all cases due to rules being token based and basically just counting indentation levels. Would accept PR if you can tweak behavior and tests still pass :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions