From ef09fcdcb7be6fc8e99d505a5bb174de9e2f4956 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 30 Aug 2025 15:09:59 +0200 Subject: [PATCH 1/4] Add BrowserKitAssertionTraitReturnTypeExtension --- extension.neon | 5 ++ ...erKitAssertionTraitReturnTypeExtension.php | 59 +++++++++++++++++++ tests/Type/Symfony/ExtensionTest.php | 1 + .../data/browserkit_assertion_trait.php | 19 ++++++ 4 files changed, 84 insertions(+) create mode 100644 src/Type/Symfony/BrowserKitAssertionTraitReturnTypeExtension.php create mode 100644 tests/Type/Symfony/data/browserkit_assertion_trait.php diff --git a/extension.neon b/extension.neon index ecc2ddf3..94b171bd 100644 --- a/extension.neon +++ b/extension.neon @@ -193,6 +193,11 @@ services: factory: PHPStan\Type\Symfony\EnvelopeReturnTypeExtension tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + # BrowserKitAssertionTrait::getClient() return type + - + class: PHPStan\Type\Symfony\BrowserKitAssertionTraitReturnTypeExtension + tags: [phpstan.broker.expressionTypeResolverExtension] + # Messenger HandleTrait::handle() return type - class: PHPStan\Type\Symfony\MessengerHandleTraitReturnTypeExtension diff --git a/src/Type/Symfony/BrowserKitAssertionTraitReturnTypeExtension.php b/src/Type/Symfony/BrowserKitAssertionTraitReturnTypeExtension.php new file mode 100644 index 00000000..b352ce09 --- /dev/null +++ b/src/Type/Symfony/BrowserKitAssertionTraitReturnTypeExtension.php @@ -0,0 +1,59 @@ +isSupported($expr, $scope)) { + $args = $expr->getArgs(); + if (count($args) > 0) { + return $scope->getType($args[0]->value); + } + + return new ObjectType('Symfony\Component\BrowserKit\AbstractBrowser'); + } + + return null; + } + + /** + * @phpstan-assert-if-true =MethodCall $expr + */ + private function isSupported(Expr $expr, Scope $scope): bool + { + if (!($expr instanceof MethodCall) || !($expr->name instanceof Identifier) || $expr->name->name !== self::TRAIT_METHOD_NAME) { + return false; + } + + if (!$scope->isInClass()) { + return false; + } + + $reflectionClass = $scope->getClassReflection()->getNativeReflection(); + + if (!$reflectionClass->hasMethod(self::TRAIT_METHOD_NAME)) { + return false; + } + + $methodReflection = $reflectionClass->getMethod(self::TRAIT_METHOD_NAME); + $declaringClassReflection = $methodReflection->getBetterReflection()->getDeclaringClass(); + + return $declaringClassReflection->getName() === self::TRAIT_NAME; + } + +} diff --git a/tests/Type/Symfony/ExtensionTest.php b/tests/Type/Symfony/ExtensionTest.php index 40420be0..7c294771 100644 --- a/tests/Type/Symfony/ExtensionTest.php +++ b/tests/Type/Symfony/ExtensionTest.php @@ -14,6 +14,7 @@ class ExtensionTest extends TypeInferenceTestCase /** @return mixed[] */ public function dataFileAsserts(): iterable { + yield from $this->gatherAssertTypes(__DIR__ . '/data/browserkit_assertion_trait.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/messenger_handle_trait.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/envelope_all.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/header_bag_get.php'); diff --git a/tests/Type/Symfony/data/browserkit_assertion_trait.php b/tests/Type/Symfony/data/browserkit_assertion_trait.php new file mode 100644 index 00000000..4889d954 --- /dev/null +++ b/tests/Type/Symfony/data/browserkit_assertion_trait.php @@ -0,0 +1,19 @@ +getClient()); + assertType('null', $this->getClient(null)); + assertType('Symfony\Component\BrowserKit\AbstractBrowser', $this->getClient($browser)); + assertType('Symfony\Component\BrowserKit\AbstractBrowser|null', $this->getClient($nullableBrowser)); + } +} From 204e0e3ac650eb5601a21750b556fd0eb7c97667 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 30 Aug 2025 15:12:58 +0200 Subject: [PATCH 2/4] Failing test --- tests/Type/Symfony/data/browserkit_assertion_trait.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Type/Symfony/data/browserkit_assertion_trait.php b/tests/Type/Symfony/data/browserkit_assertion_trait.php index 4889d954..7c3b2990 100644 --- a/tests/Type/Symfony/data/browserkit_assertion_trait.php +++ b/tests/Type/Symfony/data/browserkit_assertion_trait.php @@ -9,11 +9,12 @@ class Foo { use BrowserKitAssertionsTrait; - public function test(AbstractBrowser $browser, ?AbstractBrowser $nullableBrowser) + public function test(AbstractBrowser $browser, ?AbstractBrowser $nullableBrowser, mixed $mixed) { assertType('Symfony\Component\BrowserKit\AbstractBrowser', $this->getClient()); assertType('null', $this->getClient(null)); assertType('Symfony\Component\BrowserKit\AbstractBrowser', $this->getClient($browser)); assertType('Symfony\Component\BrowserKit\AbstractBrowser|null', $this->getClient($nullableBrowser)); + assertType('Symfony\Component\BrowserKit\AbstractBrowser|null', $this->getClient($mixed)); } } From c7e112555409e88355c8c1cb4df3398a4a4a21d6 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 30 Aug 2025 15:16:36 +0200 Subject: [PATCH 3/4] Fix --- .../BrowserKitAssertionTraitReturnTypeExtension.php | 11 ++++++++++- .../Type/Symfony/data/browserkit_assertion_trait.php | 5 ++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Type/Symfony/BrowserKitAssertionTraitReturnTypeExtension.php b/src/Type/Symfony/BrowserKitAssertionTraitReturnTypeExtension.php index b352ce09..864dc48d 100644 --- a/src/Type/Symfony/BrowserKitAssertionTraitReturnTypeExtension.php +++ b/src/Type/Symfony/BrowserKitAssertionTraitReturnTypeExtension.php @@ -7,8 +7,11 @@ use PhpParser\Node\Identifier; use PHPStan\Analyser\Scope; use PHPStan\Type\ExpressionTypeResolverExtension; +use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use PHPStan\Type\UnionType; use function count; final class BrowserKitAssertionTraitReturnTypeExtension implements ExpressionTypeResolverExtension @@ -22,7 +25,13 @@ public function getType(Expr $expr, Scope $scope): ?Type if ($this->isSupported($expr, $scope)) { $args = $expr->getArgs(); if (count($args) > 0) { - return $scope->getType($args[0]->value); + return TypeCombinator::intersect( + $scope->getType($args[0]->value), + new UnionType([ + new ObjectType('Symfony\Component\BrowserKit\AbstractBrowser'), + new NullType(), + ]), + ); } return new ObjectType('Symfony\Component\BrowserKit\AbstractBrowser'); diff --git a/tests/Type/Symfony/data/browserkit_assertion_trait.php b/tests/Type/Symfony/data/browserkit_assertion_trait.php index 7c3b2990..b91d75b5 100644 --- a/tests/Type/Symfony/data/browserkit_assertion_trait.php +++ b/tests/Type/Symfony/data/browserkit_assertion_trait.php @@ -9,7 +9,10 @@ class Foo { use BrowserKitAssertionsTrait; - public function test(AbstractBrowser $browser, ?AbstractBrowser $nullableBrowser, mixed $mixed) + /** + * @param mixed $mixed + */ + public function test(AbstractBrowser $browser, ?AbstractBrowser $nullableBrowser, $mixed) { assertType('Symfony\Component\BrowserKit\AbstractBrowser', $this->getClient()); assertType('null', $this->getClient(null)); From 2b57a140c85b0f3e7f26ae7036d1179f2f597874 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 10 Sep 2025 21:11:49 +0200 Subject: [PATCH 4/4] Rework --- ...BrowserKitAssertionTraitReturnTypeExtension.php | 14 +++++++++----- .../MessengerHandleTraitReturnTypeExtension.php | 12 ++++++++---- .../Symfony/data/browserkit_assertion_trait.php | 3 +++ 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/Type/Symfony/BrowserKitAssertionTraitReturnTypeExtension.php b/src/Type/Symfony/BrowserKitAssertionTraitReturnTypeExtension.php index 864dc48d..f1e6c75f 100644 --- a/src/Type/Symfony/BrowserKitAssertionTraitReturnTypeExtension.php +++ b/src/Type/Symfony/BrowserKitAssertionTraitReturnTypeExtension.php @@ -18,7 +18,7 @@ final class BrowserKitAssertionTraitReturnTypeExtension implements ExpressionTyp { private const TRAIT_NAME = 'Symfony\Bundle\FrameworkBundle\Test\BrowserKitAssertionsTrait'; - private const TRAIT_METHOD_NAME = 'getClient'; + private const TRAIT_METHOD_NAME = 'getclient'; public function getType(Expr $expr, Scope $scope): ?Type { @@ -45,7 +45,7 @@ public function getType(Expr $expr, Scope $scope): ?Type */ private function isSupported(Expr $expr, Scope $scope): bool { - if (!($expr instanceof MethodCall) || !($expr->name instanceof Identifier) || $expr->name->name !== self::TRAIT_METHOD_NAME) { + if (!($expr instanceof MethodCall) || !($expr->name instanceof Identifier) || $expr->name->toLowerString() !== self::TRAIT_METHOD_NAME) { return false; } @@ -53,14 +53,18 @@ private function isSupported(Expr $expr, Scope $scope): bool return false; } - $reflectionClass = $scope->getClassReflection()->getNativeReflection(); + $methodReflection = $scope->getMethodReflection($scope->getType($expr->var), $expr->name->toString()); + if ($methodReflection === null) { + return false; + } + $reflectionClass = $methodReflection->getDeclaringClass()->getNativeReflection(); if (!$reflectionClass->hasMethod(self::TRAIT_METHOD_NAME)) { return false; } - $methodReflection = $reflectionClass->getMethod(self::TRAIT_METHOD_NAME); - $declaringClassReflection = $methodReflection->getBetterReflection()->getDeclaringClass(); + $traitMethodReflection = $reflectionClass->getMethod(self::TRAIT_METHOD_NAME); + $declaringClassReflection = $traitMethodReflection->getBetterReflection()->getDeclaringClass(); return $declaringClassReflection->getName() === self::TRAIT_NAME; } diff --git a/src/Type/Symfony/MessengerHandleTraitReturnTypeExtension.php b/src/Type/Symfony/MessengerHandleTraitReturnTypeExtension.php index 2c7b1fbe..1d1b7dec 100644 --- a/src/Type/Symfony/MessengerHandleTraitReturnTypeExtension.php +++ b/src/Type/Symfony/MessengerHandleTraitReturnTypeExtension.php @@ -66,7 +66,7 @@ private function getMessageMap(): MessageMap */ private function isSupported(Expr $expr, Scope $scope): bool { - if (!($expr instanceof MethodCall) || !($expr->name instanceof Identifier) || $expr->name->name !== self::TRAIT_METHOD_NAME) { + if (!($expr instanceof MethodCall) || !($expr->name instanceof Identifier) || $expr->name->toLowerString() !== self::TRAIT_METHOD_NAME) { return false; } @@ -74,14 +74,18 @@ private function isSupported(Expr $expr, Scope $scope): bool return false; } - $reflectionClass = $scope->getClassReflection()->getNativeReflection(); + $methodReflection = $scope->getMethodReflection($scope->getType($expr->var), $expr->name->toString()); + if ($methodReflection === null) { + return false; + } + $reflectionClass = $methodReflection->getDeclaringClass()->getNativeReflection(); if (!$reflectionClass->hasMethod(self::TRAIT_METHOD_NAME)) { return false; } - $methodReflection = $reflectionClass->getMethod(self::TRAIT_METHOD_NAME); - $declaringClassReflection = $methodReflection->getBetterReflection()->getDeclaringClass(); + $traitMethodReflection = $reflectionClass->getMethod(self::TRAIT_METHOD_NAME); + $declaringClassReflection = $traitMethodReflection->getBetterReflection()->getDeclaringClass(); return $declaringClassReflection->getName() === self::TRAIT_NAME; } diff --git a/tests/Type/Symfony/data/browserkit_assertion_trait.php b/tests/Type/Symfony/data/browserkit_assertion_trait.php index b91d75b5..6ced3997 100644 --- a/tests/Type/Symfony/data/browserkit_assertion_trait.php +++ b/tests/Type/Symfony/data/browserkit_assertion_trait.php @@ -19,5 +19,8 @@ public function test(AbstractBrowser $browser, ?AbstractBrowser $nullableBrowser assertType('Symfony\Component\BrowserKit\AbstractBrowser', $this->getClient($browser)); assertType('Symfony\Component\BrowserKit\AbstractBrowser|null', $this->getClient($nullableBrowser)); assertType('Symfony\Component\BrowserKit\AbstractBrowser|null', $this->getClient($mixed)); + + assertType('Symfony\Component\BrowserKit\AbstractBrowser', $this->getclient()); + assertType('mixed', $mixed->getClient()); } }