diff --git a/Engine/Formatter.cs b/Engine/Formatter.cs index a6a25f0fb..a60943719 100644 --- a/Engine/Formatter.cs +++ b/Engine/Formatter.cs @@ -54,6 +54,7 @@ public static string Format( ScriptBlockAst scriptAst = null; Token[] scriptTokens = null; bool skipParsing = false; + bool isRangeSpecified = range != null; foreach (var rule in ruleOrder) { if (!settings.RuleArguments.ContainsKey(rule)) @@ -68,6 +69,10 @@ public static string Format( text = ScriptAnalyzer.Instance.Fix(text, range, skipParsing, out Range updatedRange, out bool fixesWereApplied, ref scriptAst, ref scriptTokens, skipVariableAnalysis: true); skipParsing = !fixesWereApplied; range = updatedRange; + if (isRangeSpecified && range == null) + { + break; + } } return text.ToString(); diff --git a/Engine/ScriptAnalyzer.cs b/Engine/ScriptAnalyzer.cs index 91bf188f2..19659a78b 100644 --- a/Engine/ScriptAnalyzer.cs +++ b/Engine/ScriptAnalyzer.cs @@ -1556,7 +1556,6 @@ public EditableText Fix(EditableText text, Range range, bool skipParsing, out Ra } range = isRangeNull ? null : SnapToEdges(text, range); - var previousLineCount = text.LineCount; var previousUnusedCorrections = 0; do { @@ -1578,7 +1577,7 @@ public EditableText Fix(EditableText text, Range range, bool skipParsing, out Ra this.outputWriter.WriteVerbose($"Found {corrections.Count} violations."); int unusedCorrections; - Fix(text, corrections, out unusedCorrections); + Fix(text, corrections, out unusedCorrections, ref range); var numberOfFixedViolatons = corrections.Count - unusedCorrections; fixesWereApplied = numberOfFixedViolatons > 0; this.outputWriter.WriteVerbose($"Fixed {numberOfFixedViolatons} violations."); @@ -1595,17 +1594,7 @@ public EditableText Fix(EditableText text, Range range, bool skipParsing, out Ra } previousUnusedCorrections = unusedCorrections; - var lineCount = text.LineCount; - if (!isRangeNull && lineCount != previousLineCount) - { - range = new Range( - range.Start, - range.End.Shift(lineCount - previousLineCount, 0)); - range = SnapToEdges(text, range); - } - - previousLineCount = lineCount; - } while (previousUnusedCorrections > 0); + } while (previousUnusedCorrections > 0 && (isRangeNull || range != null)); updatedRange = range; return text; @@ -1728,7 +1717,8 @@ private static IEnumerable GetCorrectionExtentsForFix( private static EditableText Fix( EditableText text, List correctionExtents, - out int unusedCorrections) + out int unusedCorrections, + ref Range range) { var correctionsToUse = GetCorrectionExtentsForFix(correctionExtents); var count = 0; @@ -1736,6 +1726,38 @@ private static EditableText Fix( { count++; text.ApplyEdit(correction); + if (range != null) + { + var lineDelta = correction.Lines.Length - 1 + - (correction.EndLineNumber - correction.StartLineNumber); + var columnDelta = 0; + if (correction.EndLineNumber == range.End.Line) + { + var replacementEndColumn = correction.Lines.Length == 0 + ? correction.StartColumnNumber + : correction.Lines.Last().TrimEnd('\r').Length + + (correction.Lines.Length == 1 ? correction.StartColumnNumber : 1); + columnDelta = replacementEndColumn - correction.EndColumnNumber; + } + + var endLine = range.End.Line + lineDelta; + if (correction.Lines.Length == 0 && endLine < range.Start.Line) + { + range = null; + } + else if (correction.Lines.Length == 0 + && lineDelta < 0 + && correction.EndLineNumber == range.End.Line) + { + range = new Range( + range.Start, + new Position(endLine, text.Lines[endLine - 1].Length + 1)); + } + else + { + range = new Range(range.Start, range.End.Shift(lineDelta, columnDelta)); + } + } } unusedCorrections = correctionExtents.Count - count; diff --git a/Tests/Engine/InvokeFormatter.tests.ps1 b/Tests/Engine/InvokeFormatter.tests.ps1 index dcc0a6174..e49827a6a 100644 --- a/Tests/Engine/InvokeFormatter.tests.ps1 +++ b/Tests/Engine/InvokeFormatter.tests.ps1 @@ -81,6 +81,101 @@ function foo { Invoke-Formatter -ScriptDefinition $def -Range @(3, 1, 4, 1) | Should -Be $expected } + + It "Should track the range when an edit deletes its last line" { + $text = New-Object -TypeName "Microsoft.Windows.PowerShell.ScriptAnalyzer.EditableText" -ArgumentList "first`n`nlast" + $correctionType = "Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.CorrectionExtent" -as [type] + $correction = [Activator]::CreateInstance( + $correctionType, + [object[]]@(2, 2, 1, 1, [string[]]@(), $null, $null)) + $correctionsType = [System.Collections.Generic.List``1].MakeGenericType($correctionType) + $corrections = [Activator]::CreateInstance($correctionsType) + $corrections.Add($correction) + $range = New-Object -TypeName "Microsoft.Windows.PowerShell.ScriptAnalyzer.Range" -ArgumentList 1, 1, 2, 1 + $arguments = [object[]]@( + $text.PSObject.BaseObject, + $corrections.PSObject.BaseObject, + 0, + $range.PSObject.BaseObject) + $fixMethod = ("Microsoft.Windows.PowerShell.ScriptAnalyzer.ScriptAnalyzer" -as [type]).GetMethod( + 'Fix', + [Reflection.BindingFlags]'NonPublic, Static') + + $fixMethod.Invoke($null, $arguments) | Out-Null + + $text.ToString() | Should -Be "first`nlast" + $arguments[3].End.Line | Should -Be 1 + $arguments[3].End.Column | Should -Be 6 + } + + It "Should exhaust the range when an edit deletes all selected lines" { + $text = New-Object -TypeName "Microsoft.Windows.PowerShell.ScriptAnalyzer.EditableText" -ArgumentList "first`nlast" + $correctionType = "Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.CorrectionExtent" -as [type] + $correction = [Activator]::CreateInstance( + $correctionType, + [object[]]@(2, 2, 1, 5, [string[]]@(), $null, $null)) + $correctionsType = [System.Collections.Generic.List``1].MakeGenericType($correctionType) + $corrections = [Activator]::CreateInstance($correctionsType) + $corrections.Add($correction) + $range = New-Object -TypeName "Microsoft.Windows.PowerShell.ScriptAnalyzer.Range" -ArgumentList 2, 1, 2, 5 + $arguments = [object[]]@( + $text.PSObject.BaseObject, + $corrections.PSObject.BaseObject, + 0, + $range.PSObject.BaseObject) + $fixMethod = ("Microsoft.Windows.PowerShell.ScriptAnalyzer.ScriptAnalyzer" -as [type]).GetMethod( + 'Fix', + [Reflection.BindingFlags]'NonPublic, Static') + + $fixMethod.Invoke($null, $arguments) | Out-Null + $text.ToString() | Should -Be "first" + $arguments[3] | Should -BeNullOrEmpty + } + + It "Should apply all corrections when earlier fixes expand the selected line" { + $def = "if(`$a){'x'}`nif(`$b){'y'}" + $expected = "if(`$a){'x'}`nif (`$b) { 'y' }" + + Invoke-Formatter -ScriptDefinition $def -Range @(2, 1, 2, 12) | Should -Be $expected + } + + It "Should track the selected lines when fixes " -TestCases @( + @{ + Name = 'expand the last line' + Selected = "if(`$b){'y'}" + Expected = "if (`$b) { 'y' }" + } + @{ + Name = 'shorten the last line' + Selected = "if (`$b) { 'y' }" + Expected = "if (`$b) { 'y' }" + } + @{ + Name = 'expand multiple lines' + Selected = "if(`$b){'y'}`nif(`$c){'z'}" + Expected = "if (`$b) { 'y' }`nif (`$c) { 'z' }" + } + @{ + Name = 'remove a line' + Selected = "if(`$b)`n{`n'y'`n}" + Expected = "if (`$b) {`n 'y'`n}" + } + @{ + Name = 'add a line' + Selected = "if(`$b) {`n'y'`n}" + Expected = "if (`$b)`n{`n 'y'`n}" + Settings = 'CodeFormattingAllman' + } + ) { + param($Selected, $Expected, $Settings = 'CodeFormatting') + + $def = "if(`$a){'x'}`n$Selected`nif(`$d){'w'}" + $lines = $Selected -split "`n" + $range = @(2, 1, (1 + $lines.Count), (1 + $lines[-1].Length)) + + Invoke-Formatter -ScriptDefinition $def -Range $range -Settings $Settings | + Should -Be "if(`$a){'x'}`n$Expected`nif(`$d){'w'}" + } } Context "When no settings are given" {