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
4 changes: 4 additions & 0 deletions blink/internal/config/ecsconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ func resolveECS(f ecsFile) (*Config, *ECSResolution, error) {
}
}

// apply fixers in the same canonical (priority) order as the standalone path,
// since the ECS dump lists them in set order, not execution order
rules.CanonicalOrder(config.Rules)

sort.Strings(resolution.Unsupported)
sort.Strings(resolution.ConfigIgnored)
sort.Strings(resolution.PerPathSkips)
Expand Down
228 changes: 217 additions & 11 deletions blink/internal/fixer/rules/array_notation.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package rules

import (
"slices"
"strings"

"blink/internal/fixer"
"blink/internal/token"
"blink/internal/tokens"
)
Expand Down Expand Up @@ -43,10 +45,153 @@ func convertLongArray(s *tokens.Stream, name string) bool {
return changed
}

// arrayNotationStringList reads a string-list option (a []string or []any of strings).
func arrayNotationStringList(v any) ([]string, bool) {
switch list := v.(type) {
case []string:
return list, true
case []any:
out := make([]string, 0, len(list))
for _, item := range list {
str, ok := item.(string)
if !ok {
return nil, false
}
out = append(out, str)
}
return out, true
}
return nil, false
}

func arrayNotationContains(list []string, want string) bool {
return slices.Contains(list, want)
}

func arrayNotationIsHeredoc(t token.Token) bool {
return t.Kind == token.String && strings.HasPrefix(t.Value, "<<<")
}

// arrayNotationEnclosing returns the index of the innermost unclosed opener before i, or -1.
func arrayNotationEnclosing(s *tokens.Stream, i int) int {
depth := 0
for j := i - 1; j >= 0; j-- {
if s.At(j).Kind != token.Punct {
continue
}
switch s.At(j).Value {
case ")", "]", "}":
depth++
case "(", "[", "{":
if depth == 0 {
return j
}
depth--
}
}
return -1
}

func arrayNotationIsAs(t token.Token) bool {
return (t.Kind == token.Keyword || t.Kind == token.Ident) && strings.EqualFold(t.Value, "as")
}

// arrayNotationInForeachHead reports whether the token at i sits inside the
// "(... as ...)" head of a foreach, after the "as" keyword.
func arrayNotationInForeachHead(s *tokens.Stream, i int) bool {
depth := 0
seenAs := false
for j := i - 1; j >= 0; j-- {
t := s.At(j)
if t.Kind == token.Punct {
switch t.Value {
case ")", "]", "}":
depth++
case "(", "[", "{":
if depth > 0 {
depth--
continue
}
if t.Value != "(" || !seenAs {
return false
}
p := sigPrev(s, j)
return p >= 0 && strings.EqualFold(s.At(p).Value, "foreach")
}
continue
}
if depth == 0 && arrayNotationIsAs(t) {
seenAs = true
}
}
return false
}

// arrayNotationIsDestructuring reports whether the short "[" at open is an
// array destructuring target rather than an array literal.
func arrayNotationIsDestructuring(s *tokens.Stream, open int) bool {
if closeIdx := s.MatchForward(open); closeIdx >= 0 {
if n := sigNext(s, closeIdx); n >= 0 && s.At(n).Kind == token.Punct && s.At(n).Value == "=" {
return true
}
}
if p := sigPrev(s, open); p >= 0 {
if arrayNotationIsAs(s.At(p)) {
return true
}
if s.At(p).Kind == token.Punct && s.At(p).Value == "=>" && arrayNotationInForeachHead(s, open) {
return true
}
}
enc := arrayNotationEnclosing(s, open)
if enc < 0 {
return false
}
switch s.At(enc).Value {
case "[":
return !isOffsetOpen(s, enc) && arrayNotationIsDestructuring(s, enc)
case "(":
q := sigPrev(s, enc)
return q >= 0 && strings.EqualFold(s.At(q).Value, "list")
}
return false
}

// arrayNotationToLong rewrites short "[...]" to "array(...)" (literals) or
// "list(...)" (destructuring targets), depending on destructuring.
func arrayNotationToLong(s *tokens.Stream, destructuring bool) bool {
changed := false
for i := s.Len() - 1; i >= 0; i-- {
t := s.At(i)
if t.Kind != token.Punct || t.Value != "[" || isOffsetOpen(s, i) {
continue
}
if arrayNotationIsDestructuring(s, i) != destructuring {
continue
}
closeIdx := s.MatchForward(i)
if closeIdx < 0 {
continue
}
name := "array"
if destructuring {
name = "list"
}
s.SetValue(closeIdx, ")")
s.SetValue(i, "(")
s.InsertAt(i, token.Token{Kind: token.Keyword, Value: name})
changed = true
}
return changed
}

// PHP-CS-Fixer: https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/src/Fixer/ArrayNotation/ArraySyntaxFixer.php
//
// ArraySyntax rewrites long "array(...)" to the short "[...]" form.
type ArraySyntax struct{}
// ArraySyntax rewrites long "array(...)" to the short "[...]" form. With
// syntax=long it does the opposite.
type ArraySyntax struct {
Long bool
}

func (ArraySyntax) Name() string {
return `PhpCsFixer\Fixer\ArrayNotation\ArraySyntaxFixer`
Expand All @@ -56,12 +201,27 @@ func (ArraySyntax) SourceURL() string {
return "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/src/Fixer/ArrayNotation/ArraySyntaxFixer.php"
}

func (ArraySyntax) Fix(s *tokens.Stream) bool { return convertLongArray(s, "array") }
func (f ArraySyntax) WithConfig(config map[string]any) fixer.Fixer {
if v, ok := config["syntax"].(string); ok {
f.Long = v == "long"
}
return f
}

func (f ArraySyntax) Fix(s *tokens.Stream) bool {
if f.Long {
return arrayNotationToLong(s, false)
}
return convertLongArray(s, "array")
}

// PHP-CS-Fixer: https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/src/Fixer/ListNotation/ListSyntaxFixer.php
//
// ListSyntax rewrites "list(...)" to the short "[...]" form.
type ListSyntax struct{}
// ListSyntax rewrites "list(...)" to the short "[...]" form. With syntax=long
// it does the opposite.
type ListSyntax struct {
Long bool
}

func (ListSyntax) Name() string {
return `PhpCsFixer\Fixer\ListNotation\ListSyntaxFixer`
Expand All @@ -71,7 +231,19 @@ func (ListSyntax) SourceURL() string {
return "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/src/Fixer/ListNotation/ListSyntaxFixer.php"
}

func (ListSyntax) Fix(s *tokens.Stream) bool { return convertLongArray(s, "list") }
func (f ListSyntax) WithConfig(config map[string]any) fixer.Fixer {
if v, ok := config["syntax"].(string); ok {
f.Long = v == "long"
}
return f
}

func (f ListSyntax) Fix(s *tokens.Stream) bool {
if f.Long {
return arrayNotationToLong(s, true)
}
return convertLongArray(s, "list")
}

// enclosingIsArray reports whether the innermost open bracket at index i is "[".
func topBracketIsArray(stack []string) bool {
Expand All @@ -81,8 +253,11 @@ func topBracketIsArray(stack []string) bool {
// PHP-CS-Fixer: https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/src/Fixer/ArrayNotation/NoWhitespaceBeforeCommaInArrayFixer.php
//
// NoWhitespaceBeforeCommaInArray removes single-line whitespace before a comma
// inside an array.
type NoWhitespaceBeforeCommaInArray struct{}
// inside an array. SkipAfterHeredoc (after_heredoc=false) keeps the whitespace
// between a heredoc end and the comma.
type NoWhitespaceBeforeCommaInArray struct {
SkipAfterHeredoc bool
}

func (NoWhitespaceBeforeCommaInArray) Name() string {
return `PhpCsFixer\Fixer\ArrayNotation\NoWhitespaceBeforeCommaInArrayFixer`
Expand All @@ -92,7 +267,14 @@ func (NoWhitespaceBeforeCommaInArray) SourceURL() string {
return "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/src/Fixer/ArrayNotation/NoWhitespaceBeforeCommaInArrayFixer.php"
}

func (NoWhitespaceBeforeCommaInArray) Fix(s *tokens.Stream) bool {
func (f NoWhitespaceBeforeCommaInArray) WithConfig(config map[string]any) fixer.Fixer {
if v, ok := config["after_heredoc"].(bool); ok {
f.SkipAfterHeredoc = !v
}
return f
}

func (f NoWhitespaceBeforeCommaInArray) Fix(s *tokens.Stream) bool {
changed := false
var stack []string
for i := 0; i < s.Len(); i++ {
Expand All @@ -108,6 +290,9 @@ func (NoWhitespaceBeforeCommaInArray) Fix(s *tokens.Stream) bool {
case ",":
if topBracketIsArray(stack) && i > 0 &&
s.At(i-1).Kind == token.Whitespace && !hasNewline(s.At(i-1).Value) {
if f.SkipAfterHeredoc && i > 1 && arrayNotationIsHeredoc(s.At(i-2)) {
continue
}
s.RemoveAt(i - 1)
i--
changed = true
Expand All @@ -122,7 +307,10 @@ func (NoWhitespaceBeforeCommaInArray) Fix(s *tokens.Stream) bool {
//
// WhitespaceAfterCommaInArray ensures a single space after a comma inside an
// array (unless a newline follows, or the comma is a trailing one before "]").
type WhitespaceAfterCommaInArray struct{}
// EnsureSingleSpace also collapses horizontal whitespace after the comma to one space.
type WhitespaceAfterCommaInArray struct {
EnsureSingleSpace bool
}

func (WhitespaceAfterCommaInArray) Name() string {
return `PhpCsFixer\Fixer\ArrayNotation\WhitespaceAfterCommaInArrayFixer`
Expand All @@ -132,7 +320,18 @@ func (WhitespaceAfterCommaInArray) SourceURL() string {
return "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/src/Fixer/ArrayNotation/WhitespaceAfterCommaInArrayFixer.php"
}

func (WhitespaceAfterCommaInArray) Fix(s *tokens.Stream) bool {
func (f WhitespaceAfterCommaInArray) WithConfig(config map[string]any) fixer.Fixer {
if v, ok := config["ensure_single_space"].(bool); ok {
f.EnsureSingleSpace = v
}
return f
}

func arrayNotationIsHorizontal(t token.Token) bool {
return t.Kind == token.Whitespace && t.Value != "" && strings.Trim(t.Value, " \t") == ""
}

func (f WhitespaceAfterCommaInArray) Fix(s *tokens.Stream) bool {
changed := false
var stack []string
for i := 0; i < s.Len(); i++ {
Expand All @@ -153,6 +352,13 @@ func (WhitespaceAfterCommaInArray) Fix(s *tokens.Stream) bool {
}
next := s.At(i + 1)
if next.Kind == token.Whitespace {
if f.EnsureSingleSpace && next.Value != " " && arrayNotationIsHorizontal(next) {
afterComment := i+2 < s.Len() && (s.At(i+2).Kind == token.Comment || s.At(i+2).Kind == token.DocComment)
if !afterComment || (i+3 < s.Len() && arrayNotationIsHorizontal(s.At(i+3))) {
s.SetValue(i+1, " ")
changed = true
}
}
continue // already spaced or newline
}
if next.Kind == token.Punct && next.Value == "]" {
Expand Down
28 changes: 28 additions & 0 deletions blink/internal/fixer/rules/array_notation_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package rules

import "testing"

func TestArrayNotationConfigCases(t *testing.T) {
cases := []struct {
name string
rule fixerRule
src string
want string
}{
{"array default short", ArraySyntax{}, "<?php $a = array(1, 2);", "<?php $a = [1, 2];"},
{"array long", ArraySyntax{}.WithConfig(map[string]any{"syntax": "long"}).(fixerRule), "<?php $a = [1, [2]]; $b = $a[0]; [$x, $y] = $a;", "<?php $a = array(1, array(2)); $b = $a[0]; [$x, $y] = $a;"},
{"list default short", ListSyntax{}, "<?php list($a, $b) = $x;", "<?php [$a, $b] = $x;"},
{"list long", ListSyntax{}.WithConfig(map[string]any{"syntax": "long"}).(fixerRule), "<?php [$a, [$b]] = $x; $c = [1]; foreach ($y as [$d]) {}", "<?php list($a, list($b)) = $x; $c = [1]; foreach ($y as list($d)) {}"},
{"before comma default", NoWhitespaceBeforeCommaInArray{}, "<?php $a = [1 , 2];", "<?php $a = [1, 2];"},
{"before comma heredoc kept", NoWhitespaceBeforeCommaInArray{}.WithConfig(map[string]any{"after_heredoc": false}).(fixerRule), "<?php $a = [<<<EOT\nx\nEOT , 2];", "<?php $a = [<<<EOT\nx\nEOT , 2];"},
{"before comma heredoc fixed", NoWhitespaceBeforeCommaInArray{}.WithConfig(map[string]any{"after_heredoc": true}).(fixerRule), "<?php $a = [<<<EOT\nx\nEOT , 2];", "<?php $a = [<<<EOT\nx\nEOT, 2];"},
{"after comma default", WhitespaceAfterCommaInArray{}, "<?php $a = [1, 2,3];", "<?php $a = [1, 2, 3];"},
{"after comma single space", WhitespaceAfterCommaInArray{}.WithConfig(map[string]any{"ensure_single_space": true}).(fixerRule), "<?php $a = [1, 2,3];", "<?php $a = [1, 2, 3];"},
}
for _, c := range cases {
got, _ := apply(t, c.rule, c.src)
if got != c.want {
t.Errorf("%s: got=%q want=%q", c.name, got, c.want)
}
}
}
Loading
Loading