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.25
MAX_DIFF_PERCENT=0.2
total=0
differ=0
differing_files=""
Expand Down
25 changes: 25 additions & 0 deletions blink/internal/fixer/rules/class_notation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,31 @@ func TestOrderedClassElementsSimple(t *testing.T) {
}
}

func TestOrderedClassElementsOrderUseTraitOnly(t *testing.T) {
// order ["use_trait"] groups trait uses first and leaves every other member
// in its source order (const stays after the property here)
src := "<?php class A {\n" +
" public $prop;\n" +
" const X = 1;\n" +
" use SomeTrait;\n" +
"}"
want := "<?php class A {\n" +
" use SomeTrait;\n" +
" public $prop;\n" +
" const X = 1;\n" +
"}"
f := OrderedClassElements{}.WithConfig(map[string]any{"order": []any{"use_trait"}}).(fixerRule)
got, changed := apply(t, f, src)
if !changed || got != want {
t.Fatalf("changed=%v\n got: %q\nwant: %q", changed, got, want)
}
// a class with the property before the const and no trait is left untouched
noTrait := "<?php class B {\n public $prop;\n const X = 1;\n}"
if out, changed := apply(t, f, noTrait); changed || out != noTrait {
t.Fatalf("no-trait class must be a no-op: changed=%v got=%q", changed, out)
}
}

func TestOrderedClassElementsAlreadyOrdered(t *testing.T) {
src := "<?php class A {\n" +
" const X = 1;\n" +
Expand Down
65 changes: 54 additions & 11 deletions blink/internal/fixer/rules/ordered_class_elements.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package rules

import (
"sort"
"strings"

"blink/internal/fixer"
"blink/internal/token"
"blink/internal/tokens"
)
Expand All @@ -20,7 +22,15 @@ const (
// constants, properties, then methods - keeping the original order within each
// group. It is deliberately conservative: any comment, doc comment or attribute
// in the body makes it skip the whole class so leading trivia is never detached.
type OrderedClassElements struct{}
//
// The "order" option is honoured at group granularity: a listed group keeps its
// configured index and every unlisted group shares the next index, so a stable
// sort moves only the listed groups and leaves the rest in source order (e.g.
// order ["use_trait"] groups trait uses first and touches nothing else).
type OrderedClassElements struct {
ranks [4]int
configured bool
}

func (OrderedClassElements) Name() string {
return `PhpCsFixer\Fixer\ClassNotation\OrderedClassElementsFixer`
Expand All @@ -30,14 +40,48 @@ func (OrderedClassElements) SourceURL() string {
return "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/src/Fixer/ClassNotation/OrderedClassElementsFixer.php"
}

func (OrderedClassElements) Fix(s *tokens.Stream) bool {
func (f OrderedClassElements) WithConfig(config map[string]any) fixer.Fixer {
raw, ok := config["order"].([]any)
if !ok {
return f
}
for i := range f.ranks {
f.ranks[i] = len(raw) // unlisted groups sort after all listed ones
}
nameGroup := map[string]int{
"use_trait": groupTraitUse,
"constant": groupConst,
"property": groupProperty,
"method": groupMethod,
}
for pos, v := range raw {
if name, ok := v.(string); ok {
if g, ok := nameGroup[strings.ToLower(name)]; ok {
f.ranks[g] = pos
}
}
}
f.configured = true
return f
}

// groupRank is the configured sort rank of a group, or the default group order
// (trait use, constants, properties, methods) when unconfigured.
func (f OrderedClassElements) groupRank(g int) int {
if f.configured {
return f.ranks[g]
}
return g
}

func (f OrderedClassElements) Fix(s *tokens.Stream) bool {
changed := false
i := 0
for i < s.Len() {
t := s.At(i)
if t.Kind == token.Punct && t.Value == "{" {
if kind, _ := classifyBrace(s, i); kind == braceClassLike {
if reorderClassBody(s, i) {
if f.reorderClassBody(s, i) {
changed = true
i = 0 // members moved; restart - each class sorts once so this ends
continue
Expand All @@ -49,7 +93,7 @@ func (OrderedClassElements) Fix(s *tokens.Stream) bool {
return changed
}

func reorderClassBody(s *tokens.Stream, open int) bool {
func (f OrderedClassElements) reorderClassBody(s *tokens.Stream, open int) bool {
closeIdx := s.MatchForward(open)
if closeIdx < 0 {
return false
Expand Down Expand Up @@ -87,14 +131,13 @@ func reorderClassBody(s *tokens.Stream, open int) bool {
}
}
}
order := make([]int, 0, len(starts))
for g := groupTraitUse; g <= groupMethod; g++ {
for idx, gg := range groups {
if gg == g {
order = append(order, idx)
}
}
order := make([]int, len(starts))
for i := range order {
order[i] = i
}
sort.SliceStable(order, func(a, b int) bool {
return f.groupRank(groups[order[a]]) < f.groupRank(groups[order[b]])
})
if isIdentityOrder(order) {
return false
}
Expand Down
Loading