From 92b2e64438db8443f3841fc8c2cd960397ef764c Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 5 Oct 2026 13:55:46 +0200 Subject: [PATCH 1/4] Move object operators and :: to the line start in OperatorLinebreak php-cs-fixer's OperatorLinebreak also moves "->", "?->" and "::" (object operator kinds + T_PAAMAYIM_NEKUDOTAYIM) to the beginning of the next line when they end a line in a multiline expression. blink excluded them, leaving a wrapped method chain with "->" trailing; now they are moved like ECS. --- blink/internal/fixer/rules/operator_linebreak.go | 4 +++- .../internal/fixer/rules/operator_linebreak_test.go | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/blink/internal/fixer/rules/operator_linebreak.go b/blink/internal/fixer/rules/operator_linebreak.go index 3dcf0ad100..6f2c72c147 100644 --- a/blink/internal/fixer/rules/operator_linebreak.go +++ b/blink/internal/fixer/rules/operator_linebreak.go @@ -16,7 +16,8 @@ import ( // argument, switch case, goto label or alternative-syntax colon is left alone; a // "?" that is a nullable type marker is left alone; a "|"/"&" that is a union or // intersection type separator is left alone; and a "&" that is a return-ref or a -// reference is left alone. Object operators "->"/"?->" are not operators here. +// reference is left alone. Object operators "->"/"?->" and "::" are moved too, +// matching php-cs-fixer's getObjectOperatorKinds() + T_PAAMAYIM_NEKUDOTAYIM. type OperatorLinebreak struct{} // operatorLinebreakPunct are the always-unambiguous operator tokens. @@ -28,6 +29,7 @@ var operatorLinebreakPunct = map[string]bool{ "-=": true, "*=": true, "/=": true, "%=": true, "**=": true, "&=": true, "|=": true, "^=": true, "<<=": true, ">>=": true, "??=": true, "^": true, "<<": true, ">>": true, "|": true, "&": true, ":": true, "?": true, + "->": true, "?->": true, "::": true, } func (OperatorLinebreak) Name() string { diff --git a/blink/internal/fixer/rules/operator_linebreak_test.go b/blink/internal/fixer/rules/operator_linebreak_test.go index b4099a56ad..f9e5818323 100644 --- a/blink/internal/fixer/rules/operator_linebreak_test.go +++ b/blink/internal/fixer/rules/operator_linebreak_test.go @@ -15,6 +15,18 @@ func TestOperatorLinebreak(t *testing.T) { t.Fatalf(". move: changed=%v got=%q", changed, got) } + // an object operator "->" at the end of a line moves to the start of the next + got, changed = apply(t, f, "model->\n call($a);\n") + if want := "model\n ->call($a);\n"; !changed || got != want { + t.Fatalf("-> move: changed=%v got=%q", changed, got) + } + + // "::" at the end of a line moves too + got, changed = apply(t, f, " Date: Mon, 5 Oct 2026 14:00:13 +0200 Subject: [PATCH 2/4] Expand single-line closure bodies when configured Honor BracesPosition's allow_single_line_anonymous_functions=false: a single-line anonymous function body is put on its own lines, matching php-cs-fixer. An empty body, one already on its own line, or one sharing the brace line with a comment is left alone. --- blink/internal/fixer/rules/braces_position.go | 72 ++++++++++++++++++- .../rules/braces_position_config_test.go | 18 +++++ 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/blink/internal/fixer/rules/braces_position.go b/blink/internal/fixer/rules/braces_position.go index 7aeac8fd43..c8ec358283 100644 --- a/blink/internal/fixer/rules/braces_position.go +++ b/blink/internal/fixer/rules/braces_position.go @@ -21,6 +21,11 @@ type BracesPosition struct { anonymousClassesOpeningBrace string anonymousFunctionsOpeningBrace string controlStructuresOpeningBrace string + // allowSingleLineAnonFnsSet records whether the option was given; the value + // defaults to true (php-cs-fixer default), so a false value expands a + // single-line closure body onto its own lines. + allowSingleLineAnonFns bool + allowSingleLineAnonFnsSet bool } const ( @@ -44,9 +49,12 @@ func (f BracesPosition) WithConfig(config map[string]any) fixer.Fixer { if v, ok := config["control_structures_opening_brace"].(string); ok { f.controlStructuresOpeningBrace = v } - // allow_single_line_empty_anonymous_classes and - // allow_single_line_anonymous_functions are accepted but not applied: blink + // allow_single_line_empty_anonymous_classes is accepted but not applied: blink // already keeps single-line empty bodies (matching the default true). + if v, ok := config["allow_single_line_anonymous_functions"].(bool); ok { + f.allowSingleLineAnonFns = v + f.allowSingleLineAnonFnsSet = true + } return f } @@ -60,6 +68,13 @@ func (BracesPosition) SourceURL() string { func (f BracesPosition) Fix(s *tokens.Stream) bool { changed := false + // allow_single_line_anonymous_functions=false expands a single-line closure + // body onto its own lines (statement_indentation then aligns it) + if f.allowSingleLineAnonFnsSet && !f.allowSingleLineAnonFns { + if f.expandSingleLineClosureBodies(s) { + changed = true + } + } for i := 0; i < s.Len(); i++ { if s.At(i).Kind != token.Punct || s.At(i).Value != "{" { continue @@ -118,6 +133,59 @@ func (f BracesPosition) Fix(s *tokens.Stream) bool { return changed } +// expandSingleLineClosureBodies puts the body of a single-line anonymous function +// ("function () { ... }") on its own lines, matching php-cs-fixer with +// allow_single_line_anonymous_functions=false. An empty body or one that already +// starts on a new line is left alone; statement_indentation aligns the rest. +func (f BracesPosition) expandSingleLineClosureBodies(s *tokens.Stream) bool { + changed := false + for i := s.Len() - 1; i >= 0; i-- { + if s.At(i).Kind != token.Punct || s.At(i).Value != "{" { + continue + } + if kind, _ := classifyBrace(s, i); kind != braceClosure { + continue + } + closeIdx := s.MatchForward(i) + if closeIdx < 0 || sigNext(s, i) == closeIdx { + continue // empty body + } + if i+1 < s.Len() && s.At(i+1).Kind == token.Whitespace && hasNewline(s.At(i+1).Value) { + continue // body already starts on its own line + } + if braceHasCommentOnOpenLine(s, i, closeIdx) { + continue // a comment shares the "{" line; php-cs-fixer leaves it + } + depth := braceDepthAt(s, i) + if editSlotBefore(s, closeIdx, "\n"+strings.Repeat(" ", depth)) { + changed = true + } + if editSlotAfter(s, i, "\n"+strings.Repeat(" ", depth+1)) { + changed = true + } + } + return changed +} + +// braceHasCommentOnOpenLine reports whether a comment appears between the "{" at +// open and the first newline (i.e. on the same line as the brace). +func braceHasCommentOnOpenLine(s *tokens.Stream, open, closeIdx int) bool { + for j := open + 1; j < closeIdx; j++ { + t := s.At(j) + if t.Kind == token.Whitespace { + if hasNewline(t.Value) { + return false + } + continue + } + if t.Kind == token.Comment || t.Kind == token.DocComment { + return true + } + return false + } + return false +} + // wantNextLine resolves the configured position for a brace category to whether // the "{" goes on the next line. handle is false for closures left at their // default (blink does not move anonymous-function braces unless configured). diff --git a/blink/internal/fixer/rules/braces_position_config_test.go b/blink/internal/fixer/rules/braces_position_config_test.go index 12f2be1ff4..3ffce433fb 100644 --- a/blink/internal/fixer/rules/braces_position_config_test.go +++ b/blink/internal/fixer/rules/braces_position_config_test.go @@ -22,4 +22,22 @@ func TestBracesPositionConfig(t *testing.T) { if want := " Date: Mon, 5 Oct 2026 14:03:36 +0200 Subject: [PATCH 3/4] Keep the space after a trailing comma before a closing bracket TrimArraySpaces removed the single-line space before "]" even after a trailing comma ("1, ]" -> "1,]"); php-cs-fixer keeps it when the preceding token is a comma. Guard the trailing-space trim against a preceding comma. --- blink/internal/fixer/rules/token_batch.go | 6 ++++-- blink/internal/fixer/rules/token_batch_test.go | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/blink/internal/fixer/rules/token_batch.go b/blink/internal/fixer/rules/token_batch.go index 910c37bdc8..d3c8cc0b55 100644 --- a/blink/internal/fixer/rules/token_batch.go +++ b/blink/internal/fixer/rules/token_batch.go @@ -118,9 +118,11 @@ func (TrimArraySpaces) Fix(s *tokens.Stream) bool { if closeIdx < 0 { continue } - // a space kept next to a comment is left alone ("[ // note" / "/* note */ ]") + // a space kept next to a comment is left alone ("[ // note" / "/* note */ ]"); + // a space after a trailing comma ("1, ]") is also kept, matching php-cs-fixer + prevComma := closeIdx-2 > i && s.At(closeIdx-2).Kind == token.Punct && s.At(closeIdx-2).Value == "," if closeIdx-1 > i && s.At(closeIdx-1).Kind == token.Whitespace && !hasNewline(s.At(closeIdx-1).Value) && - !commentAt(s, closeIdx-2) { + !commentAt(s, closeIdx-2) && !prevComma { s.RemoveAt(closeIdx - 1) changed = true } diff --git a/blink/internal/fixer/rules/token_batch_test.go b/blink/internal/fixer/rules/token_batch_test.go index 0779682054..5127c01e2a 100644 --- a/blink/internal/fixer/rules/token_batch_test.go +++ b/blink/internal/fixer/rules/token_batch_test.go @@ -49,6 +49,10 @@ func TestTrimArraySpaces(t *testing.T) { if _, changed := apply(t, TrimArraySpaces{}, " Date: Mon, 5 Oct 2026 14:05:52 +0200 Subject: [PATCH 4/4] Ratchet mautic parity gate to 0.15% --- .github/workflows/blink_parity_mautic.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/blink_parity_mautic.yaml b/.github/workflows/blink_parity_mautic.yaml index 05d928cdf8..6f00d34e8b 100644 --- a/.github/workflows/blink_parity_mautic.yaml +++ b/.github/workflows/blink_parity_mautic.yaml @@ -95,7 +95,7 @@ jobs: - name: Compare the two trees run: | # ratchet gate: fail once the differing share crosses this; lower it as parity improves - MAX_DIFF_PERCENT=0.2 + MAX_DIFF_PERCENT=0.15 total=0 differ=0 differing_files=""