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="" 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 := ""/"?->" 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, " 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{}, "