diff --git a/src/Support/ReflectionClosure.php b/src/Support/ReflectionClosure.php index dcdce6ae..de0175f1 100644 --- a/src/Support/ReflectionClosure.php +++ b/src/Support/ReflectionClosure.php @@ -112,7 +112,9 @@ public function getCode() $tokens = $this->getTokens(); $state = $lastState = 'start'; $inside_structure = false; + $isFirstClassCallable = false; $isShortClosure = false; + $inside_structure_mark = 0; $open = 0; $code = ''; @@ -130,11 +132,15 @@ public function getCode() case 'start': if ($token[0] === T_FUNCTION || $token[0] === T_STATIC) { $code .= $token[1]; + $state = $token[0] === T_FUNCTION ? 'function' : 'static'; } elseif ($token[0] === T_FN) { $isShortClosure = true; $code .= $token[1]; $state = 'closure_args'; + } elseif ($token[0] === T_PUBLIC || $token[0] === T_PROTECTED || $token[0] === T_PRIVATE) { + $code = ''; + $isFirstClassCallable = true; } break; case 'static': @@ -155,6 +161,11 @@ public function getCode() case 'function': switch ($token[0]) { case T_STRING: + if ($isFirstClassCallable) { + $state = 'closure_args'; + break; + } + $code = ''; $state = 'named_function'; break; diff --git a/tests/ReflectionClosurePhp81Test.php b/tests/ReflectionClosurePhp81Test.php index 8ed3dd07..c0d6bfb3 100644 --- a/tests/ReflectionClosurePhp81Test.php +++ b/tests/ReflectionClosurePhp81Test.php @@ -107,13 +107,13 @@ enum ScopedBackedEnum: string { test('readonly properties', function () { $f = function () { - $controller = new SerializerPhp81Controller(); + $controller = new ReflectionClosurePhp81Controller(); $controller->service = 'foo'; }; $e = 'function () { - $controller = new \SerializerPhp81Controller(); + $controller = new \ReflectionClosurePhp81Controller(); $controller->service = \'foo\'; }'; @@ -121,7 +121,7 @@ enum ScopedBackedEnum: string { expect($f)->toBeCode($e); }); -test('first-class callable', function () { +test('first-class callable with closures', function () { $f = function ($a) { $f = fn ($b) => $a + $b + 1; @@ -137,6 +137,77 @@ enum ScopedBackedEnum: string { expect($f)->toBeCode($e); }); +test('first-class callable with methods', function () { + $f = (new ReflectionClosurePhp81Controller())->publicGetter(...); + + $e = 'function () + { + return $this->privateGetter(); + }'; + + expect($f)->toBeCode($e); + + $f = (new ReflectionClosurePhp81Controller())->publicGetterResolver(...); + + $e = 'function () + { + return $this->privateGetterResolver(...); + }'; + + expect($f)->toBeCode($e); +}); + +test('first-class callable with static methods', function () { + $f = ReflectionClosurePhp81Controller::publicStaticGetter(...); + + $e = 'static function () + { + return static::privateStaticGetter(); + }'; + + expect($f)->toBeCode($e); + + $f = ReflectionClosurePhp81Controller::publicStaticGetterResolver(...); + + $e = 'static function () + { + return static::privateStaticGetterResolver(...); + }'; + + expect($f)->toBeCode($e); +}); + +test('first-class callable final method', function () { + $f = (new ReflectionClosurePhp81Controller())->finalPublicGetterResolver(...); + + $e = 'function () + { + return $this->privateGetterResolver(...); + }'; + + expect($f)->toBeCode($e); + + $f = ReflectionClosurePhp81Controller::finalPublicStaticGetterResolver(...); + + $e = 'static function () + { + return static::privateStaticGetterResolver(...); + }'; + + expect($f)->toBeCode($e); +}); + +test('first-class callable self return type', function () { + $f = (new ReflectionClosurePhp81Controller())->getSelf(...); + + $e = 'function (self $instance): self + { + return $instance; + }'; + + expect($f)->toBeCode($e); +}); + test('intersection types', function () { $f = function (ClassAlias&Forest $service): ClassAlias&Forest { return $service; @@ -196,5 +267,59 @@ public function __construct( ) { // .. } -} + public function publicGetter() + { + return $this->privateGetter(); + } + + private function privateGetter() + { + return $this->service; + } + + public static function publicStaticGetter() + { + return static::privateStaticGetter(); + } + + public static function privateStaticGetter() + { + return (new ReflectionClosurePhp81Controller())->service; + } + + public function publicGetterResolver() + { + return $this->privateGetterResolver(...); + } + + private function privateGetterResolver() + { + return fn () => $this->service; + } + + public static function publicStaticGetterResolver() + { + return static::privateStaticGetterResolver(...); + } + + public static function privateStaticGetterResolver() + { + return fn () => (new ReflectionClosurePhp81Controller())->service; + } + + final public function finalPublicGetterResolver() + { + return $this->privateGetterResolver(...); + } + + final public static function finalPublicStaticGetterResolver() + { + return static::privateStaticGetterResolver(...); + } + + public function getSelf(self $instance): self + { + return $instance; + } +} diff --git a/tests/SerializerPhp81Test.php b/tests/SerializerPhp81Test.php index 6ab92f00..c5105146 100644 --- a/tests/SerializerPhp81Test.php +++ b/tests/SerializerPhp81Test.php @@ -154,7 +154,7 @@ enum SerializerScopedBackedEnum: string { }); })->with('serializers'); -test('first-class callable', function () { +test('first-class callable with closures', function () { $f = function ($value) { return $value; }; @@ -174,6 +174,58 @@ enum SerializerScopedBackedEnum: string { expect($f(1)(2))->toBe(4); })->with('serializers'); +test('first-class callable with methods', function () { + $f = (new SerializerPhp81Controller())->publicGetter(...); + + $f = s($f); + + expect($f())->toBeInstanceOf(SerializerPhp81Service::class); + + $f = (new SerializerPhp81Controller())->publicGetterResolver(...); + + $f = s($f); + + expect($f()()())->toBeInstanceOf(SerializerPhp81Service::class); +})->with('serializers'); + +test('first-class callable with static methods', function () { + $f = SerializerPhp81Controller::publicStaticGetter(...); + + $f = s($f); + + expect($f())->toBeInstanceOf(SerializerPhp81Service::class); + + $f = SerializerPhp81Controller::publicStaticGetterResolver(...); + + $f = s($f); + + expect($f()()())->toBeInstanceOf(SerializerPhp81Service::class); +})->with('serializers'); + +test('first-class callable final method', function () { + $f = (new SerializerPhp81Controller())->finalPublicGetterResolver(...); + + $f = s($f); + + expect($f()()())->toBeInstanceOf(SerializerPhp81Service::class); + + $f = SerializerPhp81Controller::finalPublicStaticGetterResolver(...); + + $f = s($f); + + expect($f()()())->toBeInstanceOf(SerializerPhp81Service::class); +})->with('serializers'); + +test('first-class callable self return type', function () { + $f = (new SerializerPhp81Controller())->getSelf(...); + + $f = s($f); + + $controller = new SerializerPhp81Controller(); + + expect($f($controller))->toBeInstanceOf(SerializerPhp81Controller::class); +})->with('serializers'); + test('intersection types', function () { $f = function (SerializerPhp81HasName&SerializerPhp81HasId $service): SerializerPhp81HasName&SerializerPhp81HasId { return $service; @@ -243,5 +295,60 @@ public function __construct( ) { // .. } + + public function publicGetter() + { + return $this->privateGetter(); + } + + private function privateGetter() + { + return $this->service; + } + + public static function publicStaticGetter() + { + return static::privateStaticGetter(); + } + + public static function privateStaticGetter() + { + return (new SerializerPhp81Controller())->service; + } + + public function publicGetterResolver() + { + return $this->privateGetterResolver(...); + } + + private function privateGetterResolver() + { + return fn () => $this->service; + } + + public static function publicStaticGetterResolver() + { + return static::privateStaticGetterResolver(...); + } + + public static function privateStaticGetterResolver() + { + return fn () => (new SerializerPhp81Controller())->service; + } + + final public function finalPublicGetterResolver() + { + return $this->privateGetterResolver(...); + } + + final public static function finalPublicStaticGetterResolver() + { + return static::privateStaticGetterResolver(...); + } + + public function getSelf(self $instance): self + { + return $instance; + } }