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..f1e6c75f --- /dev/null +++ b/src/Type/Symfony/BrowserKitAssertionTraitReturnTypeExtension.php @@ -0,0 +1,72 @@ +isSupported($expr, $scope)) { + $args = $expr->getArgs(); + if (count($args) > 0) { + 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'); + } + + 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->toLowerString() !== self::TRAIT_METHOD_NAME) { + return false; + } + + if (!$scope->isInClass()) { + return false; + } + + $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; + } + + $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/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..6ced3997 --- /dev/null +++ b/tests/Type/Symfony/data/browserkit_assertion_trait.php @@ -0,0 +1,26 @@ +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)); + + assertType('Symfony\Component\BrowserKit\AbstractBrowser', $this->getclient()); + assertType('mixed', $mixed->getClient()); + } +}