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
2 changes: 1 addition & 1 deletion .github/workflows/blink_parity_mautic.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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=""
Expand Down
72 changes: 70 additions & 2 deletions blink/internal/fixer/rules/braces_position.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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
}

Expand All @@ -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
Expand Down Expand Up @@ -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).
Expand Down
18 changes: 18 additions & 0 deletions blink/internal/fixer/rules/braces_position_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,22 @@ func TestBracesPositionConfig(t *testing.T) {
if want := "<?php\nif ($a)\n{\n echo 1;\n}\n"; !changed || got != want {
t.Fatalf("control next_line: changed=%v got=%q", changed, got)
}

// allow_single_line_anonymous_functions=false expands a single-line closure body
noSingle := BracesPosition{}.WithConfig(map[string]any{"allow_single_line_anonymous_functions": false}).(fixerRule)
got, changed = apply(t, noSingle, "<?php\n$f = function ($x) { return $x; };\n")
if want := "<?php\n$f = function ($x) {\n return $x;\n};\n"; !changed || got != want {
t.Fatalf("closure expand: changed=%v got=%q", changed, got)
}
// an empty body or a comment on the brace line is left alone
if _, changed := apply(t, noSingle, "<?php\n$f = function ($x) {};\n"); changed {
t.Fatal("empty closure body must be left alone")
}
if _, changed := apply(t, noSingle, "<?php\n$f = function ($x) { // note\n return $x;\n};\n"); changed {
t.Fatal("comment on brace line must be left alone")
}
// default (option unset) keeps a single-line closure body
if _, changed := apply(t, BracesPosition{}, "<?php\n$f = function ($x) { return $x; };\n"); changed {
t.Fatal("default must keep single-line closure body")
}
}
4 changes: 3 additions & 1 deletion blink/internal/fixer/rules/operator_linebreak.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 {
Expand Down
12 changes: 12 additions & 0 deletions blink/internal/fixer/rules/operator_linebreak_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, "<?php\n$x = $this->model->\n call($a);\n")
if want := "<?php\n$x = $this->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, "<?php\n$x = Foo::\n BAR;\n")
if want := "<?php\n$x = Foo\n ::BAR;\n"; !changed || got != want {
t.Fatalf(":: move: changed=%v got=%q", changed, got)
}

if _, changed := apply(t, f, "<?php\n$a = $b\n || $c;\n"); changed {
t.Fatal("already-correct must be a no-op")
}
Expand Down
6 changes: 4 additions & 2 deletions blink/internal/fixer/rules/token_batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
4 changes: 4 additions & 0 deletions blink/internal/fixer/rules/token_batch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ func TestTrimArraySpaces(t *testing.T) {
if _, changed := apply(t, TrimArraySpaces{}, "<?php $a = [\n 1,\n];"); changed {
t.Fatal("multi-line array must be kept")
}
// a space after a trailing comma before "]" is kept ("1, ]"), matching php-cs-fixer
if _, changed := apply(t, TrimArraySpaces{}, "<?php $a = [1, 2, ];"); changed {
t.Fatal("space after trailing comma must be kept")
}
idempotent(t, TrimArraySpaces{}, "<?php $a = [1, 2];")
}

Expand Down
Loading