From 3bf9fd2d8564038ef3f930ead0adda014e78a284 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 3 Oct 2026 21:02:02 +0200 Subject: [PATCH 1/5] Insert space in empty for condition, keep it before the next semicolon php-cs-fixer's SpaceAfterSemicolon inserts a space after a ";" that is followed by another ";" (the empty condition of a for() header), only suppressing it before ")". NoSinglelineWhitespaceBeforeSemicolons must then leave that space alone when the preceding token is itself a ";". --- .../rules/no_singleline_whitespace_before_semicolons.go | 5 +++++ blink/internal/fixer/rules/rules_test.go | 7 ++++--- blink/internal/fixer/rules/space_after_semicolon.go | 4 +++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/blink/internal/fixer/rules/no_singleline_whitespace_before_semicolons.go b/blink/internal/fixer/rules/no_singleline_whitespace_before_semicolons.go index 48621407af..3558be75d8 100644 --- a/blink/internal/fixer/rules/no_singleline_whitespace_before_semicolons.go +++ b/blink/internal/fixer/rules/no_singleline_whitespace_before_semicolons.go @@ -30,6 +30,11 @@ func (NoSinglelineWhitespaceBeforeSemicolons) Fix(s *tokens.Stream) bool { } prev := s.At(i - 1) if prev.Kind == token.Whitespace && !strings.ContainsAny(prev.Value, "\n\r") { + // keep the space before a ";" that follows another ";" - the empty + // condition of a for() header ("for ($i = 0; ; ++$i)") + if p, ok := prevSignificant(s, i-1); ok && p.Kind == token.Punct && p.Value == ";" { + continue + } s.RemoveAt(i - 1) changed = true } diff --git a/blink/internal/fixer/rules/rules_test.go b/blink/internal/fixer/rules/rules_test.go index e48af016b6..465d1eab0f 100644 --- a/blink/internal/fixer/rules/rules_test.go +++ b/blink/internal/fixer/rules/rules_test.go @@ -33,9 +33,10 @@ func TestSpaceAfterSemicolon(t *testing.T) { if want := " "for (; ; )"); only ")" suppresses it + if next.Kind == token.Punct && next.Value == ")" { continue } s.InsertAt(i+1, token.Token{Kind: token.Whitespace, Value: " "}) From 444795c76a50f5abfcf35ae5c19e9628252f94a5 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 3 Oct 2026 21:05:52 +0200 Subject: [PATCH 2/5] Keep the space between a bracket and a trailing comment SpacesInsideParentheses and TrimArraySpaces removed the single-line space next to a comment ("if ( // note", "[ // note"); php-cs-fixer leaves it, so guard both against an adjacent comment token. --- blink/internal/fixer/rules/constructs.go | 3 ++- blink/internal/fixer/rules/token_batch.go | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/blink/internal/fixer/rules/constructs.go b/blink/internal/fixer/rules/constructs.go index d82d08f5d1..f70a687ccf 100644 --- a/blink/internal/fixer/rules/constructs.go +++ b/blink/internal/fixer/rules/constructs.go @@ -261,7 +261,8 @@ func (f NoSpacesInsideParenthesis) Fix(s *tokens.Stream) bool { for i := 0; i < s.Len(); i++ { t := s.At(i) if t.Kind == token.Punct && t.Value == "(" && - i+1 < s.Len() && s.At(i+1).Kind == token.Whitespace && !hasNewline(s.At(i+1).Value) { + i+1 < s.Len() && s.At(i+1).Kind == token.Whitespace && !hasNewline(s.At(i+1).Value) && + !(i+2 < s.Len() && isComment(s.At(i+2))) { // keep the space before a trailing comment s.RemoveAt(i + 1) changed = true } diff --git a/blink/internal/fixer/rules/token_batch.go b/blink/internal/fixer/rules/token_batch.go index f4ceb433be..ad46e24092 100644 --- a/blink/internal/fixer/rules/token_batch.go +++ b/blink/internal/fixer/rules/token_batch.go @@ -118,11 +118,14 @@ func (TrimArraySpaces) Fix(s *tokens.Stream) bool { if closeIdx < 0 { continue } - if closeIdx-1 > i && s.At(closeIdx-1).Kind == token.Whitespace && !hasNewline(s.At(closeIdx-1).Value) { + // a space kept next to a comment is left alone ("[ // note" / "/* note */ ]") + if closeIdx-1 > i && s.At(closeIdx-1).Kind == token.Whitespace && !hasNewline(s.At(closeIdx-1).Value) && + !(closeIdx-2 > i && isComment(s.At(closeIdx-2))) { s.RemoveAt(closeIdx - 1) changed = true } - if i+1 < s.Len() && s.At(i+1).Kind == token.Whitespace && !hasNewline(s.At(i+1).Value) { + if i+1 < s.Len() && s.At(i+1).Kind == token.Whitespace && !hasNewline(s.At(i+1).Value) && + !(i+2 < s.Len() && isComment(s.At(i+2))) { s.RemoveAt(i + 1) changed = true } From bba8780ba3d3902efb0fb21053b3d3e223937619 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 3 Oct 2026 21:10:43 +0200 Subject: [PATCH 3/5] Lex a leading-dot float (.1) as one number PHP's context-free scanner matches ".1" as a single DNUMBER; blink split it into "." and "1", so a concat-spacing rule turned "+ .1" into "+ . 1". --- blink/internal/lexer/lexer.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/blink/internal/lexer/lexer.go b/blink/internal/lexer/lexer.go index c294b26545..bcc294fa18 100644 --- a/blink/internal/lexer/lexer.go +++ b/blink/internal/lexer/lexer.go @@ -137,7 +137,8 @@ func (l *lexer) lexPHP() { l.lexString(start, '\'') case c == '"': l.lexString(start, '"') - case isDigit(c): + case isDigit(c) || (c == '.' && l.pos+1 < len(l.src) && isDigit(l.src[l.pos+1])): + // a leading-dot float (".1") is one DNUMBER in PHP's context-free scanner for l.pos < len(l.src) && isNumber(l.src[l.pos]) { l.pos++ } From ecadd957b9eb3605bb5e785211e5f9535a70716b Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 3 Oct 2026 21:11:56 +0200 Subject: [PATCH 4/5] Do not space commas in a destructuring assignment target php-cs-fixer tags "[$a,$b] = ..." as destructuring, not an array, so WhitespaceAfterCommaInArray leaves its commas alone. Track a destructuring "[" (matching "]" followed by "=", or nested in one) on the bracket stack. --- blink/internal/fixer/rules/array_notation.go | 21 +++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/blink/internal/fixer/rules/array_notation.go b/blink/internal/fixer/rules/array_notation.go index 3c78f5edc0..fcf22786bc 100644 --- a/blink/internal/fixer/rules/array_notation.go +++ b/blink/internal/fixer/rules/array_notation.go @@ -250,6 +250,17 @@ func topBracketIsArray(stack []string) bool { return len(stack) > 0 && stack[len(stack)-1] == "[" } +// isDestructuringAssignOpen reports whether the "[" at open is a short-list +// destructuring target - its matching "]" is directly followed by a single "=". +func isDestructuringAssignOpen(s *tokens.Stream, open int) bool { + c := s.MatchForward(open) + if c < 0 { + return false + } + n := nextSignificantIndex(s, c) + return n >= 0 && s.At(n).Kind == token.Punct && s.At(n).Value == "=" +} + // 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 @@ -340,7 +351,15 @@ func (f WhitespaceAfterCommaInArray) Fix(s *tokens.Stream) bool { continue } switch t.Value { - case "(", "[", "{": + case "[": + // a destructuring target ("[$a,$b] = ...", or nested in one) is not an + // array literal, so php-cs-fixer does not space its commas + if (len(stack) > 0 && stack[len(stack)-1] == "d[") || isDestructuringAssignOpen(s, i) { + stack = append(stack, "d[") + } else { + stack = append(stack, "[") + } + case "(", "{": stack = append(stack, t.Value) case ")", "]", "}": if len(stack) > 0 { From 3dc2c29a1c48150fda3222b2ca42b048d1a90298 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sun, 4 Oct 2026 08:12:23 +0200 Subject: [PATCH 5/5] Satisfy staticcheck QF1001 with a commentAt helper --- blink/internal/fixer/rules/constructs.go | 2 +- blink/internal/fixer/rules/gen_arrays.go | 5 +++++ blink/internal/fixer/rules/token_batch.go | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/blink/internal/fixer/rules/constructs.go b/blink/internal/fixer/rules/constructs.go index f70a687ccf..dde73cf984 100644 --- a/blink/internal/fixer/rules/constructs.go +++ b/blink/internal/fixer/rules/constructs.go @@ -262,7 +262,7 @@ func (f NoSpacesInsideParenthesis) Fix(s *tokens.Stream) bool { t := s.At(i) if t.Kind == token.Punct && t.Value == "(" && i+1 < s.Len() && s.At(i+1).Kind == token.Whitespace && !hasNewline(s.At(i+1).Value) && - !(i+2 < s.Len() && isComment(s.At(i+2))) { // keep the space before a trailing comment + !commentAt(s, i+2) { // keep the space before a trailing comment s.RemoveAt(i + 1) changed = true } diff --git a/blink/internal/fixer/rules/gen_arrays.go b/blink/internal/fixer/rules/gen_arrays.go index 02d79e89b7..94a007df8e 100644 --- a/blink/internal/fixer/rules/gen_arrays.go +++ b/blink/internal/fixer/rules/gen_arrays.go @@ -145,6 +145,11 @@ func isComment(t token.Token) bool { return t.Kind == token.Comment || t.Kind == token.DocComment } +// commentAt reports whether index i is in range and holds a comment token. +func commentAt(s *tokens.Stream, i int) bool { + return i >= 0 && i < s.Len() && isComment(s.At(i)) +} + func isLineComment(t token.Token) bool { if t.Kind != token.Comment { return false diff --git a/blink/internal/fixer/rules/token_batch.go b/blink/internal/fixer/rules/token_batch.go index ad46e24092..910c37bdc8 100644 --- a/blink/internal/fixer/rules/token_batch.go +++ b/blink/internal/fixer/rules/token_batch.go @@ -120,12 +120,12 @@ func (TrimArraySpaces) Fix(s *tokens.Stream) bool { } // a space kept next to a comment is left alone ("[ // note" / "/* note */ ]") if closeIdx-1 > i && s.At(closeIdx-1).Kind == token.Whitespace && !hasNewline(s.At(closeIdx-1).Value) && - !(closeIdx-2 > i && isComment(s.At(closeIdx-2))) { + !commentAt(s, closeIdx-2) { s.RemoveAt(closeIdx - 1) changed = true } if i+1 < s.Len() && s.At(i+1).Kind == token.Whitespace && !hasNewline(s.At(i+1).Value) && - !(i+2 < s.Len() && isComment(s.At(i+2))) { + !commentAt(s, i+2) { s.RemoveAt(i + 1) changed = true }