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
22 changes: 22 additions & 0 deletions blink/internal/fixer/rules/method_argument_space.go
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,25 @@ func argListIsMultiline(s *tokens.Stream, open, closeIdx int) bool {

// isCallOrDeclParen reports whether the "(" at open opens a function/method call
// or a function/closure declaration (not a control structure, array or grouping).
// isLanguageConstructBareword reports whether the identifier at p is a language
// construct (eval, exit, die) used as a bareword rather than a member call. PHP
// tokenizes these as their own constructs, not T_STRING, so MethodArgumentSpace
// never reflows their parentheses as a function argument list.
func isLanguageConstructBareword(s *tokens.Stream, p int) bool {
switch strings.ToLower(s.At(p).Value) {
case "eval", "exit", "die":
default:
return false
}
if pp := sigPrev(s, p); pp >= 0 && s.At(pp).Kind == token.Punct {
switch s.At(pp).Value {
case "->", "?->", "::":
return false // "$obj->eval(...)" is a real method call
}
}
return true
}

func isCallOrDeclParen(s *tokens.Stream, open int) bool {
p := sigPrev(s, open)
if p < 0 {
Expand All @@ -718,6 +737,9 @@ func isCallOrDeclParen(s *tokens.Stream, open int) bool {
t := s.At(p)
switch t.Kind {
case token.Ident, token.Variable:
if t.Kind == token.Ident && isLanguageConstructBareword(s, p) {
return false // eval()/exit()/die() are language constructs, not function calls
}
return true
case token.Punct:
// ")" and "]" end a prior call/subscript; "}" ends a dynamic name like
Expand Down
16 changes: 16 additions & 0 deletions blink/internal/fixer/rules/method_argument_space_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,19 @@ func TestMethodArgumentSpaceSourceURL(t *testing.T) {
t.Fatalf("SourceURL mismatch: derived=%q declared=%q", got, r.SourceURL())
}
}

// A single-argument call to the eval/exit/die language constructs is not a
// function call, so a heredoc argument must not be reflowed onto its own line.
func TestMethodArgumentSpaceEvalHeredocNotReflowed(t *testing.T) {
t.Parallel()
src := "<?php\neval(<<<EOPHP\nnamespace X;\nEOPHP\n);\n"
if got, changed := apply(t, MethodArgumentSpace{}, src); changed || got != src {
t.Fatalf("eval heredoc must be left inline; changed=%v got=%q", changed, got)
}
// a real method named eval is still a call: its heredoc arg reflows onto its own line
call := "<?php\n$o->eval(<<<EOT\nbody\nEOT\n);\n"
want := "<?php\n$o->eval(\n <<<EOT\nbody\nEOT\n);\n"
if got, changed := apply(t, MethodArgumentSpace{}, call); !changed || got != want {
t.Fatalf("a real ->eval() method call must still be processed; changed=%v got=%q", changed, got)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace App;

class Loader
{
public function load(string $ns): void
{
eval(<<<EOPHP
namespace {$ns};

function loaded(): bool
{
return true;
}
EOPHP
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace App;

class Loader
{
public function load(string $ns): void
{
eval(<<<EOPHP
namespace {$ns};

function loaded(): bool
{
return true;
}
EOPHP
);
}
}
Loading