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: 60 additions & 3 deletions blink/internal/fixer/rules/constructs.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,12 @@ var singleSpaceKnownKeywords = map[string]bool{
// single space, Preceded lists keywords ("as", "else", "elseif", "use_lambda")
// preceded by one, YieldFrom collapses whitespace inside "yield from".
type SingleSpaceAroundConstruct struct {
Followed map[string]bool
Preceded map[string]bool
YieldFrom bool
Followed map[string]bool
Preceded map[string]bool
// NamedArgument ensures a single space after a named-argument colon ("foo(a:1)"
// -> "foo(a: 1)"); it is the "named_argument" virtual construct of php-cs-fixer.
NamedArgument bool
YieldFrom bool
}

func (SingleSpaceAroundConstruct) Name() string {
Expand All @@ -59,7 +62,11 @@ func (SingleSpaceAroundConstruct) SourceURL() string {
func (f SingleSpaceAroundConstruct) WithConfig(config map[string]any) fixer.Fixer {
if list, ok := singleSpaceStringList(config["constructs_followed_by_a_single_space"]); ok {
f.Followed = map[string]bool{}
f.NamedArgument = false
for _, name := range list {
if name == "named_argument" {
f.NamedArgument = true
}
if singleSpaceKnownKeywords[name] {
f.Followed[name] = true
}
Expand Down Expand Up @@ -154,9 +161,59 @@ func (f SingleSpaceAroundConstruct) fixPreceded(s *tokens.Stream) bool {
return changed
}

// fixNamedArgument ensures a single space after a named-argument colon. A colon is
// a named-argument colon when its previous meaningful token is a name and the one
// before that is "(" or "," - php-cs-fixer's NamedArgumentTransformer rule, which
// excludes "::", ternary "?:", return-type ")" and goto labels. The space before
// the colon is left as-is, matching php-cs-fixer.
func (f SingleSpaceAroundConstruct) fixNamedArgument(s *tokens.Stream) bool {
changed := false
for i := 0; i < s.Len(); i++ {
if t := s.At(i); t.Kind != token.Punct || t.Value != ":" {
continue
}
name := prevSignificantIndex(s, i)
if name < 0 || s.At(name).Kind != token.Ident {
continue
}
pre := prevSignificantIndex(s, name)
if pre < 0 || s.At(pre).Kind != token.Punct || (s.At(pre).Value != "(" && s.At(pre).Value != ",") {
continue
}
if i+1 >= s.Len() {
continue
}
next := s.At(i + 1)
if next.Kind == token.Whitespace {
// a newline into an attribute or doc comment is kept, as php-cs-fixer does
if hasNewline(next.Value) {
if nn := i + 2; nn < s.Len() && (isAttributeComment(s.At(nn)) || s.At(nn).Kind == token.DocComment) {
continue
}
}
if next.Value != " " {
s.SetValue(i+1, " ")
changed = true
}
continue
}
// no space yet: insert one, unless a separator/closer directly follows
switch next.Value {
case ",", ":", ";", ")", "]", "}":
default:
s.InsertAt(i+1, token.Token{Kind: token.Whitespace, Value: " "})
changed = true
}
}
return changed
}

func (f SingleSpaceAroundConstruct) Fix(s *tokens.Stream) bool {
changed := f.YieldFrom && f.fixYieldFrom(s)

if f.NamedArgument && f.fixNamedArgument(s) {
changed = true
}
if len(f.Preceded) > 0 && f.fixPreceded(s) {
changed = true
}
Expand Down
9 changes: 9 additions & 0 deletions blink/internal/fixer/rules/constructs_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ func TestConstructsConfigCases(t *testing.T) {
{"preceded joins block comment before elseif", SingleSpaceAroundConstruct{}.WithConfig(map[string]any{"constructs_preceded_by_a_single_space": []any{"elseif"}}).(fixerRule), "<?php if ($a) {\n}\n/* note */\nelseif ($b) {\n}", "<?php if ($a) {\n}\n/* note */ elseif ($b) {\n}"},
{"preceded keeps line comment before else", SingleSpaceAroundConstruct{}.WithConfig(map[string]any{"constructs_preceded_by_a_single_space": []any{"else"}}).(fixerRule), "<?php if ($a) {\n}\n// note\nelse {\n}", "<?php if ($a) {\n}\n// note\nelse {\n}"},
{"yield from", SingleSpaceAroundConstruct{}.WithConfig(map[string]any{"constructs_contain_a_single_space": []any{"yield_from"}}).(fixerRule), "<?php function f() { yield from $a; }", "<?php function f() { yield from $a; }"},
{"named arg space after", SingleSpaceAroundConstruct{}.WithConfig(map[string]any{"constructs_followed_by_a_single_space": []any{"named_argument"}}).(fixerRule), "<?php foo(a:1);", "<?php foo(a: 1);"},
{"named arg second arg", SingleSpaceAroundConstruct{}.WithConfig(map[string]any{"constructs_followed_by_a_single_space": []any{"named_argument"}}).(fixerRule), "<?php foo($x, a:1);", "<?php foo($x, a: 1);"},
{"named arg collapses multiple spaces", SingleSpaceAroundConstruct{}.WithConfig(map[string]any{"constructs_followed_by_a_single_space": []any{"named_argument"}}).(fixerRule), "<?php foo(a: 1);", "<?php foo(a: 1);"},
{"named arg keeps space before", SingleSpaceAroundConstruct{}.WithConfig(map[string]any{"constructs_followed_by_a_single_space": []any{"named_argument"}}).(fixerRule), "<?php foo(a :1);", "<?php foo(a : 1);"},
{"named arg leaves scope resolution", SingleSpaceAroundConstruct{}.WithConfig(map[string]any{"constructs_followed_by_a_single_space": []any{"named_argument"}}).(fixerRule), "<?php foo(A::B);", "<?php foo(A::B);"},
{"named arg leaves ternary", SingleSpaceAroundConstruct{}.WithConfig(map[string]any{"constructs_followed_by_a_single_space": []any{"named_argument"}}).(fixerRule), "<?php $x = $c ? A:$b;", "<?php $x = $c ? A:$b;"},
{"named arg leaves return type colon", SingleSpaceAroundConstruct{}.WithConfig(map[string]any{"constructs_followed_by_a_single_space": []any{"named_argument"}}).(fixerRule), "<?php function f():int {}", "<?php function f():int {}"},
{"named arg leaves colon in string", SingleSpaceAroundConstruct{}.WithConfig(map[string]any{"constructs_followed_by_a_single_space": []any{"named_argument"}}).(fixerRule), "<?php foo('a:1');", "<?php foo('a:1');"},
{"named arg off by default", SingleSpaceAroundConstruct{}, "<?php foo(a:1);", "<?php foo(a:1);"},
{"parens default none", NoSpacesInsideParenthesis{}, "<?php foo( $a );", "<?php foo($a);"},
{"parens single", NoSpacesInsideParenthesis{}.WithConfig(map[string]any{"space": "single"}).(fixerRule), "<?php foo($a, bar());", "<?php foo( $a, bar() );"},
{"parens single skips cast and trims empty", NoSpacesInsideParenthesis{}.WithConfig(map[string]any{"space": "single"}).(fixerRule), "<?php if ($a) { $b = (int) $c; foo( ); }", "<?php if ( $a ) { $b = (int) $c; foo(); }"},
Expand Down
Loading