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
63 changes: 47 additions & 16 deletions blink/internal/fixer/rules/standalone_line_promoted_property.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import (

// Symplify: https://github.com/symplify/coding-standard/blob/main/src/Fixer/Spacing/StandaloneLinePromotedPropertyFixer.php
//
// StandaloneLinePromotedProperty puts each constructor parameter on its own line:
// a "function __construct(...)" with at least one parameter becomes fully
// multiline, "(" and ")" on their own lines. Matches symplify's constructor
// param newliners (promoted and plain), which together break every __construct.
// StandaloneLinePromotedProperty is a file-level fixer: when any constructor in
// the file declares a promoted property, it puts each parameter of every
// constructor in the file on its own line ("(" and ")" on their own lines),
// promoted or plain. A file with no promoted constructor is left untouched (plain
// constructors with 4+ params are then handled by StandaloneLinePlainConstructorParam).
type StandaloneLinePromotedProperty struct{}

func (StandaloneLinePromotedProperty) Name() string {
Expand All @@ -24,33 +25,63 @@ func (StandaloneLinePromotedProperty) SourceURL() string {
}

func (StandaloneLinePromotedProperty) Fix(s *tokens.Stream) bool {
if !fileHasPromotedConstructor(s) {
return false // the fixer only runs when a promoted constructor is present
}
changed := false
for i := 0; i < s.Len(); i++ {
if s.At(i).Kind != token.Ident || !strings.EqualFold(s.At(i).Value, "__construct") {
continue
}
// must be a declaration: "function __construct("
if p := sigPrev(s, i); p < 0 || s.At(p).Kind != token.Keyword || strings.ToLower(s.At(p).Value) != "function" {
continue
}
open := sigNext(s, i)
if open < 0 || s.At(open).Kind != token.Punct || s.At(open).Value != "(" {
open := constructorParamOpen(s, i)
if open < 0 {
continue
}
closeIdx := s.MatchForward(open)
if closeIdx < 0 || sigNext(s, open) == closeIdx {
continue // no parameters
}
if !hasPromotedParam(s, open, closeIdx) {
continue // only constructor property promotion is split out
}
if reflowParen(s, open, closeIdx, false) {
changed = true
}
}
return changed
}

// fileHasPromotedConstructor reports whether any constructor in the file declares
// a promoted property. Symplify's StandaloneLinePromotedPropertyFixer is a file-
// level candidate: once one promoted constructor exists it breaks the params of
// every constructor in the file onto standalone lines, promoted or plain.
func fileHasPromotedConstructor(s *tokens.Stream) bool {
for i := 0; i < s.Len(); i++ {
open := constructorParamOpen(s, i)
if open < 0 {
continue
}
closeIdx := s.MatchForward(open)
if closeIdx < 0 {
continue
}
if hasPromotedParam(s, open, closeIdx) {
return true
}
}
return false
}

// constructorParamOpen returns the index of the "(" opening the parameter list of
// a "function __construct(" declaration at i, or -1 when i is not such a name.
func constructorParamOpen(s *tokens.Stream, i int) int {
if s.At(i).Kind != token.Ident || !strings.EqualFold(s.At(i).Value, "__construct") {
return -1
}
if p := sigPrev(s, i); p < 0 || s.At(p).Kind != token.Keyword || strings.ToLower(s.At(p).Value) != "function" {
return -1
}
open := sigNext(s, i)
if open < 0 || s.At(open).Kind != token.Punct || s.At(open).Value != "(" {
return -1
}
return open
}

// hasPromotedParam reports whether the parameter list has a promoted property -
// a parameter carrying a public/protected/private/readonly modifier.
func hasPromotedParam(s *tokens.Stream, open, closeIdx int) bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,18 @@ func TestStandaloneLinePromotedProperty(t *testing.T) {
t.Fatalf("promoted: changed=%v got=%q", changed, got)
}

// a plain constructor is left inline
// a plain constructor is left inline when the file has no promoted constructor
if _, changed := apply(t, f, "<?php\nclass A {\n public function __construct($x, $y)\n {\n }\n}"); changed {
t.Fatal("plain constructor must stay inline")
t.Fatal("plain constructor must stay inline without a promoted constructor")
}

// file-level trigger: once any constructor in the file is promoted, every
// constructor's params break onto standalone lines, plain ones included
mixed := "<?php\nclass A {\n public function __construct(private int $x)\n {\n }\n}\nclass B {\n public function __construct($y)\n {\n }\n}"
got, changed = apply(t, f, mixed)
want := "<?php\nclass A {\n public function __construct(\n private int $x\n )\n {\n }\n}\nclass B {\n public function __construct(\n $y\n )\n {\n }\n}"
if !changed || got != want {
t.Fatalf("file-level trigger: changed=%v got=%q", changed, got)
}

// a non-constructor method is untouched
Expand Down
Loading