From 807bca97c460f771597b7ea7f040b08ff58a48a5 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 28 Apr 2025 14:59:52 +0200 Subject: [PATCH 1/2] Resolve `ExtendedFunctionVariant::getReturnType()` more lazily This will allow the `$this` or `static` in PHPDoc return type to be first replaced with final-overriden ObjectType before being thrown into `TypehintHelper::decideType()`. --- src/Reflection/ExtendedFunctionVariant.php | 20 ++++++- .../Php/PhpClassReflectionExtension.php | 10 ++-- .../PhpFunctionFromParserNodeReflection.php | 2 +- src/Reflection/Php/PhpFunctionReflection.php | 2 +- src/Reflection/Php/PhpMethodReflection.php | 57 +++++++++---------- .../NativeFunctionReflectionProvider.php | 2 +- ...ackUnresolvedMethodPrototypeReflection.php | 19 +++---- ...ypeUnresolvedMethodPrototypeReflection.php | 12 ++-- .../Type/IntersectionTypeMethodReflection.php | 8 +-- .../nsrt/conditional-return-static-union.php | 45 +++++++++++++++ turbo-ext/src/AcceptorValues.h | 4 +- turbo-ext/src/ExtendedFunctionVariant.cpp | 48 +++++++++++++--- turbo-ext/src/FunctionVariant.cpp | 4 +- turbo-ext/src/PhpClassReflectionExtension.cpp | 19 +++---- turbo-ext/src/TypeTraits.cpp | 50 ++++++++-------- .../ExtendedCallableFunctionVariant.h | 18 +++--- .../src/generated/ExtendedFunctionVariant.h | 19 ++++--- turbo-ext/src/support.h | 3 + 18 files changed, 216 insertions(+), 126 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/conditional-return-static-union.php diff --git a/src/Reflection/ExtendedFunctionVariant.php b/src/Reflection/ExtendedFunctionVariant.php index d945f5f3320..00fd8b38b5c 100644 --- a/src/Reflection/ExtendedFunctionVariant.php +++ b/src/Reflection/ExtendedFunctionVariant.php @@ -6,6 +6,7 @@ use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\Type; +use PHPStan\Type\TypehintHelper; /** * @api @@ -23,7 +24,7 @@ public function __construct( ?TemplateTypeMap $resolvedTemplateTypeMap, array $parameters, bool $isVariadic, - Type $returnType, + private ?Type $returnType, private Type $phpDocReturnType, private Type $nativeReturnType, ?TemplateTypeVarianceMap $callSiteVarianceMap = null, @@ -34,7 +35,10 @@ public function __construct( $resolvedTemplateTypeMap, $parameters, $isVariadic, - $returnType, + $returnType ?? TypehintHelper::decideType( + $nativeReturnType, + $phpDocReturnType, + ), $callSiteVarianceMap, ); } @@ -50,6 +54,18 @@ public function getParameters(): array return $parameters; } + public function getReturnType(): Type + { + if ($this->returnType === null) { + return $this->returnType = TypehintHelper::decideType( + $this->nativeReturnType, + $this->phpDocReturnType, + ); + } + + return $this->returnType; + } + public function getPhpDocReturnType(): Type { return $this->phpDocReturnType; diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index 55dbd02acae..add07883f22 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -1109,10 +1109,8 @@ private function createNativeMethodVariant( ); } - if ($phpDocFromStubs && $phpDocReturnType !== null) { - $returnType = $phpDocReturnType; - } else { - $returnType = TypehintHelper::decideType($methodSignature->getReturnType(), $phpDocReturnType); + if (!$phpDocFromStubs || $phpDocReturnType === null) { + $phpDocReturnType = TypehintHelper::decideType($methodSignature->getReturnType(), $phpDocReturnType); } return new ExtendedFunctionVariant( @@ -1120,8 +1118,8 @@ private function createNativeMethodVariant( null, $parameters, $methodSignature->isVariadic(), - $returnType, - $phpDocReturnType ?? new MixedType(), + null, + $phpDocReturnType, $methodSignature->getNativeReturnType(), ); } diff --git a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php index 12ec0f36278..41c1fa6e191 100644 --- a/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpFunctionFromParserNodeReflection.php @@ -127,7 +127,7 @@ public function getVariants(): array $this->getResolvedTemplateTypeMap(), $this->getParameters(), $this->isVariadic(), - $this->getReturnType(), + null, $this->getPhpDocReturnType(), $this->getNativeReturnType(), ), diff --git a/src/Reflection/Php/PhpFunctionReflection.php b/src/Reflection/Php/PhpFunctionReflection.php index 1dbee429546..7f7cb9f0759 100644 --- a/src/Reflection/Php/PhpFunctionReflection.php +++ b/src/Reflection/Php/PhpFunctionReflection.php @@ -96,7 +96,7 @@ public function getVariants(): array null, $this->getParameters(), $this->isVariadic(), - $this->getReturnType(), + null, $this->getPhpDocReturnType(), $this->getNativeReturnType(), ), diff --git a/src/Reflection/Php/PhpMethodReflection.php b/src/Reflection/Php/PhpMethodReflection.php index db2f8a8d69c..eb297f0aae2 100644 --- a/src/Reflection/Php/PhpMethodReflection.php +++ b/src/Reflection/Php/PhpMethodReflection.php @@ -209,7 +209,7 @@ public function getVariants(): array null, $this->getParameters(), $this->isVariadic(), - $this->getReturnType(), + null, $this->getPhpDocReturnType(), $this->getNativeReturnType(), ), @@ -263,52 +263,51 @@ public function isPublic(): bool private function getReturnType(): Type { - if ($this->returnType === null) { - $name = strtolower($this->getName()); + return $this->returnType ??= TypehintHelper::decideType( + $this->getNativeReturnType(), + $this->phpDocReturnType, + ); + } + + private function getPhpDocReturnType(): Type + { + if ($this->phpDocReturnType !== null) { + return $this->phpDocReturnType; + } + + return new MixedType(); + } + + private function getNativeReturnType(): Type + { + if ($this->nativeReturnType === null) { $returnType = $this->reflection->getReturnType(); if ($returnType === null) { + $name = strtolower($this->getName()); if (in_array($name, ['__construct', '__destruct', '__unset', '__wakeup', '__clone'], true)) { - return $this->returnType = TypehintHelper::decideType(new VoidType(), $this->phpDocReturnType); + return $this->nativeReturnType = new VoidType(); } if ($name === '__tostring') { - return $this->returnType = TypehintHelper::decideType(new StringType(), $this->phpDocReturnType); + return $this->nativeReturnType = new StringType(); } if ($name === '__isset') { - return $this->returnType = TypehintHelper::decideType(new BooleanType(), $this->phpDocReturnType); + return $this->nativeReturnType = new BooleanType(); } if ($name === '__sleep') { - return $this->returnType = TypehintHelper::decideType(new ArrayType(new IntegerType(), new StringType()), $this->phpDocReturnType); + return $this->nativeReturnType = new ArrayType(new IntegerType(), new StringType()); } if ($name === '__set_state') { - return $this->returnType = TypehintHelper::decideType(new ObjectWithoutClassType(), $this->phpDocReturnType); + return $this->nativeReturnType = new ObjectWithoutClassType(); } } - $this->returnType = TypehintHelper::decideTypeFromReflection( + $this->nativeReturnType = TypehintHelper::decideTypeFromReflection( $returnType, - $this->phpDocReturnType, - $this->declaringClass, + selfClass: $this->declaringClass, ); } - return $this->returnType; - } - - private function getPhpDocReturnType(): Type - { - if ($this->phpDocReturnType !== null) { - return $this->phpDocReturnType; - } - - return new MixedType(); - } - - private function getNativeReturnType(): Type - { - return $this->nativeReturnType ??= TypehintHelper::decideTypeFromReflection( - $this->reflection->getReturnType(), - selfClass: $this->declaringClass, - ); + return $this->nativeReturnType; } public function getDeprecatedDescription(): ?string diff --git a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php index 6a868d39971..d96fe21cf3e 100644 --- a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php +++ b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php @@ -176,7 +176,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef ); }, $functionSignature->getParameters()), $functionSignature->isVariadic(), - TypehintHelper::decideType($functionSignature->getReturnType(), $phpDocReturnType), + null, $phpDocReturnType ?? new MixedType(), $functionSignature->getReturnType(), ); diff --git a/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php index f0e0254fd40..abe6f9ed3a6 100644 --- a/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php @@ -88,16 +88,15 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass, { $selfOutType = $method->getSelfOutType() !== null ? $this->transformStaticType($method->getSelfOutType()) : null; $variantFn = function (ExtendedParametersAcceptor $acceptor) use (&$selfOutType): ExtendedParametersAcceptor { - $originalReturnType = $acceptor->getReturnType(); - $transformedReturnType = $this->transformStaticType($originalReturnType); - if ($originalReturnType instanceof ThisType && $selfOutType !== null) { - $returnType = TypeCombinator::intersect($selfOutType, $transformedReturnType); - $selfOutType = $returnType; + $originalPhpDocReturnType = $acceptor->getPhpDocReturnType(); + $transformedPhpDocReturnType = $this->transformStaticType($originalPhpDocReturnType); + if ($originalPhpDocReturnType instanceof ThisType && $selfOutType !== null) { + $phpDocReturnType = TypeCombinator::intersect($selfOutType, $transformedPhpDocReturnType); + $selfOutType = $phpDocReturnType; } else { - $returnType = $transformedReturnType; + $phpDocReturnType = $transformedPhpDocReturnType; } - $phpDocReturnType = $acceptor->getPhpDocReturnType(); $nativeReturnType = $acceptor->getNativeReturnType(); return new ExtendedFunctionVariant( @@ -128,9 +127,9 @@ function (ExtendedParameterReflection $parameter): ExtendedParameterReflection { $acceptor->getParameters(), ), $acceptor->isVariadic(), - $returnType, - $originalReturnType->equals($phpDocReturnType) ? $transformedReturnType : $this->transformStaticType($phpDocReturnType), - $originalReturnType->equals($nativeReturnType) ? $transformedReturnType : $this->transformStaticType($nativeReturnType), + null, + $phpDocReturnType, + $originalPhpDocReturnType->equals($nativeReturnType) ? $transformedPhpDocReturnType : $this->transformStaticType($nativeReturnType), $acceptor->getCallSiteVarianceMap(), ); }; diff --git a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php index ba61cd9c1fc..6bfce576569 100644 --- a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php @@ -84,11 +84,11 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass, { $selfOutType = $method->getSelfOutType() !== null ? $this->transformStaticType($method->getSelfOutType()) : null; $variantFn = function (ExtendedParametersAcceptor $acceptor) use ($selfOutType): ExtendedParametersAcceptor { - $originalReturnType = $acceptor->getReturnType(); - if ($originalReturnType instanceof ThisType && $selfOutType !== null) { - $returnType = $selfOutType; + $originalPhpDocReturnType = $acceptor->getPhpDocReturnType(); + if ($originalPhpDocReturnType instanceof ThisType && $selfOutType !== null) { + $phpDocReturnType = $selfOutType; } else { - $returnType = $this->transformStaticType($originalReturnType); + $phpDocReturnType = $this->transformStaticType($originalPhpDocReturnType); } return new ExtendedFunctionVariant( $acceptor->getTemplateTypeMap(), @@ -114,8 +114,8 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass, $acceptor->getParameters(), ), $acceptor->isVariadic(), - $returnType, - $this->transformStaticType($acceptor->getPhpDocReturnType()), + null, + $phpDocReturnType, $this->transformStaticType($acceptor->getNativeReturnType()), $acceptor->getCallSiteVarianceMap(), ); diff --git a/src/Reflection/Type/IntersectionTypeMethodReflection.php b/src/Reflection/Type/IntersectionTypeMethodReflection.php index 108047fb638..0df6981ee1e 100644 --- a/src/Reflection/Type/IntersectionTypeMethodReflection.php +++ b/src/Reflection/Type/IntersectionTypeMethodReflection.php @@ -81,23 +81,17 @@ public function getPrototype(): ClassMemberReflection public function getVariants(): array { - $returnTypes = []; $phpDocReturnTypes = []; $nativeReturnTypes = []; foreach ($this->methods as $method) { $variants = $method->getVariants(); foreach ($variants as $acceptor) { - $returnTypes[] = $acceptor->getReturnType(); $phpDocReturnTypes[] = $acceptor->getPhpDocReturnType(); $nativeReturnTypes[] = $acceptor->getNativeReturnType(); } } - $returnType = $returnTypes[0]; - for ($i = 1, $count = count($returnTypes); $i < $count; $i++) { - $returnType = TypeCombinator::intersect($returnType, $returnTypes[$i]); - } $phpDocReturnType = $phpDocReturnTypes[0]; for ($i = 1, $count = count($phpDocReturnTypes); $i < $count; $i++) { $phpDocReturnType = TypeCombinator::intersect($phpDocReturnType, $phpDocReturnTypes[$i]); @@ -111,7 +105,7 @@ public function getVariants(): array $acceptor->getResolvedTemplateTypeMap(), $acceptor->getParameters(), $acceptor->isVariadic(), - $returnType, + null, $phpDocReturnType, $nativeReturnType, $acceptor->getCallSiteVarianceMap(), diff --git a/tests/PHPStan/Analyser/nsrt/conditional-return-static-union.php b/tests/PHPStan/Analyser/nsrt/conditional-return-static-union.php new file mode 100644 index 00000000000..d27eebf07e8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/conditional-return-static-union.php @@ -0,0 +1,45 @@ += 8.0 + +namespace ConditionalReturnStaticUnion; + +use function PHPStan\Testing\assertType; + +class Config {} +class MainConfig +{ + /** + * @param array|Config $value + * @return ($value is array ? Config : $this) + */ + public function invalidReturn(array|Config $value = []): Config|static + { + if (is_array($value)) { + return new Config(); + } + return $this; + } + + /** + * @param array|Config $value + * @return ($value is array ? Config : $this) + */ + public function validReturn(array|Config $value = []): Config|self + { + if (is_array($value)) { + return new Config(); + } + return $this; + } +} + +function (MainConfig $c): void { + assertType(Config::class, (new MainConfig())->invalidReturn()); + assertType(Config::class, (new MainConfig())->validReturn()); + assertType(MainConfig::class, (new MainConfig())->invalidReturn(new Config())); + assertType(MainConfig::class, (new MainConfig())->validReturn(new Config())); + + assertType(Config::class, $c->invalidReturn()); + assertType(Config::class, $c->validReturn()); + assertType(MainConfig::class, $c->invalidReturn(new Config())); + assertType(MainConfig::class, $c->validReturn(new Config())); +}; diff --git a/turbo-ext/src/AcceptorValues.h b/turbo-ext/src/AcceptorValues.h index a9c19382386..a0b519d08a0 100644 --- a/turbo-ext/src/AcceptorValues.h +++ b/turbo-ext/src/AcceptorValues.h @@ -46,13 +46,13 @@ inline constexpr int8_t functionVariantSlots[PT_PA_MEMBER_COUNT] = { }; inline constexpr int8_t extendedFunctionVariantSlots[PT_PA_MEMBER_COUNT] = { - PT_ACC_FV(templateTypeMap), PT_ACC_NONE, PT_ACC_FV(parameters), PT_ACC_FV(isVariadic), PT_ACC_FV(returnType), + PT_ACC_FV(templateTypeMap), PT_ACC_NONE, PT_ACC_FV(parameters), PT_ACC_FV(isVariadic), PT_ACC_EFV(returnType), PT_ACC_EFV(phpDocReturnType), PT_ACC_EFV(nativeReturnType), PT_ACC_FV(callSiteVarianceMap), PT_ACC_NONE, PT_ACC_NONE, PT_ACC_NONE, PT_ACC_NONE, PT_ACC_NONE, PT_ACC_NONE, PT_ACC_NONE, PT_ACC_NONE, PT_ACC_NONE, PT_ACC_NONE, PT_ACC_NONE, }; inline constexpr int8_t extendedCallableFunctionVariantSlots[PT_PA_MEMBER_COUNT] = { - PT_ACC_FV(templateTypeMap), PT_ACC_NONE, PT_ACC_FV(parameters), PT_ACC_FV(isVariadic), PT_ACC_FV(returnType), + PT_ACC_FV(templateTypeMap), PT_ACC_NONE, PT_ACC_FV(parameters), PT_ACC_FV(isVariadic), PT_ACC_EFV(returnType), PT_ACC_EFV(phpDocReturnType), PT_ACC_EFV(nativeReturnType), PT_ACC_FV(callSiteVarianceMap), PT_ACC_NONE, PT_ACC_NONE, PT_ACC_ECFV(throwPoints), PT_ACC_ECFV(isPure), PT_ACC_ECFV(impurePoints), PT_ACC_ECFV(invalidateExpressions), PT_ACC_ECFV(usedVariables), PT_ACC_ECFV(acceptsNamedArguments), PT_ACC_ECFV(mustUseReturnValue), PT_ACC_NONE, PT_ACC_NONE, diff --git a/turbo-ext/src/ExtendedFunctionVariant.cpp b/turbo-ext/src/ExtendedFunctionVariant.cpp index 0c1172a21d7..6d27d02eb64 100644 --- a/turbo-ext/src/ExtendedFunctionVariant.cpp +++ b/turbo-ext/src/ExtendedFunctionVariant.cpp @@ -6,7 +6,9 @@ * types: its two promoted slots follow the parent's six, the constructor * writes them and runs the parent's constructor body directly * (pt_function_variant_construct() — the parent method is native, nothing - * can sit in between). getParameters() is parent::getParameters(). Native + * can sit in between). A null $returnType is decided from the native and + * PHPDoc return types, for the parent eagerly and for getReturnType() into + * its own slot. getParameters() is parent::getParameters(). Native * creators use pt_extended_function_variant_new(); native callers read the * getters through pt_parameters_acceptor_call() (AcceptorValues.h). */ @@ -32,13 +34,39 @@ class ExtendedFunctionVariant public: explicit ExtendedFunctionVariant(zend_object *self) : self(self) {} - /* the promoted slots, then parent::__construct(...) (NULL for the - * nullable nulls); false = pending exception */ + /* the promoted slots, then parent::__construct(...) with $returnType ?? + * TypehintHelper::decideType($nativeReturnType, $phpDocReturnType) (NULL + * for the nullable nulls); false = pending exception */ [[nodiscard]] bool construct(zval *templateTypeMap, zval *resolvedTemplateTypeMap, zval *parameters, bool isVariadic, zval *returnType, zval *phpDocReturnType, zval *nativeReturnType, zval *callSiteVarianceMap) const { + zval nullValue; + ZVAL_NULL(&nullValue); + pt_write_slot(self, slots::returnType, returnType != NULL ? returnType : &nullValue); pt_write_slot(self, slots::phpDocReturnType, phpDocReturnType); pt_write_slot(self, slots::nativeReturnType, nativeReturnType); - return pt_function_variant_construct(self, templateTypeMap, resolvedTemplateTypeMap, parameters, isVariadic, returnType, callSiteVarianceMap); + if (returnType != NULL) { + return pt_function_variant_construct(self, templateTypeMap, resolvedTemplateTypeMap, parameters, isVariadic, returnType, callSiteVarianceMap); + } + zv::Val decided = pt_typehint_helper_decide_type(nativeReturnType, phpDocReturnType); + if (UNEXPECTED(decided.isUndef())) return false; + return pt_function_variant_construct(self, templateTypeMap, resolvedTemplateTypeMap, parameters, isVariadic, decided.raw(), callSiteVarianceMap); + } + + /* $this->returnType, filled with TypehintHelper::decideType( + * $this->nativeReturnType, $this->phpDocReturnType) when null */ + zv::Val getReturnType() const + { + zval *returnType = pt_typed_slot(self, slots::returnType, pt_ce_extended_function_variant, "returnType"); + if (UNEXPECTED(returnType == NULL)) return zv::Val(); + if (Z_TYPE_P(returnType) == IS_OBJECT) return zv::Val::copyOf(zv::Ref(returnType)); + zval *nativeReturnType = pt_typed_slot(self, slots::nativeReturnType, pt_ce_extended_function_variant, "nativeReturnType"); + if (UNEXPECTED(nativeReturnType == NULL)) return zv::Val(); + zval *phpDocReturnType = pt_typed_slot(self, slots::phpDocReturnType, pt_ce_extended_function_variant, "phpDocReturnType"); + if (UNEXPECTED(phpDocReturnType == NULL)) return zv::Val(); + zv::Val decided = pt_typehint_helper_decide_type(nativeReturnType, phpDocReturnType); + if (UNEXPECTED(decided.isUndef())) return zv::Val(); + pt_write_slot(self, slots::returnType, decided.raw()); + return decided; } /* parent::getParameters() */ @@ -72,18 +100,23 @@ bool pt_extended_function_variant_construct(zend_object *variant, zval *template return ExtendedFunctionVariant(variant).construct(templateTypeMap, resolvedTemplateTypeMap, parameters, isVariadic, returnType, phpDocReturnType, nativeReturnType, callSiteVarianceMap); } +zv::Val pt_extended_function_variant_get_return_type(zend_object *variant) +{ + return ExtendedFunctionVariant(variant).getReturnType(); +} + zv::Val pt_extended_function_variant_new(uint32_t argc, zval *argv) { if (EXPECTED(argc >= 7 && argc <= 8)) { zval *callSiteVarianceMap = argc == 8 ? &argv[7] : NULL; if (EXPECTED(Z_TYPE(argv[0]) == IS_OBJECT && (Z_TYPE(argv[1]) == IS_OBJECT || Z_TYPE(argv[1]) == IS_NULL) && Z_TYPE(argv[2]) == IS_ARRAY - && (Z_TYPE(argv[3]) == IS_TRUE || Z_TYPE(argv[3]) == IS_FALSE) && Z_TYPE(argv[4]) == IS_OBJECT && Z_TYPE(argv[5]) == IS_OBJECT && Z_TYPE(argv[6]) == IS_OBJECT + && (Z_TYPE(argv[3]) == IS_TRUE || Z_TYPE(argv[3]) == IS_FALSE) && (Z_TYPE(argv[4]) == IS_OBJECT || Z_TYPE(argv[4]) == IS_NULL) && Z_TYPE(argv[5]) == IS_OBJECT && Z_TYPE(argv[6]) == IS_OBJECT && (callSiteVarianceMap == NULL || Z_TYPE_P(callSiteVarianceMap) == IS_OBJECT || Z_TYPE_P(callSiteVarianceMap) == IS_NULL))) { zval object; if (UNEXPECTED(object_init_ex(&object, pt_ce_extended_function_variant) != SUCCESS)) return zv::Val(); zv::Val result = zv::Val::adopt(object); if (callSiteVarianceMap != NULL && Z_TYPE_P(callSiteVarianceMap) == IS_NULL) callSiteVarianceMap = NULL; - if (UNEXPECTED(!ExtendedFunctionVariant(Z_OBJ_P(result.raw())).construct(&argv[0], Z_TYPE(argv[1]) == IS_NULL ? NULL : &argv[1], &argv[2], Z_TYPE(argv[3]) == IS_TRUE, &argv[4], &argv[5], &argv[6], callSiteVarianceMap))) return zv::Val(); + if (UNEXPECTED(!ExtendedFunctionVariant(Z_OBJ_P(result.raw())).construct(&argv[0], Z_TYPE(argv[1]) == IS_NULL ? NULL : &argv[1], &argv[2], Z_TYPE(argv[3]) == IS_TRUE, Z_TYPE(argv[4]) == IS_NULL ? NULL : &argv[4], &argv[5], &argv[6], callSiteVarianceMap))) return zv::Val(); return result; } } @@ -112,7 +145,7 @@ PT_MINIT_REGISTRATION(pt_register_extended_function_variant) Z_PARAM_OBJECT_OF_CLASS_OR_NULL(resolvedTemplateTypeMap, pt_ce_template_type_map) Z_PARAM_ARRAY(parameters) Z_PARAM_BOOL(isVariadic) - Z_PARAM_OBJECT_OF_CLASS(returnType, pt_class(PT_CLASS_TYPE)) + Z_PARAM_OBJECT_OF_CLASS_OR_NULL(returnType, pt_class(PT_CLASS_TYPE)) Z_PARAM_OBJECT_OF_CLASS(phpDocReturnType, pt_class(PT_CLASS_TYPE)) Z_PARAM_OBJECT_OF_CLASS(nativeReturnType, pt_class(PT_CLASS_TYPE)) Z_PARAM_OPTIONAL @@ -122,6 +155,7 @@ PT_MINIT_REGISTRATION(pt_register_extended_function_variant) }); cls.method<&ExtendedFunctionVariant::getParameters>(sigs::getParameters); + cls.method<&ExtendedFunctionVariant::getReturnType>(sigs::getReturnType); cls.method<&ExtendedFunctionVariant::getPhpDocReturnType>(sigs::getPhpDocReturnType); cls.method<&ExtendedFunctionVariant::getNativeReturnType>(sigs::getNativeReturnType); diff --git a/turbo-ext/src/FunctionVariant.cpp b/turbo-ext/src/FunctionVariant.cpp index ec119f94859..f9a930ea064 100644 --- a/turbo-ext/src/FunctionVariant.cpp +++ b/turbo-ext/src/FunctionVariant.cpp @@ -177,7 +177,9 @@ zv::Val pt_function_variant_call(zend_object *variant, pt_parameters_acceptor_me case PT_PA_GET_RESOLVED_TEMPLATE_TYPE_MAP: return FunctionVariant(variant).getResolvedTemplateTypeMap(); case PT_PA_GET_PARAMETERS: return FunctionVariant(variant).getParameters(); case PT_PA_IS_VARIADIC: return FunctionVariant(variant).isVariadic(); - case PT_PA_GET_RETURN_TYPE: return FunctionVariant(variant).getReturnType(); + case PT_PA_GET_RETURN_TYPE: + if (extended) return pt_extended_function_variant_get_return_type(variant); + return FunctionVariant(variant).getReturnType(); case PT_PA_GET_CALL_SITE_VARIANCE_MAP: return FunctionVariant(variant).getCallSiteVarianceMap(); case PT_PA_GET_PHPDOC_RETURN_TYPE: if (!extended) break; diff --git a/turbo-ext/src/PhpClassReflectionExtension.cpp b/turbo-ext/src/PhpClassReflectionExtension.cpp index 89b432c2f41..68ddcb3cdec 100644 --- a/turbo-ext/src/PhpClassReflectionExtension.cpp +++ b/turbo-ext/src/PhpClassReflectionExtension.cpp @@ -2038,12 +2038,14 @@ class PhpClassReflectionExtension zv::Val signatureReturnType = call(methodSignature, PT_LC("getreturntype")); if (UNEXPECTED(signatureReturnType.isUndef())) return zv::Val(); - zv::Val returnType; + /* the PHPDoc return type decided against the signature's, unless it + * comes from stubs; the variant decides its return type lazily */ + zv::Val decidedPhpDocReturnType; if (phpDocFromStubs && Z_TYPE_P(phpDocReturnType) != IS_NULL) { - returnType = zv::Val::copyOf(zv::Ref(phpDocReturnType)); + decidedPhpDocReturnType = zv::Val::copyOf(zv::Ref(phpDocReturnType)); } else { - returnType = decideType(signatureReturnType.raw(), phpDocReturnType); - if (UNEXPECTED(returnType.isUndef())) return zv::Val(); + decidedPhpDocReturnType = decideType(signatureReturnType.raw(), phpDocReturnType); + if (UNEXPECTED(decidedPhpDocReturnType.isUndef())) return zv::Val(); } zv::Val ownedEmptyMap = templateTypeMapEmpty(); @@ -2052,19 +2054,14 @@ class PhpClassReflectionExtension if (UNEXPECTED(!ok)) return zv::Val(); zv::Val nativeReturnType = call(methodSignature, PT_LC("getnativereturntype")); if (UNEXPECTED(nativeReturnType.isUndef())) return zv::Val(); - zv::Val mixedReturnType; - if (Z_TYPE_P(phpDocReturnType) == IS_NULL) { - mixedReturnType = mixedType(); - if (UNEXPECTED(mixedReturnType.isUndef())) return zv::Val(); - } zval variantArgs[7]; ZVAL_COPY_VALUE(&variantArgs[0], ownedEmptyMap.raw()); ZVAL_NULL(&variantArgs[1]); ZVAL_COPY_VALUE(&variantArgs[2], parameters.raw()); ZVAL_BOOL(&variantArgs[3], isVariadicSignature); - ZVAL_COPY_VALUE(&variantArgs[4], returnType.raw()); - ZVAL_COPY_VALUE(&variantArgs[5], Z_TYPE_P(phpDocReturnType) == IS_NULL ? mixedReturnType.raw() : phpDocReturnType); + ZVAL_NULL(&variantArgs[4]); + ZVAL_COPY_VALUE(&variantArgs[5], decidedPhpDocReturnType.raw()); ZVAL_COPY_VALUE(&variantArgs[6], nativeReturnType.raw()); return pt_extended_function_variant_new(7, variantArgs); } diff --git a/turbo-ext/src/TypeTraits.cpp b/turbo-ext/src/TypeTraits.cpp index 58b93068057..5c430c1d459 100644 --- a/turbo-ext/src/TypeTraits.cpp +++ b/turbo-ext/src/TypeTraits.cpp @@ -5244,34 +5244,32 @@ zv::Val protoParameter(PrototypeKind kind, const PrototypeTransformer &transform * UNDEF = pending exception */ zv::Val protoVariant(PrototypeKind kind, const PrototypeTransformer &transformer, zend_object *acceptor, zv::Val &selfOutType) { - zv::Val originalReturnType = protoAcceptorCall(acceptor, PT_PA_GET_RETURN_TYPE, "getreturntype", PROTO_OBJECT); - if (UNEXPECTED(originalReturnType.isUndef())) return zv::Val(); + zv::Val originalPhpDocReturnType = protoAcceptorCall(acceptor, PT_PA_GET_PHPDOC_RETURN_TYPE, "getphpdocreturntype", PROTO_OBJECT); + if (UNEXPECTED(originalPhpDocReturnType.isUndef())) return zv::Val(); bool returnsThis; - pt_type_instanceof_ce(originalReturnType.raw(), pt_ce_this_type, returnsThis); - zv::Val returnType, transformedReturnType; + pt_type_instanceof_ce(originalPhpDocReturnType.raw(), pt_ce_this_type, returnsThis); + zv::Val phpDocReturnType, transformedPhpDocReturnType; if (kind == phpstanturbo::PT_PROTOTYPE_CALLED_ON_TYPE) { - /* $originalReturnType instanceof ThisType && $selfOutType !== null ? $selfOutType : transform($originalReturnType) */ + /* $originalPhpDocReturnType instanceof ThisType && $selfOutType !== null ? $selfOutType : transform($originalPhpDocReturnType) */ if (returnsThis && !selfOutType.isNull()) { - returnType = zv::Val::copyOf(zv::Ref(selfOutType.raw())); + phpDocReturnType = zv::Val::copyOf(zv::Ref(selfOutType.raw())); } else { - returnType = transformer.transform(originalReturnType.raw()); - if (UNEXPECTED(returnType.isUndef())) return zv::Val(); + phpDocReturnType = transformer.transform(originalPhpDocReturnType.raw()); + if (UNEXPECTED(phpDocReturnType.isUndef())) return zv::Val(); } } else { - transformedReturnType = transformer.transform(originalReturnType.raw()); - if (UNEXPECTED(transformedReturnType.isUndef())) return zv::Val(); + transformedPhpDocReturnType = transformer.transform(originalPhpDocReturnType.raw()); + if (UNEXPECTED(transformedPhpDocReturnType.isUndef())) return zv::Val(); if (returnsThis && !selfOutType.isNull()) { - /* $returnType = TypeCombinator::intersect($selfOutType, $transformedReturnType); $selfOutType = $returnType; */ - zv::Args args{selfOutType.raw(), transformedReturnType.raw()}; - returnType = pt_type_combinator_call(PT_LC("intersect"), 2, args); - if (UNEXPECTED(returnType.isUndef())) return zv::Val(); - selfOutType = zv::Val::copyOf(zv::Ref(returnType.raw())); + /* $phpDocReturnType = TypeCombinator::intersect($selfOutType, $transformedPhpDocReturnType); $selfOutType = $phpDocReturnType; */ + zv::Args args{selfOutType.raw(), transformedPhpDocReturnType.raw()}; + phpDocReturnType = pt_type_combinator_call(PT_LC("intersect"), 2, args); + if (UNEXPECTED(phpDocReturnType.isUndef())) return zv::Val(); + selfOutType = zv::Val::copyOf(zv::Ref(phpDocReturnType.raw())); } else { - returnType = zv::Val::copyOf(zv::Ref(transformedReturnType.raw())); + phpDocReturnType = zv::Val::copyOf(zv::Ref(transformedPhpDocReturnType.raw())); } } - zv::Val phpDocReturnType = protoAcceptorCall(acceptor, PT_PA_GET_PHPDOC_RETURN_TYPE, "getphpdocreturntype", PROTO_OBJECT); - if (UNEXPECTED(phpDocReturnType.isUndef())) return zv::Val(); zv::Val nativeReturnType = protoAcceptorCall(acceptor, PT_PA_GET_NATIVE_RETURN_TYPE, "getnativereturntype", PROTO_OBJECT); if (UNEXPECTED(nativeReturnType.isUndef())) return zv::Val(); zv::Val templateTypeMap = protoAcceptorCall(acceptor, PT_PA_GET_TEMPLATE_TYPE_MAP, "gettemplatetypemap", PROTO_ANY); @@ -5284,22 +5282,22 @@ zv::Val protoVariant(PrototypeKind kind, const PrototypeTransformer &transformer if (UNEXPECTED(mappedParameters.isUndef())) return zv::Val(); zv::Val variadic = protoAcceptorCall(acceptor, PT_PA_IS_VARIADIC, "isvariadic", PROTO_ANY); if (UNEXPECTED(variadic.isUndef())) return zv::Val(); - /* the Callback twin reuses the transformed return type for an equal - * PHPDoc / native return type; the CalledOnType twin transforms both */ - zval *transformedOriginal = kind == phpstanturbo::PT_PROTOTYPE_CALLBACK ? transformedReturnType.raw() : NULL; - zv::Val transformedPhpDocReturnType = protoTransformUnlessEqual(kind, transformer, originalReturnType.raw(), transformedOriginal, phpDocReturnType.raw()); - if (UNEXPECTED(transformedPhpDocReturnType.isUndef())) return zv::Val(); - zv::Val transformedNativeReturnType = protoTransformUnlessEqual(kind, transformer, originalReturnType.raw(), transformedOriginal, nativeReturnType.raw()); + /* the Callback twin reuses the transformed PHPDoc return type for an + * equal native return type; the CalledOnType twin transforms it */ + zval *transformedOriginal = kind == phpstanturbo::PT_PROTOTYPE_CALLBACK ? transformedPhpDocReturnType.raw() : NULL; + zv::Val transformedNativeReturnType = protoTransformUnlessEqual(kind, transformer, originalPhpDocReturnType.raw(), transformedOriginal, nativeReturnType.raw()); if (UNEXPECTED(transformedNativeReturnType.isUndef())) return zv::Val(); zv::Val callSiteVarianceMap = protoAcceptorCall(acceptor, PT_PA_GET_CALL_SITE_VARIANCE_MAP, "getcallsitevariancemap", PROTO_ANY); if (UNEXPECTED(callSiteVarianceMap.isUndef())) return zv::Val(); + /* the return type is left null: the variant decides it from the + * transformed PHPDoc and native return types */ zval args[8]; ZVAL_COPY_VALUE(&args[0], templateTypeMap.raw()); ZVAL_COPY_VALUE(&args[1], resolvedTemplateTypeMap.raw()); ZVAL_COPY_VALUE(&args[2], mappedParameters.raw()); ZVAL_COPY_VALUE(&args[3], variadic.raw()); - ZVAL_COPY_VALUE(&args[4], returnType.raw()); - ZVAL_COPY_VALUE(&args[5], transformedPhpDocReturnType.raw()); + ZVAL_NULL(&args[4]); + ZVAL_COPY_VALUE(&args[5], phpDocReturnType.raw()); ZVAL_COPY_VALUE(&args[6], transformedNativeReturnType.raw()); ZVAL_COPY_VALUE(&args[7], callSiteVarianceMap.raw()); return pt_extended_function_variant_new(8, args); diff --git a/turbo-ext/src/generated/ExtendedCallableFunctionVariant.h b/turbo-ext/src/generated/ExtendedCallableFunctionVariant.h index 3944c75aad2..5f5cc2597f7 100644 --- a/turbo-ext/src/generated/ExtendedCallableFunctionVariant.h +++ b/turbo-ext/src/generated/ExtendedCallableFunctionVariant.h @@ -10,15 +10,15 @@ namespace ptdecl::ExtendedCallableFunctionVariant { /* the OBJ_PROP_NUM slots of the instance properties the class declares (the inherited ones come first) */ namespace slot { -inline constexpr uint32_t throwPoints = 8; -inline constexpr uint32_t isPure = 9; -inline constexpr uint32_t impurePoints = 10; -inline constexpr uint32_t invalidateExpressions = 11; -inline constexpr uint32_t usedVariables = 12; -inline constexpr uint32_t acceptsNamedArguments = 13; -inline constexpr uint32_t mustUseReturnValue = 14; -inline constexpr uint32_t assertions = 15; -inline constexpr uint32_t isStatic = 16; +inline constexpr uint32_t throwPoints = 9; +inline constexpr uint32_t isPure = 10; +inline constexpr uint32_t impurePoints = 11; +inline constexpr uint32_t invalidateExpressions = 12; +inline constexpr uint32_t usedVariables = 13; +inline constexpr uint32_t acceptsNamedArguments = 14; +inline constexpr uint32_t mustUseReturnValue = 15; +inline constexpr uint32_t assertions = 16; +inline constexpr uint32_t isStatic = 17; } // namespace slot inline void declareClass(reg::Class &cls) diff --git a/turbo-ext/src/generated/ExtendedFunctionVariant.h b/turbo-ext/src/generated/ExtendedFunctionVariant.h index 0004fa9e706..430e0e85fbd 100644 --- a/turbo-ext/src/generated/ExtendedFunctionVariant.h +++ b/turbo-ext/src/generated/ExtendedFunctionVariant.h @@ -10,8 +10,9 @@ namespace ptdecl::ExtendedFunctionVariant { /* the OBJ_PROP_NUM slots of the instance properties the class declares (the inherited ones come first) */ namespace slot { -inline constexpr uint32_t phpDocReturnType = 6; -inline constexpr uint32_t nativeReturnType = 7; +inline constexpr uint32_t returnType = 6; +inline constexpr uint32_t phpDocReturnType = 7; +inline constexpr uint32_t nativeReturnType = 8; } // namespace slot inline void declareClass(reg::Class &cls) @@ -23,6 +24,7 @@ inline void declareClass(reg::Class &cls) /* the properties the class declares itself, in declaration order (a used trait's come from its registrar) */ inline void declareProperties(reg::Class &cls) { + cls.property("returnType", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_NULL, "PHPStan\\Type\\Type"); cls.property("phpDocReturnType", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Type\\Type"); cls.property("nativeReturnType", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Type\\Type"); } @@ -45,18 +47,20 @@ inline constexpr char strings[] = "__construct\0" /* 232 */ "\0" /* 244 */ "getParameters\0" /* 245 */ - "getPhpDocReturnType\0" /* 259 */ - "getNativeReturnType"; /* 279 */ + "getReturnType\0" /* 259 */ + "getPhpDocReturnType\0" /* 273 */ + "getNativeReturnType"; /* 293 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 16), /* __construct $templateTypeMap */ reg::packed(53, MAY_BE_NULL, 16), /* __construct $resolvedTemplateTypeMap */ reg::packed(77, MAY_BE_ARRAY), /* __construct $parameters */ reg::packed(88, MAY_BE_BOOL), /* __construct $isVariadic */ - reg::packed(99, 0, 110), /* __construct $returnType */ + reg::packed(99, MAY_BE_NULL, 110), /* __construct $returnType */ reg::packed(128, 0, 110), /* __construct $phpDocReturnType */ reg::packed(145, 0, 110), /* __construct $nativeReturnType */ reg::packed(162, MAY_BE_NULL, 182, false, false, 227), /* __construct $callSiteVarianceMap */ reg::packed(244, MAY_BE_ARRAY), /* getParameters return */ + reg::packed(244, 0, 110), /* getReturnType return */ reg::packed(244, 0, 110), /* getPhpDocReturnType return */ reg::packed(244, 0, 110), /* getNativeReturnType return */ }; @@ -67,8 +71,9 @@ using Sig = reg::Sig; namespace sig { inline constexpr sigtab::Sig __construct = { { 232 /* __construct */, 7, 0, 8, reg::NoArg, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig getParameters = { { 245 /* getParameters */, 0, 8, 0, 8, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getPhpDocReturnType = { { 259 /* getPhpDocReturnType */, 0, 9, 0, 9, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getNativeReturnType = { { 279 /* getNativeReturnType */, 0, 10, 0, 10, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getReturnType = { { 259 /* getReturnType */, 0, 9, 0, 9, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getPhpDocReturnType = { { 273 /* getPhpDocReturnType */, 0, 10, 0, 10, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getNativeReturnType = { { 293 /* getNativeReturnType */, 0, 11, 0, 11, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::ExtendedFunctionVariant diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h index 6c0e26f9c3a..83e805f674f 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -3916,6 +3916,9 @@ zv::Val pt_extended_callable_function_variant_new(uint32_t argc, zval *argv); * for the nullable nulls); false = pending exception */ [[nodiscard]] bool pt_function_variant_construct(zend_object *variant, zval *templateTypeMap, zval *resolvedTemplateTypeMap, zval *parameters, bool isVariadic, zval *returnType, zval *callSiteVarianceMap); [[nodiscard]] bool pt_extended_function_variant_construct(zend_object *variant, zval *templateTypeMap, zval *resolvedTemplateTypeMap, zval *parameters, bool isVariadic, zval *returnType, zval *phpDocReturnType, zval *nativeReturnType, zval *callSiteVarianceMap); +/* ExtendedFunctionVariant::getReturnType() of a native extended variant; + * UNDEF = pending exception */ +zv::Val pt_extended_function_variant_get_return_type(zend_object *variant); /* new ResolvedFunctionVariantWithOriginal(...) (borrowed, already of the * constructor's parameter types) / new TrivialParametersAcceptor($callableName) * (NULL for the default); UNDEF = pending exception */ From bf8332191bbac4886bc981ad7a129def4d502ad7 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 28 Apr 2025 21:17:16 +0200 Subject: [PATCH 2/2] Allow Traversable in PHPDoc and Iterator in native type to cooperate --- phpstan-baseline.neon | 6 +++++ src/Type/TypehintHelper.php | 18 +++++++++++++- turbo-ext/src/TypehintHelper.cpp | 41 ++++++++++++++++++++++++++++++-- 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 301f9f86803..7ab668de967 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1723,6 +1723,12 @@ parameters: count: 1 path: src/Type/TypehintHelper.php + - + rawMessage: 'Doing instanceof PHPStan\Type\TypeWithClassName is error-prone and deprecated. Use Type::getObjectClassNames() or Type::getObjectClassReflections() instead.' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/TypehintHelper.php + - rawMessage: 'Doing instanceof PHPStan\Type\CallableType is error-prone and deprecated. Use Type::isCallable() and Type::getCallableParametersAcceptors() instead.' identifier: phpstanApi.instanceofType diff --git a/src/Type/TypehintHelper.php b/src/Type/TypehintHelper.php index 6fdab79212b..5a4c4786c42 100644 --- a/src/Type/TypehintHelper.php +++ b/src/Type/TypehintHelper.php @@ -13,6 +13,7 @@ use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Generic\TemplateTypeHelper; use ReflectionType; +use Traversable; use function array_map; use function count; use function get_class; @@ -126,11 +127,26 @@ public static function decideType( } } + $resolvedPhpDocTypeToBounds = TemplateTypeHelper::resolveToBounds($phpDocType); + $isSuperType = $type->isSuperTypeOf($resolvedPhpDocTypeToBounds); + if ( + !$isSuperType->yes() + && (new ObjectType(Traversable::class))->isSuperTypeOf($phpDocType)->yes() + && $type instanceof TypeWithClassName + ) { + // if native type is Iterator and PHPDoc type is Traversable + // Allow PHPDoc type to win + $traversableAncestor = $type->getAncestorWithClassName(Traversable::class); + if ($traversableAncestor !== null) { + $isSuperType = $traversableAncestor->isSuperTypeOf($resolvedPhpDocTypeToBounds); + } + } + if ( ($type->isCallable()->yes() && $phpDocType->isCallable()->yes()) || ( (!$phpDocType instanceof NeverType || ($type instanceof MixedType && !$type->isExplicitMixed())) - && $type->isSuperTypeOf(TemplateTypeHelper::resolveToBounds($phpDocType))->yes() + && $isSuperType->yes() ) ) { $resultType = $phpDocType; diff --git a/turbo-ext/src/TypehintHelper.cpp b/turbo-ext/src/TypehintHelper.cpp index bd0b0286049..d6767544445 100644 --- a/turbo-ext/src/TypehintHelper.cpp +++ b/turbo-ext/src/TypehintHelper.cpp @@ -357,8 +357,12 @@ class TypehintHelper /* ($type->isCallable()->yes() && $phpDocType->isCallable()->yes()) * || ((!$phpDocType instanceof NeverType || ($type instanceof MixedType && !$type->isExplicitMixed())) - * && $type->isSuperTypeOf(TemplateTypeHelper::resolveToBounds($phpDocType))->yes()) - * — short-circuited as the twin evaluates it; false = pending exception */ + * && $isSuperType->yes()), where $isSuperType is + * $type->isSuperTypeOf(TemplateTypeHelper::resolveToBounds($phpDocType)), + * or for a native TypeWithClassName below a Traversable PHPDoc type its + * Traversable ancestor's verdict — the twin computes $isSuperType up + * front, here it is computed where it decides (pure calls); false = + * pending exception */ [[nodiscard]] static bool decidesForPhpDoc(zval *type, zval *phpDocType, bool &out) { zend_long typeCallable = pt_type_op_trinary(Z_OBJ_P(type), PT_OP_IS_CALLABLE, 0, NULL); @@ -392,6 +396,39 @@ class TypehintHelper if (UNEXPECTED(isSuperType.isUndef())) return false; zend_long trinary = pt_type_result_trinary(isSuperType.raw()); if (UNEXPECTED(trinary < 0)) return false; + if (trinary != PT_TRI_YES) { + /* if native type is Iterator and PHPDoc type is Traversable, + * allow PHPDoc type to win: + * (new ObjectType(Traversable::class))->isSuperTypeOf($phpDocType)->yes() + * && $type instanceof TypeWithClassName */ + zval traversableRaw; + zend_string *traversableName = zend_string_init("Traversable", sizeof("Traversable") - 1, 0); + bool created = pt_object_type_new(&traversableRaw, traversableName); + zend_string_release(traversableName); + if (UNEXPECTED(!created)) return false; + zv::Val traversableType = zv::Val::adopt(traversableRaw); + zv::Val phpDocTraversable = pt_type_op(Z_OBJ_P(traversableType.raw()), PT_OP_IS_SUPER_TYPE_OF, 1, phpDocType); + if (UNEXPECTED(phpDocTraversable.isUndef())) return false; + zend_long phpDocIsTraversable = pt_type_result_trinary(phpDocTraversable.raw()); + if (UNEXPECTED(phpDocIsTraversable < 0)) return false; + if (phpDocIsTraversable == PT_TRI_YES) { + bool withClassName; + if (UNEXPECTED(!pt_type_instanceof(type, PT_CLASS_TYPE_WITH_CLASS_NAME, withClassName))) return false; + if (withClassName) { + zval className; + ZVAL_STRINGL(&className, "Traversable", sizeof("Traversable") - 1); + zv::Val ancestor = pt_type_call(Z_OBJ_P(type), PT_LC("getancestorwithclassname"), 1, &className); + zval_ptr_dtor(&className); + if (UNEXPECTED(ancestor.isUndef())) return false; + if (!ancestor.isNull()) { + zv::Val ancestorIsSuperType = pt_type_op(Z_OBJ_P(ancestor.raw()), PT_OP_IS_SUPER_TYPE_OF, 1, bounds.raw()); + if (UNEXPECTED(ancestorIsSuperType.isUndef())) return false; + trinary = pt_type_result_trinary(ancestorIsSuperType.raw()); + if (UNEXPECTED(trinary < 0)) return false; + } + } + } + } out = trinary == PT_TRI_YES; return true; }