Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 85 additions & 18 deletions Rules/UseConsistentIndentation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -275,17 +275,16 @@ public override IEnumerable<DiagnosticRecord> 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,
out _);
if (commentRunStartsAfterLineContinuation)
{
tempIndentationLevel++;
}
Expand All @@ -298,7 +297,25 @@ public override IEnumerable<DiagnosticRecord> 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);
Comment on lines +314 to +315
}

AddViolation(token, tempIndentationLevel, diagnosticRecords, ref onNewLine, lineHasPipelineBeforeToken, commentIndentationOffset);
}
break;
}
Expand Down Expand Up @@ -336,6 +353,58 @@ public override IEnumerable<DiagnosticRecord> AnalyzeScript(Ast ast, string file
return diagnosticRecords;
}

private static int GetCommentRunIndentationOffset(
Token[] tokens,
int tokenIndex,
out bool commentRunStartsAfterLineContinuation,
out bool commentRunStartsAfterPipeline)
{
int firstCommentIndex = tokenIndex;
int currentLine = tokens[tokenIndex].Extent.StartLineNumber;
int precedingIndex = tokenIndex - 1;
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 &&
tokens[precedingIndex - 1].Kind != TokenKind.LineContinuation))
{
break;
}

firstCommentIndex = precedingIndex;
currentLine = tokens[precedingIndex].Extent.StartLineNumber;
precedingIndex--;
}

commentRunStartsAfterLineContinuation =
firstCommentIndex > 0 &&
tokens[firstCommentIndex - 1].Kind == TokenKind.LineContinuation;
commentRunStartsAfterPipeline =
PreviousLineEndedWithPipe(tokens, firstCommentIndex, tokens[firstCommentIndex]);
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.LineContinuation) &&
tokens[nextCommentIndex].Kind == TokenKind.Comment &&
tokens[nextCommentIndex].Extent.StartLineNumber == tokens[nextCommentIndex - 2].Extent.EndLineNumber + 1;
Comment on lines +393 to +396
nextCommentIndex += 2)
{
leastIndentedColumn = Math.Min(
leastIndentedColumn,
tokens[nextCommentIndex].Extent.StartColumnNumber);
}

return tokens[tokenIndex].Extent.StartColumnNumber -
leastIndentedColumn;
}

/// <summary>
/// Scans forward from the current opener to the end of the line.
/// Returns true if there is at least one unclosed opener when
Expand Down Expand Up @@ -612,12 +681,14 @@ private void AddViolation(
int expectedIndentationLevel,
List<DiagnosticRecord> 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);
Comment thread
bergmeister marked this conversation as resolved.
if (token.Extent.StartColumnNumber - 1 != expectedIndentation)
{
var fileName = token.Extent.File;
var extent = token.Extent;
Expand All @@ -640,14 +711,14 @@ private void AddViolation(
GetDiagnosticSeverity(),
fileName,
null,
GetSuggestedCorrections(token, expectedIndentationLevel, lineHasPipelineBeforeToken)));
GetSuggestedCorrections(token, expectedIndentation, lineHasPipelineBeforeToken)));
}
}
}

private List<CorrectionExtent> GetSuggestedCorrections(
Token token,
int indentationLevel,
int indentation,
bool lineHasPipelineBeforeToken = false)
{
// TODO Add another constructor for correction extent that takes extent
Expand All @@ -661,7 +732,7 @@ private List<CorrectionExtent> 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;
}
Expand All @@ -677,9 +748,5 @@ private int GetIndentation(int indentationLevel)
return indentationLevel * indentationLevelMultiplier;
}

private string GetIndentationString(int indentationLevel)
{
return new string(indentationChar, GetIndentation(indentationLevel));
}
}
}
146 changes: 145 additions & 1 deletion Tests/Rules/UseConsistentIndentation.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,139 @@ 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 <Kind> 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 <Kind> 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 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 <Kind> 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 preserve a comment run containing an internal line continuation with <Kind> 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 <Kind> 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 `
Expand Down Expand Up @@ -530,6 +661,19 @@ 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 indent properly after line continuation (backtick) character with pipeline" {
$def = @'
foo |
Expand Down
3 changes: 3 additions & 0 deletions docs/Rules/UseConsistentIndentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading