From ab4a69559131ef8c7e1a14da1e09bf43f06d9976 Mon Sep 17 00:00:00 2001 From: Liam Peters Date: Thu, 2 Apr 2026 10:44:13 +0100 Subject: [PATCH 1/2] Improve pipeline indentation handling in UseConsistentIndentation rule --- Rules/UseConsistentIndentation.cs | 76 +++- .../Rules/UseConsistentIndentation.tests.ps1 | 364 ++++++++++++++++++ 2 files changed, 431 insertions(+), 9 deletions(-) diff --git a/Rules/UseConsistentIndentation.cs b/Rules/UseConsistentIndentation.cs index 41aa4ef4d..1a01a22fe 100644 --- a/Rules/UseConsistentIndentation.cs +++ b/Rules/UseConsistentIndentation.cs @@ -130,9 +130,21 @@ public override IEnumerable AnalyzeScript(Ast ast, string file var tokens = Helper.Instance.Tokens; var diagnosticRecords = new List(); var indentationLevel = 0; - var currentIndenationLevelIncreaseDueToPipelines = 0; var onNewLine = true; var pipelineAsts = ast.FindAll(testAst => testAst is PipelineAst && (testAst as PipelineAst).PipelineElements.Count > 1, true).ToList(); + // Sort by end position so that inner (nested) pipelines appear before outer ones. + // This is required by MatchingPipelineAstEnd, whose early-break optimization + // would otherwise skip nested pipelines that end before their outer pipeline. + pipelineAsts.Sort((a, b) => + { + int lineCmp = a.Extent.EndScriptPosition.LineNumber.CompareTo(b.Extent.EndScriptPosition.LineNumber); + return lineCmp != 0 ? lineCmp : a.Extent.EndScriptPosition.ColumnNumber.CompareTo(b.Extent.EndScriptPosition.ColumnNumber); + }); + // Track pipeline indentation increases per PipelineAst instead of as a single + // flat counter. A flat counter caused all accumulated pipeline indentation to be + // subtracted when *any* pipeline ended, instead of only the contribution from + // that specific pipeline — leading to runaway indentation with nested pipelines. + var pipelineIndentationIncreases = new Dictionary(); /* When an LParen and LBrace are on the same line, it can lead to too much de-indentation. In order to prevent the RParen code from de-indenting too much, we keep a stack of when we skipped the indentation @@ -188,18 +200,33 @@ caused by tokens that require a closing RParen (which are LParen, AtParen and Do if (pipelineIndentationStyle == PipelineIndentationStyle.IncreaseIndentationAfterEveryPipeline) { AddViolation(token, indentationLevel++, diagnosticRecords, ref onNewLine); - currentIndenationLevelIncreaseDueToPipelines++; + // Attribute this increase to the innermost pipeline containing + // this pipe token so it is only reversed when that specific + // pipeline ends, not when an unrelated outer pipeline ends. + PipelineAst containingPipeline = FindInnermostContainingPipeline(pipelineAsts, token); + if (containingPipeline != null) + { + if (!pipelineIndentationIncreases.ContainsKey(containingPipeline)) + pipelineIndentationIncreases[containingPipeline] = 0; + pipelineIndentationIncreases[containingPipeline]++; + } break; } if (pipelineIndentationStyle == PipelineIndentationStyle.IncreaseIndentationForFirstPipeline) { - bool isFirstPipeInPipeline = pipelineAsts.Any(pipelineAst => - PositionIsEqual(LastPipeOnFirstLineWithPipeUsage((PipelineAst)pipelineAst).Extent.EndScriptPosition, - tokens[tokenIndex - 1].Extent.EndScriptPosition)); - if (isFirstPipeInPipeline) + // Capture which specific PipelineAst this is the first pipe for, + // so the indentation increase is attributed to that pipeline only. + PipelineAst firstPipePipeline = pipelineAsts + .Cast() + .FirstOrDefault(pipelineAst => + PositionIsEqual(LastPipeOnFirstLineWithPipeUsage(pipelineAst).Extent.EndScriptPosition, + tokens[tokenIndex - 1].Extent.EndScriptPosition)); + if (firstPipePipeline != null) { AddViolation(token, indentationLevel++, diagnosticRecords, ref onNewLine); - currentIndenationLevelIncreaseDueToPipelines++; + if (!pipelineIndentationIncreases.ContainsKey(firstPipePipeline)) + pipelineIndentationIncreases[firstPipePipeline] = 0; + pipelineIndentationIncreases[firstPipePipeline]++; } } break; @@ -290,8 +317,13 @@ caused by tokens that require a closing RParen (which are LParen, AtParen and Do if (pipelineIndentationStyle == PipelineIndentationStyle.IncreaseIndentationForFirstPipeline || pipelineIndentationStyle == PipelineIndentationStyle.IncreaseIndentationAfterEveryPipeline) { - indentationLevel = ClipNegative(indentationLevel - currentIndenationLevelIncreaseDueToPipelines); - currentIndenationLevelIncreaseDueToPipelines = 0; + // Only subtract the indentation contributed by this specific pipeline, + // leaving contributions from outer/unrelated pipelines intact. + if (pipelineIndentationIncreases.TryGetValue(matchingPipeLineAstEnd, out int contribution)) + { + indentationLevel = ClipNegative(indentationLevel - contribution); + pipelineIndentationIncreases.Remove(matchingPipeLineAstEnd); + } } } @@ -432,6 +464,32 @@ private static PipelineAst MatchingPipelineAstEnd(List pipelineAsts, Token return matchingPipeLineAstEnd; } + /// + /// Finds the innermost (smallest) PipelineAst whose extent fully contains the given token. + /// Used to attribute pipeline indentation increases to the correct pipeline when + /// using IncreaseIndentationAfterEveryPipeline. + /// + private static PipelineAst FindInnermostContainingPipeline(List pipelineAsts, Token token) + { + PipelineAst best = null; + int bestSize = int.MaxValue; + foreach (var ast in pipelineAsts) + { + var pipeline = (PipelineAst)ast; + int pipelineStart = pipeline.Extent.StartOffset; + int pipelineEnd = pipeline.Extent.EndOffset; + int pipelineSize = pipelineEnd - pipelineStart; + if (pipelineStart <= token.Extent.StartOffset && + token.Extent.EndOffset <= pipelineEnd && + pipelineSize < bestSize) + { + best = pipeline; + bestSize = pipelineSize; + } + } + return best; + } + private static bool PositionIsEqual(IScriptPosition position1, IScriptPosition position2) { return position1.ColumnNumber == position2.ColumnNumber && diff --git a/Tests/Rules/UseConsistentIndentation.tests.ps1 b/Tests/Rules/UseConsistentIndentation.tests.ps1 index 0d26ff39d..d550be5d9 100644 --- a/Tests/Rules/UseConsistentIndentation.tests.ps1 +++ b/Tests/Rules/UseConsistentIndentation.tests.ps1 @@ -549,6 +549,370 @@ foo | } } + Context "When a nested multi-line pipeline is inside a pipelined script block" { + + It "Should preserve indentation with nested pipeline using " -TestCases @( + @{ + PipelineIndentation = 'IncreaseIndentationForFirstPipeline' + IdempotentScriptDefinition = @' +$Test | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } +'@ + } + @{ + PipelineIndentation = 'IncreaseIndentationAfterEveryPipeline' + IdempotentScriptDefinition = @' +$Test | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } +'@ + } + @{ + PipelineIndentation = 'NoIndentation' + IdempotentScriptDefinition = @' +$Test | +ForEach-Object { + Get-Process | + Select-Object -Last 1 +} +'@ + } + @{ + PipelineIndentation = 'None' + IdempotentScriptDefinition = @' +$Test | + ForEach-Object { + Get-Process | + Select-Object -Last 1 +} +'@ + } + ) { + param ($PipelineIndentation, $IdempotentScriptDefinition) + + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = $PipelineIndentation + Invoke-Formatter -ScriptDefinition $IdempotentScriptDefinition -Settings $settings | Should -Be $IdempotentScriptDefinition + } + + It "Should recover indentation after nested pipeline block using " -TestCases @( + @{ + PipelineIndentation = 'IncreaseIndentationForFirstPipeline' + IdempotentScriptDefinition = @' +function foo { + $Test | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } + $thisLineShouldBeAtOneIndent +} +'@ + } + @{ + PipelineIndentation = 'IncreaseIndentationAfterEveryPipeline' + IdempotentScriptDefinition = @' +function foo { + $Test | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } + $thisLineShouldBeAtOneIndent +} +'@ + } + @{ + PipelineIndentation = 'NoIndentation' + IdempotentScriptDefinition = @' +function foo { + $Test | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } + $thisLineShouldBeAtOneIndent +} +'@ + } + @{ + PipelineIndentation = 'None' + IdempotentScriptDefinition = @' +function foo { + $Test | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } + $thisLineShouldBeAtOneIndent +} +'@ + } + ) { + param ($PipelineIndentation, $IdempotentScriptDefinition) + + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = $PipelineIndentation + Invoke-Formatter -ScriptDefinition $IdempotentScriptDefinition -Settings $settings | Should -Be $IdempotentScriptDefinition + } + + It "Should handle multiple sequential nested pipeline blocks using " -TestCases @( + @{ + PipelineIndentation = 'IncreaseIndentationForFirstPipeline' + IdempotentScriptDefinition = @' +function foo { + $a | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } + $b | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } + $stillCorrect +} +'@ + } + @{ + PipelineIndentation = 'IncreaseIndentationAfterEveryPipeline' + IdempotentScriptDefinition = @' +function foo { + $a | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } + $b | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } + $stillCorrect +} +'@ + } + @{ + PipelineIndentation = 'NoIndentation' + IdempotentScriptDefinition = @' +function foo { + $a | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } + $b | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } + $stillCorrect +} +'@ + } + @{ + PipelineIndentation = 'None' + IdempotentScriptDefinition = @' +function foo { + $a | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } + $b | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } + $stillCorrect +} +'@ + } + ) { + param ($PipelineIndentation, $IdempotentScriptDefinition) + + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = $PipelineIndentation + Invoke-Formatter -ScriptDefinition $IdempotentScriptDefinition -Settings $settings | Should -Be $IdempotentScriptDefinition + } + + It "Should handle inner pipeline with 3+ elements using " -TestCases @( + @{ + PipelineIndentation = 'IncreaseIndentationForFirstPipeline' + IdempotentScriptDefinition = @' +$Test | + ForEach-Object { + Get-Process | + Where-Object Path | + Select-Object -Last 1 + } +'@ + } + @{ + PipelineIndentation = 'IncreaseIndentationAfterEveryPipeline' + IdempotentScriptDefinition = @' +$Test | + ForEach-Object { + Get-Process | + Where-Object Path | + Select-Object -Last 1 + } +'@ + } + @{ + PipelineIndentation = 'NoIndentation' + IdempotentScriptDefinition = @' +$Test | +ForEach-Object { + Get-Process | + Where-Object Path | + Select-Object -Last 1 +} +'@ + } + @{ + PipelineIndentation = 'None' + IdempotentScriptDefinition = @' +$Test | + ForEach-Object { + Get-Process | + Where-Object Path | + Select-Object -Last 1 +} +'@ + } + ) { + param ($PipelineIndentation, $IdempotentScriptDefinition) + + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = $PipelineIndentation + Invoke-Formatter -ScriptDefinition $IdempotentScriptDefinition -Settings $settings | Should -Be $IdempotentScriptDefinition + } + + It "Should handle outer pipeline on same line as command using " -TestCases @( + @{ + PipelineIndentation = 'IncreaseIndentationForFirstPipeline' + IdempotentScriptDefinition = @' +$Test | ForEach-Object { + Get-Process | + Select-Object -Last 1 +} +'@ + } + @{ + PipelineIndentation = 'IncreaseIndentationAfterEveryPipeline' + IdempotentScriptDefinition = @' +$Test | ForEach-Object { + Get-Process | + Select-Object -Last 1 +} +'@ + } + @{ + PipelineIndentation = 'NoIndentation' + IdempotentScriptDefinition = @' +$Test | ForEach-Object { + Get-Process | + Select-Object -Last 1 +} +'@ + } + @{ + PipelineIndentation = 'None' + IdempotentScriptDefinition = @' +$Test | ForEach-Object { + Get-Process | + Select-Object -Last 1 +} +'@ + } + ) { + param ($PipelineIndentation, $IdempotentScriptDefinition) + + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = $PipelineIndentation + Invoke-Formatter -ScriptDefinition $IdempotentScriptDefinition -Settings $settings | Should -Be $IdempotentScriptDefinition + } + + It "Should handle deeply nested pipelines (3 levels) using " -TestCases @( + @{ + PipelineIndentation = 'IncreaseIndentationForFirstPipeline' + IdempotentScriptDefinition = @' +$a | + ForEach-Object { + $b | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } + } +'@ + } + @{ + PipelineIndentation = 'IncreaseIndentationAfterEveryPipeline' + IdempotentScriptDefinition = @' +$a | + ForEach-Object { + $b | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } + } +'@ + } + @{ + PipelineIndentation = 'NoIndentation' + IdempotentScriptDefinition = @' +$a | +ForEach-Object { + $b | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } +} +'@ + } + @{ + PipelineIndentation = 'None' + IdempotentScriptDefinition = @' +$a | + ForEach-Object { + $b | + ForEach-Object { + Get-Process | + Select-Object -Last 1 + } +} +'@ + } + ) { + param ($PipelineIndentation, $IdempotentScriptDefinition) + + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = $PipelineIndentation + Invoke-Formatter -ScriptDefinition $IdempotentScriptDefinition -Settings $settings | Should -Be $IdempotentScriptDefinition + } + + It "Should handle single-line inner pipeline inside multi-line outer pipeline using " -TestCases @( + @{ PipelineIndentation = 'IncreaseIndentationForFirstPipeline' } + @{ PipelineIndentation = 'IncreaseIndentationAfterEveryPipeline' } + @{ PipelineIndentation = 'NoIndentation' } + @{ PipelineIndentation = 'None' } + ) { + param ($PipelineIndentation) + + $idempotentScriptDefinition = @' +$Test | ForEach-Object { + Get-Process | Select-Object -Last 1 +} +'@ + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = $PipelineIndentation + Invoke-Formatter -ScriptDefinition $IdempotentScriptDefinition -Settings $settings | Should -Be $IdempotentScriptDefinition + } + } + Context "When tabs instead of spaces are used for indentation" { BeforeEach { $settings.Rules.PSUseConsistentIndentation.Kind = 'tab' From d89d877f317e53fdc3d2d13ec6c036aa6d1e64d4 Mon Sep 17 00:00:00 2001 From: Liam Peters Date: Wed, 15 Apr 2026 17:22:38 +0100 Subject: [PATCH 2/2] Fix over-indentation when multiple openers appear on the same line --- Rules/UseConsistentIndentation.cs | 111 ++++++++++++----- .../Rules/UseConsistentIndentation.tests.ps1 | 116 ++++++++++++++++++ 2 files changed, 196 insertions(+), 31 deletions(-) diff --git a/Rules/UseConsistentIndentation.cs b/Rules/UseConsistentIndentation.cs index 1a01a22fe..2c77787c6 100644 --- a/Rules/UseConsistentIndentation.cs +++ b/Rules/UseConsistentIndentation.cs @@ -143,15 +143,14 @@ public override IEnumerable AnalyzeScript(Ast ast, string file // Track pipeline indentation increases per PipelineAst instead of as a single // flat counter. A flat counter caused all accumulated pipeline indentation to be // subtracted when *any* pipeline ended, instead of only the contribution from - // that specific pipeline — leading to runaway indentation with nested pipelines. + // that specific pipeline - leading to runaway indentation with nested pipelines. var pipelineIndentationIncreases = new Dictionary(); - /* - When an LParen and LBrace are on the same line, it can lead to too much de-indentation. - In order to prevent the RParen code from de-indenting too much, we keep a stack of when we skipped the indentation - caused by tokens that require a closing RParen (which are LParen, AtParen and DollarParen). - */ - var lParenSkippedIndentation = new Stack(); - + // When multiple openers appear on the same line (e.g. ({ or @(@{), + // only the last unclosed opener should affect indentation. We + // track, for every opener, whether its indentation increment was + // skipped so that the matching closer knows not to decrement. + var openerSkippedIndentation = new Stack(); + for (int tokenIndex = 0; tokenIndex < tokens.Length; tokenIndex++) { var token = tokens[tokenIndex]; @@ -165,27 +164,39 @@ caused by tokens that require a closing RParen (which are LParen, AtParen and Do { case TokenKind.AtCurly: case TokenKind.LCurly: - AddViolation(token, indentationLevel++, diagnosticRecords, ref onNewLine); - break; - case TokenKind.DollarParen: case TokenKind.AtParen: - lParenSkippedIndentation.Push(false); - AddViolation(token, indentationLevel++, diagnosticRecords, ref onNewLine); + AddViolation(token, indentationLevel, diagnosticRecords, ref onNewLine); + if (HasUnclosedOpenerBeforeLineEnd(tokens, tokenIndex)) + { + openerSkippedIndentation.Push(true); + } + else + { + indentationLevel++; + openerSkippedIndentation.Push(false); + } break; case TokenKind.LParen: AddViolation(token, indentationLevel, diagnosticRecords, ref onNewLine); - // When a line starts with a parenthesis and it is not the last non-comment token of that line, - // then indentation does not need to be increased. + // When a line starts with a parenthesis and it is not the + // last non-comment token of that line, indentation does + // not need to be increased. if ((tokenIndex == 0 || tokens[tokenIndex - 1].Kind == TokenKind.NewLine) && NextTokenIgnoringComments(tokens, tokenIndex)?.Kind != TokenKind.NewLine) { - onNewLine = false; - lParenSkippedIndentation.Push(true); + openerSkippedIndentation.Push(true); + break; + } + // General case: skip when another opener follows so that + // only the last unclosed opener on a line is indent-affecting. + if (HasUnclosedOpenerBeforeLineEnd(tokens, tokenIndex)) + { + openerSkippedIndentation.Push(true); break; } - lParenSkippedIndentation.Push(false); + openerSkippedIndentation.Push(false); indentationLevel++; break; @@ -232,23 +243,18 @@ caused by tokens that require a closing RParen (which are LParen, AtParen and Do break; case TokenKind.RParen: - bool matchingLParenIncreasedIndentation = false; - if (lParenSkippedIndentation.Count > 0) + case TokenKind.RCurly: + if (openerSkippedIndentation.Count > 0 && openerSkippedIndentation.Pop()) { - matchingLParenIncreasedIndentation = lParenSkippedIndentation.Pop(); + // The matching opener skipped its increment, so we + // skip the decrement but still enforce indentation. + AddViolation(token, indentationLevel, diagnosticRecords, ref onNewLine); } - if (matchingLParenIncreasedIndentation) + else { - onNewLine = false; - break; + indentationLevel = ClipNegative(indentationLevel - 1); + AddViolation(token, indentationLevel, diagnosticRecords, ref onNewLine); } - indentationLevel = ClipNegative(indentationLevel - 1); - AddViolation(token, indentationLevel, diagnosticRecords, ref onNewLine); - break; - - case TokenKind.RCurly: - indentationLevel = ClipNegative(indentationLevel - 1); - AddViolation(token, indentationLevel, diagnosticRecords, ref onNewLine); break; case TokenKind.NewLine: @@ -330,6 +336,49 @@ caused by tokens that require a closing RParen (which are LParen, AtParen and Do return diagnosticRecords; } + /// + /// Scans forward from the current opener to the end of the line. + /// Returns true if there is at least one unclosed opener when + /// the line ends, meaning the current opener should skip its + /// indentation increment. If the current opener's own closer + /// is found on the same line (depth drops below zero), returns + /// false so that it indents normally. + /// + private static bool HasUnclosedOpenerBeforeLineEnd(Token[] tokens, int currentIndex) + { + int depth = 0; + for (int i = currentIndex + 1; i < tokens.Length; i++) + { + switch (tokens[i].Kind) + { + case TokenKind.NewLine: + case TokenKind.LineContinuation: + case TokenKind.EndOfInput: + return depth > 0; + + case TokenKind.LCurly: + case TokenKind.AtCurly: + case TokenKind.LParen: + case TokenKind.AtParen: + case TokenKind.DollarParen: + depth++; + break; + + case TokenKind.RCurly: + case TokenKind.RParen: + depth--; + if (depth < 0) + { + // Our own closer was found on this line. + return false; + } + break; + } + } + + return depth > 0; + } + private static Token NextTokenIgnoringComments(Token[] tokens, int startIndex) { if (startIndex >= tokens.Length - 1) diff --git a/Tests/Rules/UseConsistentIndentation.tests.ps1 b/Tests/Rules/UseConsistentIndentation.tests.ps1 index d550be5d9..6bf241116 100644 --- a/Tests/Rules/UseConsistentIndentation.tests.ps1 +++ b/Tests/Rules/UseConsistentIndentation.tests.ps1 @@ -913,6 +913,122 @@ $Test | ForEach-Object { } } + Context "When multiple openers appear on the same line" { + It "Should not double-indent for paren-then-brace: .foreach({" { + $def = @' +@('a', 'b').foreach({ + $_.ToUpper() + }) +'@ + $expected = @' +@('a', 'b').foreach({ + $_.ToUpper() +}) +'@ + Invoke-Formatter -ScriptDefinition $def -Settings $settings | Should -Be $expected + } + + It "Should not double-indent for brace-then-paren: {(" { + $def = @' +@('a', 'b').foreach({( + $_.ToUpper() + )}) +'@ + $expected = @' +@('a', 'b').foreach({( + $_.ToUpper() +)}) +'@ + Invoke-Formatter -ScriptDefinition $def -Settings $settings | Should -Be $expected + } + + It "Should not double-indent for array-then-hashtable on same line: @(@{" { + $idempotentScriptDefinition = @' +$x = @(@{ + key = 'value' +}) +'@ + Invoke-Formatter -ScriptDefinition $idempotentScriptDefinition -Settings $settings | Should -Be $idempotentScriptDefinition + } + + It "Should not double-indent when non-opener tokens separate openers: ([PSCustomObject]@{" { + $def = @' +$list.Add([PSCustomObject]@{ + Name = "Test" + Value = 123 + }) +'@ + $expected = @' +$list.Add([PSCustomObject]@{ + Name = "Test" + Value = 123 +}) +'@ + Invoke-Formatter -ScriptDefinition $def -Settings $settings | Should -Be $expected + } + + It "Should indent normally when all openers are closed on the same line" { + $idempotentScriptDefinition = @' +$list.Add([PSCustomObject]@{Name = "Test"; Value = 123}) +'@ + Invoke-Formatter -ScriptDefinition $idempotentScriptDefinition -Settings $settings | Should -Be $idempotentScriptDefinition + } + + It "Should handle closing brace and paren on separate lines" { + $def = @' +@('a', 'b').foreach({ + $_.ToUpper() + } + ) +'@ + $expected = @' +@('a', 'b').foreach({ + $_.ToUpper() +} +) +'@ + Invoke-Formatter -ScriptDefinition $def -Settings $settings | Should -Be $expected + } + + It "Should handle nested .foreach({ }) calls" { + $def = @' +@(1, 2).foreach({ +@('a', 'b').foreach({ +"$_ and $_" +}) +}) +'@ + $expected = @' +@(1, 2).foreach({ + @('a', 'b').foreach({ + "$_ and $_" + }) +}) +'@ + Invoke-Formatter -ScriptDefinition $def -Settings $settings | Should -Be $expected + } + + It "Should still indent each opener separately when on different lines" { + $idempotentScriptDefinition = @' +$x = @( + @{ + key = 'value' + } +) +'@ + Invoke-Formatter -ScriptDefinition $idempotentScriptDefinition -Settings $settings | Should -Be $idempotentScriptDefinition + } + + It "Should still indent normally for sub-expressions" { + $idempotentScriptDefinition = @' +$( + Get-Process +) +'@ + Invoke-Formatter -ScriptDefinition $idempotentScriptDefinition -Settings $settings | Should -Be $idempotentScriptDefinition + } + } + Context "When tabs instead of spaces are used for indentation" { BeforeEach { $settings.Rules.PSUseConsistentIndentation.Kind = 'tab'