Skip to content
Merged
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
78 changes: 19 additions & 59 deletions blink/internal/fixer/rules/no_trailing_whitespace.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package rules

import (
"strings"

"github.com/ecsphp/ecs-src/blink/internal/token"
"github.com/ecsphp/ecs-src/blink/internal/tokens"
)

// PHP-CS-Fixer: https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/src/Fixer/Whitespace/NoTrailingWhitespaceFixer.php
//
// NoTrailingWhitespace removes spaces and tabs at the end of a line.
// NoTrailingWhitespace removes spaces and tabs at the end of a line. Like
// PHP-CS-Fixer, it trims a whitespace token only up to its first line break,
// so a line holding nothing but whitespace is left alone, and the same goes for
// a single-line token unless it is the file's last.
type NoTrailingWhitespace struct{}

func (NoTrailingWhitespace) Name() string {
Expand All @@ -26,66 +31,21 @@ func (NoTrailingWhitespace) Fix(s *tokens.Stream) bool {
if t.Kind != token.Whitespace {
continue
}
v := stripTrailingWS(t.Value, i == last)
if v != t.Value {
s.SetValue(i, v)
changed = true
head, rest := t.Value, ""
if nl := strings.IndexAny(t.Value, "\r\n"); nl >= 0 {
head, rest = t.Value[:nl], t.Value[nl:]
} else if i != last {
continue
}
}
return changed
}

// stripTrailingWS drops a run of spaces/tabs that sits immediately before a
// (\r?\n) newline, and - only in the file's final token (isLast) - a run at the
// very end. It returns the input unchanged, with no allocation, when there is
// nothing to strip, matching the previous regexes `[ \t]+(\r?\n)` and `[ \t]+$`.
func stripTrailingWS(val string, isLast bool) string {
for i := 0; i < len(val); {
if val[i] == ' ' || val[i] == '\t' {
start := i
for i < len(val) && (val[i] == ' ' || val[i] == '\t') {
i++
}
if trailing(val, i, isLast) {
return stripTrailingBuild(val, start, isLast)
}
} else {
i++
// an open tag that already ends the line leaves this token's head as
// the next line's indentation
if i > 0 && s.At(i-1).Kind == token.OpenTag && strings.HasSuffix(s.At(i-1).Value, "\n") {
continue
}
}
return val
}

// stripTrailingBuild builds the stripped string once a first run to drop is
// found at firstDrop, copying the untouched prefix and continuing the scan.
func stripTrailingBuild(val string, firstDrop int, isLast bool) string {
out := make([]byte, 0, len(val))
out = append(out, val[:firstDrop]...)
for i := firstDrop; i < len(val); {
if val[i] == ' ' || val[i] == '\t' {
start := i
for i < len(val) && (val[i] == ' ' || val[i] == '\t') {
i++
}
if !trailing(val, i, isLast) {
out = append(out, val[start:i]...)
}
} else {
out = append(out, val[i])
i++
if trimmed := strings.TrimRight(head, " \t"); trimmed != head {
s.SetValue(i, trimmed+rest)
changed = true
}
}
return string(out)
}

// trailing reports whether a space/tab run ending at end should be dropped: it
// is directly before a newline, or at the file's end in the final token.
func trailing(val string, end int, isLast bool) bool {
if end < len(val) && val[end] == '\n' {
return true
}
if end+1 < len(val) && val[end] == '\r' && val[end+1] == '\n' {
return true
}
return end == len(val) && isLast
return changed
}
17 changes: 17 additions & 0 deletions blink/internal/fixer/rules/rules_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,23 @@ func TestNoTrailingWhitespace(t *testing.T) {
if _, changed := apply(t, NoTrailingWhitespace{}, "<?php $x = 1;\n"); changed {
t.Fatal("clean input should not change")
}
// as in PHP-CS-Fixer: only the part before a token's first line break is
// trimmed, so whitespace-only lines and the next line's indent stay
src := "<?php\nfunction f()\n{\n $a = 1; \n \n $b = 2;\n}\n"
got, changed = apply(t, NoTrailingWhitespace{}, src)
if want := "<?php\nfunction f()\n{\n $a = 1;\n \n $b = 2;\n}\n"; !changed || got != want {
t.Fatalf("blank line: changed=%v got=%q want=%q", changed, got, want)
}
// a single-line run is trimmed only at the end of the file
got, changed = apply(t, NoTrailingWhitespace{}, "<?php $x = 1; ")
if want := "<?php $x = 1;"; !changed || got != want {
t.Fatalf("eof: changed=%v got=%q want=%q", changed, got, want)
}
// \r\n and a lone \r count as line breaks
got, changed = apply(t, NoTrailingWhitespace{}, "<?php $x = 1; \r\n$y = 2;\t\r$z = 3;\n")
if want := "<?php $x = 1;\r\n$y = 2;\r$z = 3;\n"; !changed || got != want {
t.Fatalf("crlf: changed=%v got=%q want=%q", changed, got, want)
}
}

func TestSingleBlankLineAtEndOfFile(t *testing.T) {
Expand Down
Loading