diff --git a/.github/workflows/blink_parity_mautic.yaml b/.github/workflows/blink_parity_mautic.yaml index a9c2f04b13..c18b354024 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.8 + MAX_DIFF_PERCENT=0.6 total=0 differ=0 differing_files="" diff --git a/blink/internal/fixer/rules/array_list_item_newline.go b/blink/internal/fixer/rules/array_list_item_newline.go index fcbce91066..9a4592f9ee 100644 --- a/blink/internal/fixer/rules/array_list_item_newline.go +++ b/blink/internal/fixer/rules/array_list_item_newline.go @@ -1,6 +1,8 @@ package rules import ( + "slices" + "blink/internal/token" "blink/internal/tokens" ) @@ -23,6 +25,9 @@ func (ArrayListItemNewline) SourceURL() string { func (ArrayListItemNewline) Fix(s *tokens.Stream) bool { changed := false + // openers of enclosing list arrays whose last element is an associative array + // we expand here; broken after the main loop, high-to-low (see newliner note) + var outerOpenerBreaks []int for open := 0; open < s.Len(); open++ { if s.At(open).Kind != token.Punct || s.At(open).Value != "[" { continue @@ -49,6 +54,13 @@ func (ArrayListItemNewline) Fix(s *tokens.Stream) bool { } continue } + // expanding this single-line associative array from inside an enclosing list + // (as its last element) also breaks the list's opener upstream - the two + // inserts shift the list's stored end so ArrayOpenerAndCloserNewline treats + // it as indexed for the opener. Decide before reflowing shifts indices. + if enc := enclosingListForLastAssoc(s, open, closeIdx); enc >= 0 { + outerOpenerBreaks = append(outerOpenerBreaks, enc) + } // a multiline array carries a trailing comma; add it before reflowing so // the last item ends up on its own line with the comma if last := sigPrev(s, closeIdx); last > open && s.At(last).Value != "," { @@ -60,9 +72,75 @@ func (ArrayListItemNewline) Fix(s *tokens.Stream) bool { changed = true } } + // break the recorded enclosing-list openers, high-to-low so each insert leaves + // the lower, still-pending positions valid + slices.SortFunc(outerOpenerBreaks, func(a, b int) int { return b - a }) + for _, enc := range outerOpenerBreaks { + if editSlotAfter(s, enc, "\n") { + changed = true + } + } return changed } +// enclosingListForLastAssoc returns the opener of the array that directly encloses +// the associative array [open, closeIdx] as its last element, when that enclosing +// array is a plain list (no top-level "=>"), its first element is not an array, +// and its opener is not already on its own line. Otherwise -1. +func enclosingListForLastAssoc(s *tokens.Stream, open, closeIdx int) int { + // nearest enclosing bracket of open + depth := 0 + enc := -1 + for j := open - 1; j >= 0; j-- { + t := s.At(j) + if t.Kind != token.Punct { + continue + } + switch t.Value { + case ")", "]", "}": + depth++ + case "(", "{": + if depth == 0 { + return -1 // directly inside a call/block, not an array + } + depth-- + case "[": + if depth == 0 { + enc = j + } else { + depth-- + } + } + if enc >= 0 { + break + } + } + if enc < 0 || !isArrayLiteralOpen(s, enc) || isDestructuringAssignOpen(s, enc) { + return -1 + } + encClose := s.MatchForward(enc) + if encClose <= closeIdx || arrayHasTopLevelArrow(s, enc, encClose) { + return -1 // not an enclosing list + } + // the assoc must be the last element of the enclosing list + lastEnd := sigPrev(s, encClose) + if lastEnd >= 0 && s.At(lastEnd).Kind == token.Punct && s.At(lastEnd).Value == "," { + lastEnd = sigPrev(s, lastEnd) + } + if lastEnd != closeIdx { + return -1 + } + // first element must not itself be an array opener + first := sigNext(s, enc) + if first == closeIdx || (s.At(first).Kind == token.Punct && s.At(first).Value == "[" && isArrayLiteralOpen(s, first)) { + return -1 + } + if enc+1 < s.Len() && s.At(enc+1).Kind == token.Whitespace && hasNewline(s.At(enc+1).Value) { + return -1 // opener already on its own line + } + return enc +} + // splitTopLevelCommas breaks every top-level comma of a multiline array onto a // new line when it still shares its line with the next item, mirroring Symplify's // ArrayItemNewliner. A comma already followed by a newline, by a comment, or by a diff --git a/blink/internal/fixer/rules/array_list_item_newline_test.go b/blink/internal/fixer/rules/array_list_item_newline_test.go index bbd19720dd..f46bf8e9db 100644 --- a/blink/internal/fixer/rules/array_list_item_newline_test.go +++ b/blink/internal/fixer/rules/array_list_item_newline_test.go @@ -43,4 +43,17 @@ func TestArrayListItemNewline(t *testing.T) { if want := " 1,\n 'y' => [\n 'z' => 2,\n ],\n'w' => 3,\n];"; !changed || got != want { t.Fatalf("multiline split: changed=%v got=%q", changed, got) } + + // a list array whose last element is a single-line associative array: expanding + // the inner array also breaks the outer list's opener (not its closer) + got, changed = apply(t, f, " 1, 'm' => 2]];") + if want := " 1,\n 'm' => 2,\n]];"; !changed || got != want { + t.Fatalf("outer opener break: changed=%v got=%q", changed, got) + } + + // but when that inner associative array is already multiline, the outer list + // opener is left alone (no upstream position shift happens) + if _, changed := apply(t, f, " 1,\n 'm' => 2,\n]];"); changed { + t.Fatal("already-multiline inner must not break the outer opener") + } }