diff --git a/Rules/UseConsistentIndentation.cs b/Rules/UseConsistentIndentation.cs index 2c77787c6..7fc98509d 100644 --- a/Rules/UseConsistentIndentation.cs +++ b/Rules/UseConsistentIndentation.cs @@ -275,17 +275,25 @@ public override IEnumerable AnalyzeScript(Ast ast, string file ++tempIndentationLevel; } - // check for comments in between multi-line commands with line continuation + // Check for comments in between multi-line commands with line continuation. if (tokenIndex > 2 && tokens[tokenIndex - 1].Kind == TokenKind.NewLine && tokens[tokenIndex - 2].Kind == TokenKind.Comment) { - int searchForPrecedingLineContinuationIndex = tokenIndex - 2; - while (searchForPrecedingLineContinuationIndex > 0 && tokens[searchForPrecedingLineContinuationIndex].Kind == TokenKind.Comment) + int precedingCommentIndex = tokenIndex - 2; + while (precedingCommentIndex > 0 && + tokens[precedingCommentIndex - 1].Kind == TokenKind.Comment && + tokens[precedingCommentIndex - 1].Extent.EndLineNumber >= + tokens[precedingCommentIndex].Extent.StartLineNumber) { - searchForPrecedingLineContinuationIndex--; + precedingCommentIndex--; } - if (searchForPrecedingLineContinuationIndex >= 0 && tokens[searchForPrecedingLineContinuationIndex].Kind == TokenKind.LineContinuation) + GetCommentRunIndentationOffset( + tokens, + precedingCommentIndex, + out bool commentRunStartsAfterLineContinuation, + out _); + if (commentRunStartsAfterLineContinuation) { tempIndentationLevel++; } @@ -298,7 +306,25 @@ public override IEnumerable AnalyzeScript(Ast ast, string file } bool lineHasPipelineBeforeToken = LineHasPipelineBeforeToken(tokens, tokenIndex, token); - AddViolation(token, tempIndentationLevel, diagnosticRecords, ref onNewLine, lineHasPipelineBeforeToken); + int commentIndentationOffset = 0; + if (token.Kind == TokenKind.Comment) + { + commentIndentationOffset = GetCommentRunIndentationOffset( + tokens, + tokenIndex, + out bool commentRunStartsAfterLineContinuation, + out bool commentRunStartsAfterPipeline); + if (pipelineIndentationStyle == PipelineIndentationStyle.None && + commentRunStartsAfterPipeline) + { + onNewLine = false; + continue; + } + tempIndentationLevel = indentationLevel + + (commentRunStartsAfterLineContinuation ? 1 : 0); + } + + AddViolation(token, tempIndentationLevel, diagnosticRecords, ref onNewLine, lineHasPipelineBeforeToken, commentIndentationOffset); } break; } @@ -336,6 +362,187 @@ public override IEnumerable AnalyzeScript(Ast ast, string file return diagnosticRecords; } + private static int GetCommentRunIndentationOffset( + Token[] tokens, + int tokenIndex, + out bool commentRunStartsAfterLineContinuation, + out bool commentRunStartsAfterPipeline) + { + commentRunStartsAfterLineContinuation = false; + commentRunStartsAfterPipeline = false; + if (HasCodeAfterCommentOnSameLine(tokens, tokenIndex)) + { + commentRunStartsAfterLineContinuation = + tokenIndex > 0 && + tokens[tokenIndex - 1].Kind == TokenKind.LineContinuation; + int precedingCommentIndex = tokenIndex - 1; + if (precedingCommentIndex >= 0 && + (tokens[precedingCommentIndex].Kind == TokenKind.NewLine || + tokens[precedingCommentIndex].Kind == TokenKind.LineContinuation)) + { + precedingCommentIndex--; + if (precedingCommentIndex >= 0 && + tokens[precedingCommentIndex].Kind == TokenKind.Comment) + { + while (precedingCommentIndex > 0 && + tokens[precedingCommentIndex - 1].Kind == TokenKind.Comment && + tokens[precedingCommentIndex - 1].Extent.EndLineNumber >= + tokens[precedingCommentIndex].Extent.StartLineNumber) + { + precedingCommentIndex--; + } + + GetCommentRunIndentationOffset( + tokens, + precedingCommentIndex, + out bool precedingCommentRunStartsAfterLineContinuation, + out commentRunStartsAfterPipeline); + commentRunStartsAfterLineContinuation |= precedingCommentRunStartsAfterLineContinuation; + } + } + + return 0; + } + + if (HasCodeBeforeCommentOnSameLine(tokens, tokenIndex)) + { + return 0; + } + + int firstCommentIndex = tokenIndex; + int currentLine = tokens[tokenIndex].Extent.StartLineNumber; + int precedingIndex = tokenIndex - 1; + while (precedingIndex >= 0 && + tokens[precedingIndex].Kind == TokenKind.Comment && + (tokens[precedingIndex].Extent.StartLineNumber == currentLine || + tokens[precedingIndex].Extent.EndLineNumber >= + tokens[firstCommentIndex].Extent.StartLineNumber)) + { + firstCommentIndex = precedingIndex; + precedingIndex--; + } + + currentLine = tokens[firstCommentIndex].Extent.StartLineNumber; + while (precedingIndex >= 0 && + (tokens[precedingIndex].Kind == TokenKind.NewLine || + tokens[precedingIndex].Kind == TokenKind.LineContinuation)) + { + precedingIndex--; + if (precedingIndex < 0 || + tokens[precedingIndex].Kind != TokenKind.Comment || + HasCodeAfterCommentOnSameLine(tokens, precedingIndex) || + HasCodeBeforeCommentOnSameLine(tokens, precedingIndex) || + tokens[precedingIndex].Extent.EndLineNumber != currentLine - 1) + { + break; + } + + int previousCommentLine = tokens[precedingIndex].Extent.StartLineNumber; + int firstCommentOnPreviousLine = precedingIndex; + int precedingCommentIndex = precedingIndex - 1; + while (precedingCommentIndex >= 0 && + tokens[precedingCommentIndex].Kind == TokenKind.Comment && + (tokens[precedingCommentIndex].Extent.StartLineNumber == previousCommentLine || + tokens[precedingCommentIndex].Extent.EndLineNumber >= + tokens[firstCommentOnPreviousLine].Extent.StartLineNumber)) + { + firstCommentOnPreviousLine = precedingCommentIndex; + precedingCommentIndex--; + } + + if (precedingCommentIndex >= 0 && + tokens[precedingCommentIndex].Kind != TokenKind.NewLine && + tokens[precedingCommentIndex].Kind != TokenKind.LineContinuation) + { + break; + } + + firstCommentIndex = firstCommentOnPreviousLine; + currentLine = tokens[firstCommentOnPreviousLine].Extent.StartLineNumber; + precedingIndex = precedingCommentIndex; + } + + commentRunStartsAfterLineContinuation = + firstCommentIndex > 0 && + tokens[firstCommentIndex - 1].Kind == TokenKind.LineContinuation; + commentRunStartsAfterPipeline = + PreviousLineEndedWithPipe(tokens, firstCommentIndex, tokens[firstCommentIndex]); + int leastIndentedColumn = int.MaxValue; + int nextCommentIndex = firstCommentIndex; + while (nextCommentIndex < tokens.Length && + tokens[nextCommentIndex].Kind == TokenKind.Comment) + { + int firstCommentInGroup = nextCommentIndex; + int currentCommentEndLine = tokens[nextCommentIndex].Extent.EndLineNumber; + while (nextCommentIndex < tokens.Length && + tokens[nextCommentIndex].Kind == TokenKind.Comment && + tokens[nextCommentIndex].Extent.StartLineNumber <= currentCommentEndLine) + { + if (nextCommentIndex == firstCommentInGroup) + { + leastIndentedColumn = Math.Min( + leastIndentedColumn, + tokens[nextCommentIndex].Extent.StartColumnNumber); + } + currentCommentEndLine = Math.Max( + currentCommentEndLine, + tokens[nextCommentIndex].Extent.EndLineNumber); + nextCommentIndex++; + } + + if (nextCommentIndex + 1 >= tokens.Length || + (tokens[nextCommentIndex].Kind != TokenKind.NewLine && + tokens[nextCommentIndex].Kind != TokenKind.LineContinuation) || + tokens[nextCommentIndex + 1].Kind != TokenKind.Comment || + HasCodeAfterCommentOnSameLine(tokens, nextCommentIndex + 1) || + tokens[nextCommentIndex + 1].Extent.StartLineNumber != + currentCommentEndLine + 1) + { + break; + } + + nextCommentIndex++; + } + + return tokens[tokenIndex].Extent.StartColumnNumber - + leastIndentedColumn; + } + + private static bool HasCodeAfterCommentOnSameLine(Token[] tokens, int commentIndex) + { + Token nextToken = NextTokenIgnoringComments(tokens, commentIndex); + int commentGroupEndLine = tokens[commentIndex].Extent.EndLineNumber; + for (int i = commentIndex + 1; i < tokens.Length && + tokens[i].Kind == TokenKind.Comment && + tokens[i].Extent.StartLineNumber <= commentGroupEndLine; i++) + { + commentGroupEndLine = Math.Max(commentGroupEndLine, tokens[i].Extent.EndLineNumber); + } + + return nextToken != null && + nextToken.Kind != TokenKind.NewLine && + nextToken.Kind != TokenKind.LineContinuation && + nextToken.Kind != TokenKind.EndOfInput && + nextToken.Extent.StartLineNumber <= commentGroupEndLine; + } + + private static bool HasCodeBeforeCommentOnSameLine(Token[] tokens, int commentIndex) + { + int commentStartLine = tokens[commentIndex].Extent.StartLineNumber; + int precedingIndex = commentIndex - 1; + while (precedingIndex >= 0 && + tokens[precedingIndex].Kind == TokenKind.Comment && + tokens[precedingIndex].Extent.EndLineNumber >= commentStartLine) + { + precedingIndex--; + } + + return precedingIndex >= 0 && + tokens[precedingIndex].Kind != TokenKind.NewLine && + tokens[precedingIndex].Kind != TokenKind.LineContinuation && + tokens[precedingIndex].Extent.EndLineNumber == commentStartLine; + } + /// /// Scans forward from the current opener to the end of the line. /// Returns true if there is at least one unclosed opener when @@ -434,7 +641,10 @@ private static bool PipelineIsFollowedByNewlineOrLineContinuation(Token[] tokens private static bool PreviousLineEndedWithPipe(Token[] tokens, int tokenIndex, Token token) { - if (tokenIndex < 2 || token.Extent.StartLineNumber == 1) + if (tokenIndex < 2 || + (tokens[tokenIndex - 1].Kind != TokenKind.NewLine && + tokens[tokenIndex - 1].Kind != TokenKind.LineContinuation) || + token.Extent.StartLineNumber == 1) { return false; } @@ -612,12 +822,14 @@ private void AddViolation( int expectedIndentationLevel, List diagnosticRecords, ref bool onNewLine, - bool lineHasPipelineBeforeToken = false) + bool lineHasPipelineBeforeToken = false, + int indentationOffset = 0) { if (onNewLine) { onNewLine = false; - if (token.Extent.StartColumnNumber - 1 != GetIndentation(expectedIndentationLevel)) + int expectedIndentation = Math.Max(0, GetIndentation(expectedIndentationLevel) + indentationOffset); + if (token.Extent.StartColumnNumber - 1 != expectedIndentation) { var fileName = token.Extent.File; var extent = token.Extent; @@ -640,14 +852,14 @@ private void AddViolation( GetDiagnosticSeverity(), fileName, null, - GetSuggestedCorrections(token, expectedIndentationLevel, lineHasPipelineBeforeToken))); + GetSuggestedCorrections(token, expectedIndentation, lineHasPipelineBeforeToken))); } } } private List GetSuggestedCorrections( Token token, - int indentationLevel, + int indentation, bool lineHasPipelineBeforeToken = false) { // TODO Add another constructor for correction extent that takes extent @@ -661,7 +873,7 @@ private List GetSuggestedCorrections( token.Extent.EndLineNumber, 1, token.Extent.EndColumnNumber, - GetIndentationString(indentationLevel) + optionalPipeline + token.Extent.Text, + new string(indentationChar, indentation) + optionalPipeline + token.Extent.Text, token.Extent.File)); return corrections; } @@ -677,9 +889,5 @@ private int GetIndentation(int indentationLevel) return indentationLevel * indentationLevelMultiplier; } - private string GetIndentationString(int indentationLevel) - { - return new string(indentationChar, GetIndentation(indentationLevel)); - } } } diff --git a/Tests/Rules/UseConsistentIndentation.tests.ps1 b/Tests/Rules/UseConsistentIndentation.tests.ps1 index 6bf241116..f1ce8839e 100644 --- a/Tests/Rules/UseConsistentIndentation.tests.ps1 +++ b/Tests/Rules/UseConsistentIndentation.tests.ps1 @@ -204,8 +204,367 @@ function foo { } } - Context "When a multi-line command is given" { + Context "When a commented-out code block is provided" { + + It 'Should preserve relative indentation in commented-out code with indentation' -TestCases @( + @{ Kind = 'space'; Indentation = ' ' } + @{ Kind = 'tab'; Indentation = "`t" } + ) { + param([string] $Kind, [string] $Indentation) + $scriptDefinition = @( + 'if ($a) {' + "$Indentation# if (`$b) {" + "$Indentation$Indentation# doThing" + "$Indentation# }" + "$($Indentation)Write-Output 'after'" + '}' + ) -join "`n" + $settings.Rules.PSUseConsistentIndentation.Kind = $Kind + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = 'NoIndentation' + + Invoke-Formatter -ScriptDefinition $scriptDefinition -Settings $settings | + Should -Be $scriptDefinition + } + + It 'Should shift a commented-out code block without flattening it with indentation' -TestCases @( + @{ Kind = 'space'; Indentation = ' ' } + @{ Kind = 'tab'; Indentation = "`t" } + ) { + param([string] $Kind, [string] $Indentation) + $scriptDefinition = @( + 'if ($a) {' + '# if ($b) {' + "$Indentation# doThing" + '# }' + "$($Indentation)Write-Output 'after'" + '}' + ) -join "`n" + $expectedScriptDefinition = @( + 'if ($a) {' + "$Indentation# if (`$b) {" + "$Indentation$Indentation# doThing" + "$Indentation# }" + "$($Indentation)Write-Output 'after'" + '}' + ) -join "`n" + $settings.Rules.PSUseConsistentIndentation.Kind = $Kind + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = 'NoIndentation' + + Invoke-Formatter -ScriptDefinition $scriptDefinition -Settings $settings | + Should -Be $expectedScriptDefinition + } + + It 'Should treat a blank line as a boundary between comment runs with indentation' -TestCases @( + @{ Kind = 'space'; Indentation = ' ' } + @{ Kind = 'tab'; Indentation = "`t" } + ) { + param([string] $Kind, [string] $Indentation) + $scriptDefinition = @( + 'if ($a) {' + "$Indentation# first" + "$Indentation$Indentation# nested" + '' + "$Indentation$Indentation# second" + "$Indentation$Indentation$Indentation# nested" + '}' + ) -join "`n" + $expectedScriptDefinition = @( + 'if ($a) {' + "$Indentation# first" + "$Indentation$Indentation# nested" + '' + "$Indentation# second" + "$Indentation$Indentation# nested" + '}' + ) -join "`n" + $settings.Rules.PSUseConsistentIndentation.Kind = $Kind + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = 'NoIndentation' + + $formatted = Invoke-Formatter -ScriptDefinition $scriptDefinition -Settings $settings + $formatted | Should -Be $expectedScriptDefinition + Invoke-Formatter -ScriptDefinition $formatted -Settings $settings | + Should -Be $expectedScriptDefinition + } + + It 'Should restart comment-run indentation after executable code with indentation' -TestCases @( + @{ Kind = 'space'; Indentation = ' ' } + @{ Kind = 'tab'; Indentation = "`t" } + ) { + param([string] $Kind, [string] $Indentation) + $scriptDefinition = @( + 'if ($a) {' + "$Indentation# first" + "$Indentation$Indentation# nested" + "${Indentation}Write-Output 'between'" + "$Indentation$Indentation# second" + "$Indentation$Indentation$Indentation# nested" + '}' + ) -join "`n" + $expectedScriptDefinition = @( + 'if ($a) {' + "$Indentation# first" + "$Indentation$Indentation# nested" + "${Indentation}Write-Output 'between'" + "$Indentation# second" + "$Indentation$Indentation# nested" + '}' + ) -join "`n" + $settings.Rules.PSUseConsistentIndentation.Kind = $Kind + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = 'NoIndentation' + + $formatted = Invoke-Formatter -ScriptDefinition $scriptDefinition -Settings $settings + $formatted | Should -Be $expectedScriptDefinition + Invoke-Formatter -ScriptDefinition $formatted -Settings $settings | + Should -Be $expectedScriptDefinition + } + + It 'Should preserve relative indentation in comments after a line continuation' { + $scriptDefinition = @' +Write-Output ` + # first + # nested + # last + -InputObject 'value' +'@ + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = 'NoIndentation' + + Invoke-Formatter -ScriptDefinition $scriptDefinition -Settings $settings | + Should -Be $scriptDefinition + } + + It 'Should preserve indentation when a comment-only line contains multiple block comments' { + $scriptDefinition = @' +if ($a) { + <# first #> <# second #> + # nested +} +'@ + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = 'NoIndentation' + + Invoke-Formatter -ScriptDefinition $scriptDefinition -Settings $settings | + Should -Be $scriptDefinition + } + + It 'Should preserve continuation indentation before a block-comment-prefixed argument' { + $scriptDefinition = @' +Write-Output ` + # comment + <# note #> -InputObject 'value' +'@ + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = 'NoIndentation' + + Invoke-Formatter -ScriptDefinition $scriptDefinition -Settings $settings | + Should -Be $scriptDefinition + } + + It 'Should preserve continuation indentation after multiple inline block comments' { + $scriptDefinition = @' +Write-Output ` + <# first #> <# second #> + -InputObject 'value' +'@ + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = 'NoIndentation' + + Invoke-Formatter -ScriptDefinition $scriptDefinition -Settings $settings | + Should -Be $scriptDefinition + } + + It 'Should preserve continuation indentation after multiple block comments before an argument' { + $scriptDefinition = @' +Write-Output ` + <# first #> <# second #> + <# note #> -InputObject 'value' +'@ + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = 'NoIndentation' + + Invoke-Formatter -ScriptDefinition $scriptDefinition -Settings $settings | + Should -Be $scriptDefinition + } + + It 'Should preserve pipeline context after multiple block comments before an argument' { + $scriptDefinition = @' +Write-Output | + <# first #> <# second #> + <# note #> -InputObject 'value' +'@ + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = 'None' + + Invoke-Formatter -ScriptDefinition $scriptDefinition -Settings $settings | + Should -Be $scriptDefinition + } + + It 'Should preserve nested indentation across a multiline block comment' { + $scriptDefinition = @' +if ($a) { + <# first + #> <# second #> + # nested +} +'@ + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = 'NoIndentation' + + Invoke-Formatter -ScriptDefinition $scriptDefinition -Settings $settings | + Should -Be $scriptDefinition + } + + It 'Should not increase indentation across a multiline block comment when repeatedly formatted' { + $scriptDefinition = @' +if ($a) { + <# first +#> <# second #> + # nested +} +'@ + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = 'NoIndentation' + + $formatted = Invoke-Formatter -ScriptDefinition $scriptDefinition -Settings $settings + $formatted | Should -Be $scriptDefinition + Invoke-Formatter -ScriptDefinition $formatted -Settings $settings | + Should -Be $scriptDefinition + } + + It 'Should preserve indentation with many adjacent block comments' { + $comments = (1..16 | ForEach-Object { "<# comment $_ #>" }) -join ' ' + $scriptDefinition = @( + 'if ($a) {' + " $comments" + ' # nested' + '}' + ) -join "`n" + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = 'NoIndentation' + + Invoke-Formatter -ScriptDefinition $scriptDefinition -Settings $settings | + Should -Be $scriptDefinition + } + + It 'Should preserve indentation when the first comment is not the least-indented with indentation' -TestCases @( + @{ Kind = 'space'; Indentation = ' ' } + @{ Kind = 'tab'; Indentation = "`t" } + ) { + param([string] $Kind, [string] $Indentation) + $scriptDefinition = @( + "$Indentation# inner" + '# outer' + ) -join "`n" + $settings.Rules.PSUseConsistentIndentation.Kind = $Kind + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = 'NoIndentation' + + Invoke-Formatter -ScriptDefinition $scriptDefinition -Settings $settings | + Should -Be $scriptDefinition + } + + It 'Should not include a block comment followed by code in a comment run' { + $scriptDefinition = @' +if ($a) { + # comment + <# note #> Write-Output 'value' +} +'@ + $expectedScriptDefinition = @' +if ($a) { + # comment + <# note #> Write-Output 'value' +} +'@ + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = 'NoIndentation' + + Invoke-Formatter -ScriptDefinition $scriptDefinition -Settings $settings | + Should -Be $expectedScriptDefinition + } + + It 'Should not include a trailing inline comment in the following comment run' { + $scriptDefinition = @' +function test { + $value = 1 # trailing + # standalone +} +'@ + $expectedScriptDefinition = @' +function test { + $value = 1 # trailing + # standalone +} +'@ + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = 'NoIndentation' + + Invoke-Formatter -ScriptDefinition $scriptDefinition -Settings $settings | + Should -Be $expectedScriptDefinition + } + + It 'Should not include a multiline block-comment group followed by code in a comment run' { + $scriptDefinition = @' +if ($a) { + # comment +<# first #> <# second +#> Write-Output 'value' +} +'@ + $expectedScriptDefinition = @' +if ($a) { + # comment + <# first #> <# second +#> Write-Output 'value' +} +'@ + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = 'NoIndentation' + + Invoke-Formatter -ScriptDefinition $scriptDefinition -Settings $settings | + Should -Be $expectedScriptDefinition + } + It 'Should preserve a comment run containing an internal line continuation with indentation' -TestCases @( + @{ Kind = 'space'; Indentation = ' ' } + @{ Kind = 'tab'; Indentation = "`t" } + ) { + param([string] $Kind, [string] $Indentation) + $scriptDefinition = @( + '<# first #> `' + "$Indentation<# second #>" + "Write-Output 'after'" + ) -join "`n" + $settings.Rules.PSUseConsistentIndentation.Kind = $Kind + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 0 + $formatted = Invoke-Formatter -ScriptDefinition $scriptDefinition -Settings $settings + $formatted | Should -Be $scriptDefinition + Invoke-Formatter -ScriptDefinition $formatted -Settings $settings | + Should -Be $formatted + } + + It 'Should shift a comment run containing an internal line continuation uniformly with indentation' -TestCases @( + @{ Kind = 'space'; Indentation = ' ' } + @{ Kind = 'tab'; Indentation = "`t" } + ) { + param([string] $Kind, [string] $Indentation) + $scriptDefinition = @( + 'if ($a) {' + '<# first #> `' + "$Indentation<# second #>" + "$($Indentation)Write-Output 'after'" + '}' + ) -join "`n" + $expectedScriptDefinition = @( + 'if ($a) {' + "$Indentation<# first #> ``" + "$Indentation$Indentation<# second #>" + "$($Indentation)Write-Output 'after'" + '}' + ) -join "`n" + $settings.Rules.PSUseConsistentIndentation.Kind = $Kind + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $scriptDefinition -Settings $settings + $violations.Count | Should -Be 2 + $formatted = Invoke-Formatter -ScriptDefinition $scriptDefinition -Settings $settings + $formatted | Should -Be $expectedScriptDefinition + Invoke-Formatter -ScriptDefinition $formatted -Settings $settings | + Should -Be $formatted + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $formatted -Settings $settings + $violations.Count | Should -Be 0 + } + } + + Context "When a multi-line command is given" { It "When a comment is in the middle of a multi-line statement with preceding and succeeding line continuations" { $scriptDefinition = @' foo ` @@ -530,6 +889,105 @@ foo | Test-CorrectionExtentFromContent @params } + It "Should preserve indentation in a comment run after a pipeline with PipelineIndentation None" { + $def = @' +foo | + # first comment + # second comment +bar +'@ + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = 'None' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $violations | Should -BeNullOrEmpty + Invoke-Formatter -ScriptDefinition $def -Settings $settings | Should -Be $def + } + + It "Should preserve comment-run indentation in a multiline pipeline using " -TestCases @( + @{ + PipelineIndentation = 'IncreaseIndentationForFirstPipeline' + ExpectedFinalCommandIndentation = ' ' + } + @{ + PipelineIndentation = 'IncreaseIndentationAfterEveryPipeline' + ExpectedFinalCommandIndentation = ' ' + } + ) { + param( + [string] $PipelineIndentation, + [string] $ExpectedFinalCommandIndentation + ) + $def = @( + 'foo |' + ' # first comment' + ' # nested comment' + ' bar |' + "${ExpectedFinalCommandIndentation}baz" + ) -join "`n" + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = $PipelineIndentation + + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $violations | Should -BeNullOrEmpty + $formatted = Invoke-Formatter -ScriptDefinition $def -Settings $settings + $formatted | Should -Be $def + Invoke-Formatter -ScriptDefinition $formatted -Settings $settings | + Should -Be $def + } + + It "Should preserve pipeline indentation across a comment line continuation with PipelineIndentation None" { + $def = @' +foo | + <# comment #> ` + <# note #> bar +'@ + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = 'None' + Invoke-Formatter -ScriptDefinition $def -Settings $settings | Should -Be $def + } + + It "Should not inherit pipeline indentation from a trailing comment with PipelineIndentation None" { + $def = @' +function test { + foo | bar # trailing + <# note #> baz +} +'@ + $expected = @' +function test { + foo | bar # trailing + <# note #> baz +} +'@ + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = 'None' + $violations = Invoke-ScriptAnalyzer -ScriptDefinition $def -Settings $settings + $violations.Count | Should -Be 1 + Invoke-Formatter -ScriptDefinition $def -Settings $settings | Should -Be $expected + Invoke-Formatter -ScriptDefinition $expected -Settings $settings | Should -Be $expected + } + + It "Should exclude a trailing comment after a line-leading pipe from comment runs" { + $def = @' +function test { + foo ` + | # trailing +# standalone + bar +} +'@ + $expected = @' +function test { + foo ` + | # trailing + # standalone + bar +} +'@ + $settings.Rules.PSUseConsistentIndentation.PipelineIndentation = 'NoIndentation' + $formatted = Invoke-Formatter -ScriptDefinition $def -Settings $settings + + $formatted | Should -Be $expected + Invoke-Formatter -ScriptDefinition $formatted -Settings $settings | + Should -Be $expected + } + It "Should indent properly after line continuation (backtick) character with pipeline" { $def = @' foo | diff --git a/docs/Rules/UseConsistentIndentation.md b/docs/Rules/UseConsistentIndentation.md index 5e6392772..4d020bbd4 100644 --- a/docs/Rules/UseConsistentIndentation.md +++ b/docs/Rules/UseConsistentIndentation.md @@ -15,6 +15,9 @@ title: UseConsistentIndentation This rule detects inconsistent indentation patterns within a source file. Indentation should be consistent throughout your script. +Consecutive comment lines are shifted together to preserve their relative indentation, including +when a line continuation occurs within the comment run. + ## Example ### Noncompliant