diff --git a/Rules/UseConsistentIndentation.cs b/Rules/UseConsistentIndentation.cs index 7c51b98f6..5fd573cc5 100644 --- a/Rules/UseConsistentIndentation.cs +++ b/Rules/UseConsistentIndentation.cs @@ -275,17 +275,15 @@ 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) - { - searchForPrecedingLineContinuationIndex--; - } - - if (searchForPrecedingLineContinuationIndex >= 0 && tokens[searchForPrecedingLineContinuationIndex].Kind == TokenKind.LineContinuation) + GetCommentRunIndentationOffset( + tokens, + tokenIndex - 2, + out bool commentRunStartsAfterLineContinuation); + if (commentRunStartsAfterLineContinuation) { tempIndentationLevel++; } @@ -298,9 +296,17 @@ public override IEnumerable AnalyzeScript(Ast ast, string file } bool lineHasPipelineBeforeToken = LineHasPipelineBeforeToken(tokens, tokenIndex, token); - int commentIndentationOffset = token.Kind == TokenKind.Comment - ? GetCommentRunIndentationOffset(tokens, tokenIndex) - : 0; + int commentIndentationOffset = 0; + if (token.Kind == TokenKind.Comment) + { + commentIndentationOffset = GetCommentRunIndentationOffset( + tokens, + tokenIndex, + out bool commentRunStartsAfterLineContinuation); + tempIndentationLevel = indentationLevel + + (commentRunStartsAfterLineContinuation ? 1 : 0); + } + AddViolation(token, tempIndentationLevel, diagnosticRecords, ref onNewLine, lineHasPipelineBeforeToken, commentIndentationOffset); } break; @@ -339,18 +345,25 @@ public override IEnumerable AnalyzeScript(Ast ast, string file return diagnosticRecords; } - private static int GetCommentRunIndentationOffset(Token[] tokens, int tokenIndex) + private static int GetCommentRunIndentationOffset( + Token[] tokens, + int tokenIndex, + out bool commentRunStartsAfterLineContinuation) { int firstCommentIndex = tokenIndex; int currentLine = tokens[tokenIndex].Extent.StartLineNumber; int precedingIndex = tokenIndex - 1; - while (precedingIndex >= 0 && tokens[precedingIndex].Kind == TokenKind.NewLine) + while (precedingIndex >= 0 && + (tokens[precedingIndex].Kind == TokenKind.NewLine || + tokens[precedingIndex].Kind == TokenKind.LineContinuation)) { precedingIndex--; if (precedingIndex < 0 || tokens[precedingIndex].Kind != TokenKind.Comment || tokens[precedingIndex].Extent.EndLineNumber != currentLine - 1 || - (precedingIndex > 0 && tokens[precedingIndex - 1].Kind != TokenKind.NewLine)) + (precedingIndex > 0 && + tokens[precedingIndex - 1].Kind != TokenKind.NewLine && + tokens[precedingIndex - 1].Kind != TokenKind.LineContinuation)) { break; } @@ -360,10 +373,14 @@ private static int GetCommentRunIndentationOffset(Token[] tokens, int tokenIndex precedingIndex--; } + commentRunStartsAfterLineContinuation = + firstCommentIndex > 0 && + tokens[firstCommentIndex - 1].Kind == TokenKind.LineContinuation; int leastIndentedColumn = tokens[firstCommentIndex].Extent.StartColumnNumber; for (int nextCommentIndex = firstCommentIndex + 2; nextCommentIndex < tokens.Length && - tokens[nextCommentIndex - 1].Kind == TokenKind.NewLine && + (tokens[nextCommentIndex - 1].Kind == TokenKind.NewLine || + tokens[nextCommentIndex - 1].Kind == TokenKind.LineContinuation) && tokens[nextCommentIndex].Kind == TokenKind.Comment && tokens[nextCommentIndex].Extent.StartLineNumber == tokens[nextCommentIndex - 2].Extent.EndLineNumber + 1; nextCommentIndex += 2) diff --git a/Tests/Rules/UseConsistentIndentation.tests.ps1 b/Tests/Rules/UseConsistentIndentation.tests.ps1 index c6418ed49..6febc02c7 100644 --- a/Tests/Rules/UseConsistentIndentation.tests.ps1 +++ b/Tests/Rules/UseConsistentIndentation.tests.ps1 @@ -254,6 +254,20 @@ function foo { 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 the first comment is not the least-indented with indentation' -TestCases @( @{ Kind = 'space'; Indentation = ' ' } @{ Kind = 'tab'; Indentation = "`t" } @@ -269,6 +283,57 @@ function foo { Invoke-Formatter -ScriptDefinition $scriptDefinition -Settings $settings | Should -Be $scriptDefinition } + + 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" { 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