From 15fa8a2f1208233d92c989eb0517d319cbffd489 Mon Sep 17 00:00:00 2001 From: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Date: Wed, 4 Nov 2026 12:36:10 +0000 Subject: [PATCH 1/3] Resolve `static` in parameter and writable property types to the called-on type without the `new Foo()` exactness - `CalledOnTypeUnresolvedMethodPrototypeReflection::transformStaticType()` now takes an `$isParameterType` flag. For parameter types (`getType()` and `getPhpDocType()`) it replaces `static` with `TemplateTypeHelper::removeFinalByKeywordOverrides($calledOnType)`. Without this, `$b = new B(); $b->method(new A($b))` with `@param A` expected `A`, but the inferred `A` never carries exactness, so it was rejected. - `CalledOnTypeUnresolvedPropertyPrototypeReflection` does the same for the writable type, so `$b->prop = new A($b)` is accepted for `@var A`. - Return, readable, self-out, out, closure-this, throw and assert types keep the exact flavour, so `(new Foo())->fluent()` stays exact. - turbo-ext: `PrototypeTransformer` gains an `inputFn`, used for parameter types and the writable property type. The CalledOnType natives pass the flag through the native callback state and strip through `TemplateTypeHelper::removeFinalByKeywordOverrides()`. The Callback natives use the same fn for both. - Covered: instance method calls, static method calls on `$b::`, first-class callables (`$b->method(...)`) and property assignments. The invariant `A` parameter and nested `static` in return types already worked. Co-Authored-By: Claude Opus 5.5 --- ...ypeUnresolvedMethodPrototypeReflection.php | 29 ++++++++--- ...eUnresolvedPropertyPrototypeReflection.php | 27 +++++++--- .../Rules/Functions/CallCallablesRuleTest.php | 6 +++ .../Rules/Methods/CallMethodsRuleTest.php | 9 ++++ .../Methods/CallStaticMethodsRuleTest.php | 7 +++ .../PHPStan/Rules/Methods/data/bug-15235.php | 41 +++++++++++++++ .../TypesAssignedToPropertiesRuleTest.php | 6 +++ .../Rules/Properties/data/bug-15235.php | 25 +++++++++ ...ackUnresolvedMethodPrototypeReflection.cpp | 2 +- ...kUnresolvedPropertyPrototypeReflection.cpp | 2 +- ...ypeUnresolvedMethodPrototypeReflection.cpp | 50 ++++++++++++++---- ...eUnresolvedPropertyPrototypeReflection.cpp | 51 +++++++++++++++---- turbo-ext/src/TypeTraits.cpp | 13 ++--- turbo-ext/src/TypeTraits.h | 7 ++- 14 files changed, 235 insertions(+), 40 deletions(-) create mode 100644 tests/PHPStan/Rules/Methods/data/bug-15235.php create mode 100644 tests/PHPStan/Rules/Properties/data/bug-15235.php diff --git a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php index ba61cd9c1fc..e7c2d01e805 100644 --- a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php @@ -12,6 +12,7 @@ use PHPStan\Reflection\ResolvedMethodReflection; use PHPStan\Turbo\ShadowedByTurboExtension; use PHPStan\Type\Generic\GenericStaticType; +use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\StaticType; use PHPStan\Type\ThisType; use PHPStan\Type\Type; @@ -96,13 +97,13 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass, array_map( fn (ExtendedParameterReflection $parameter): ExtendedParameterReflection => new ExtendedDummyParameter( $parameter->getName(), - $this->transformStaticType($parameter->getType()), + $this->transformStaticType($parameter->getType(), true), $parameter->isOptional(), $parameter->passedByReference(), $parameter->isVariadic(), $parameter->getDefaultValue(), $parameter->getNativeType(), - $this->transformStaticType($parameter->getPhpDocType()), + $this->transformStaticType($parameter->getPhpDocType(), true), $parameter->getOutType() !== null ? $this->transformStaticType($parameter->getOutType()) : null, $parameter->isImmediatelyInvokedCallable(), $parameter->getClosureThisType() !== null ? $this->transformStaticType($parameter->getClosureThisType()) : null, @@ -141,25 +142,39 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass, ); } - private function transformStaticType(Type $type): Type + /** + * A parameter type is compared against argument types, whose template arguments never carry + * the "exactly this class" flavour of `new Foo()` - so `static` resolved inside it must not either. + */ + private function transformStaticType(Type $type, bool $isParameterType = false): Type { - return TypeTraverser::mapMemoized($type, function (Type $type, callable $traverse): Type { + return TypeTraverser::mapMemoized($type, function (Type $type, callable $traverse) use ($isParameterType): Type { if ($type instanceof GenericStaticType) { - $calledOnTypeReflections = $this->calledOnType->getObjectClassReflections(); + $calledOnType = $this->getCalledOnType($isParameterType); + $calledOnTypeReflections = $calledOnType->getObjectClassReflections(); if (count($calledOnTypeReflections) === 1) { $calledOnTypeReflection = $calledOnTypeReflections[0]; return $traverse($type->changeBaseClass($calledOnTypeReflection)->getStaticObjectType()); } - return $this->calledOnType; + return $calledOnType; } if ($type instanceof StaticType) { - return $this->calledOnType; + return $this->getCalledOnType($isParameterType); } return $traverse($type); }); } + private function getCalledOnType(bool $isParameterType): Type + { + if ($isParameterType) { + return TemplateTypeHelper::removeFinalByKeywordOverrides($this->calledOnType); + } + + return $this->calledOnType; + } + } diff --git a/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php b/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php index eda211fb11e..22aa71097cb 100644 --- a/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php +++ b/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php @@ -8,6 +8,7 @@ use PHPStan\Reflection\ResolvedPropertyReflection; use PHPStan\Turbo\ShadowedByTurboExtension; use PHPStan\Type\Generic\GenericStaticType; +use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\StaticType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; @@ -77,30 +78,44 @@ public function withFechedOnType(Type $type): UnresolvedPropertyPrototypeReflect private function transformPropertyWithStaticType(ClassReflection $declaringClass, ExtendedPropertyReflection $property): ExtendedPropertyReflection { $readableType = $this->transformStaticType($property->getReadableType()); - $writableType = $this->transformStaticType($property->getWritableType()); + $writableType = $this->transformStaticType($property->getWritableType(), true); $phpDocType = $this->transformStaticType($property->getPhpDocType()); $nativeType = $this->transformStaticType($property->getNativeType()); return new ChangedTypePropertyReflection($declaringClass, $property, $readableType, $writableType, $phpDocType, $nativeType); } - private function transformStaticType(Type $type): Type + /** + * A writable type is compared against assigned types, whose template arguments never carry + * the "exactly this class" flavour of `new Foo()` - so `static` resolved inside it must not either. + */ + private function transformStaticType(Type $type, bool $isWritableType = false): Type { - return TypeTraverser::map($type, function (Type $type, callable $traverse): Type { + return TypeTraverser::map($type, function (Type $type, callable $traverse) use ($isWritableType): Type { if ($type instanceof GenericStaticType) { - $fetchedOnTypeReflections = $this->fetchedOnType->getObjectClassReflections(); + $fetchedOnType = $this->getFetchedOnType($isWritableType); + $fetchedOnTypeReflections = $fetchedOnType->getObjectClassReflections(); if (count($fetchedOnTypeReflections) === 1) { return $traverse($type->changeBaseClass($fetchedOnTypeReflections[0])->getStaticObjectType()); } - return $this->fetchedOnType; + return $fetchedOnType; } if ($type instanceof StaticType) { - return $this->fetchedOnType; + return $this->getFetchedOnType($isWritableType); } return $traverse($type); }); } + private function getFetchedOnType(bool $isWritableType): Type + { + if ($isWritableType) { + return TemplateTypeHelper::removeFinalByKeywordOverrides($this->fetchedOnType); + } + + return $this->fetchedOnType; + } + } diff --git a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php index a4ae024b9df..510efc6e821 100644 --- a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php @@ -468,6 +468,12 @@ public function testBug15442(): void $this->analyse([__DIR__ . '/data/bug-15442.php'], []); } + #[RequiresPhp('>= 8.1.0')] + public function testBug15235(): void + { + $this->analyse([__DIR__ . '/../Methods/data/bug-15235.php'], []); + } + #[RequiresPhp('>= 8.0.0')] public function testBug11935WithCheckExplicitMixed(): void { diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 62761326049..62658809769 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -4537,6 +4537,15 @@ public function testBug15166(): void $this->analyse([__DIR__ . '/data/bug-15166.php'], []); } + #[RequiresPhp('>= 8.1.0')] + public function testBug15235(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-15235.php'], []); + } + public function testBug6732(): void { $this->checkThisOnly = false; diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index f499119a6f0..6da5e402076 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -1158,4 +1158,11 @@ public function testBug11421(): void ]); } + #[RequiresPhp('>= 8.1.0')] + public function testBug15235(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/bug-15235.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-15235.php b/tests/PHPStan/Rules/Methods/data/bug-15235.php new file mode 100644 index 00000000000..39591a0b981 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-15235.php @@ -0,0 +1,41 @@ += 8.1 + +namespace Bug15235; + +/** + * @template T of object + */ +class A +{ + /** + * @param T $object + */ + public function __construct(public object $object) {} +} + +class B { + /** + * @param A $a + */ + public function method(A $a): void {} + + /** + * @param A $a + */ + public static function staticMethod(A $a): void {} +} + +class D { + public function inconsistent(B $b): void { + $b->method(new A($b)); // (!) No error. + } +} + +$b = new B(); + +$b->method(new A($b)); // (!) Parameter #1 $a of method B::method() expects A, A given. + +$b::staticMethod(new A($b)); + +$callable = $b->method(...); +$callable(new A($b)); diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index 885902a72b7..26a204eb64b 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -1101,6 +1101,12 @@ public function testBug15166(): void $this->analyse([__DIR__ . '/data/bug-15166.php'], []); } + #[RequiresPhp('>= 8.0.0')] + public function testBug15235(): void + { + $this->analyse([__DIR__ . '/data/bug-15235.php'], []); + } + public function testBug6732(): void { $this->analyse([__DIR__ . '/data/bug-6732.php'], [ diff --git a/tests/PHPStan/Rules/Properties/data/bug-15235.php b/tests/PHPStan/Rules/Properties/data/bug-15235.php new file mode 100644 index 00000000000..af032b423fc --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-15235.php @@ -0,0 +1,25 @@ += 8.0 + +namespace Bug15235Properties; + +/** + * @template T of object + */ +class A +{ + /** + * @param T $object + */ + public function __construct(public object $object) {} +} + +class B +{ + /** @var A|null */ + public $a = null; +} + +function (): void { + $b = new B(); + $b->a = new A($b); +}; diff --git a/turbo-ext/src/CallbackUnresolvedMethodPrototypeReflection.cpp b/turbo-ext/src/CallbackUnresolvedMethodPrototypeReflection.cpp index 8e595528a65..be30f84b0b7 100644 --- a/turbo-ext/src/CallbackUnresolvedMethodPrototypeReflection.cpp +++ b/turbo-ext/src/CallbackUnresolvedMethodPrototypeReflection.cpp @@ -91,7 +91,7 @@ class CallbackUnresolvedMethodPrototypeReflection zval *resolveToBounds = methodReflection != NULL ? slot(slots::resolveTemplateTypeMapToBounds, "resolveTemplateTypeMapToBounds") : NULL; if (UNEXPECTED(resolveToBounds == NULL)) return zv::Val(); /* $method->getAsserts()->mapTypes($this->transformStaticTypeCallback) */ - PrototypeTransformer transformer = { transformStaticType, self }; + PrototypeTransformer transformer = { transformStaticType, transformStaticType, self }; zv::Val result = pt_prototype_resolved_method(PT_PROTOTYPE_CALLBACK, transformer, Z_OBJ_P(resolvedDeclaringClass), Z_OBJ_P(methodReflection), zend_is_true(resolveToBounds), OBJ_PROP_NUM(self, slots::transformStaticTypeCallback)); if (UNEXPECTED(result.isUndef())) return zv::Val(); write(slots::transformedMethod, zv::Val::copyOf(zv::Ref(result.raw()))); diff --git a/turbo-ext/src/CallbackUnresolvedPropertyPrototypeReflection.cpp b/turbo-ext/src/CallbackUnresolvedPropertyPrototypeReflection.cpp index 1256a2aa6a8..c8ede4a32af 100644 --- a/turbo-ext/src/CallbackUnresolvedPropertyPrototypeReflection.cpp +++ b/turbo-ext/src/CallbackUnresolvedPropertyPrototypeReflection.cpp @@ -88,7 +88,7 @@ class CallbackUnresolvedPropertyPrototypeReflection zval *propertyReflection = resolvedDeclaringClass != NULL ? slot(slots::propertyReflection, "propertyReflection") : NULL; zval *resolveToBounds = propertyReflection != NULL ? slot(slots::resolveTemplateTypeMapToBounds, "resolveTemplateTypeMapToBounds") : NULL; if (UNEXPECTED(resolveToBounds == NULL)) return zv::Val(); - PrototypeTransformer transformer = { transformStaticType, self }; + PrototypeTransformer transformer = { transformStaticType, transformStaticType, self }; zv::Val result = pt_prototype_resolved_property(PT_PROTOTYPE_CALLBACK, transformer, Z_OBJ_P(resolvedDeclaringClass), Z_OBJ_P(propertyReflection), zend_is_true(resolveToBounds)); if (UNEXPECTED(result.isUndef())) return zv::Val(); write(slots::transformedProperty, zv::Val::copyOf(zv::Ref(result.raw()))); diff --git a/turbo-ext/src/CalledOnTypeUnresolvedMethodPrototypeReflection.cpp b/turbo-ext/src/CalledOnTypeUnresolvedMethodPrototypeReflection.cpp index 0a1465aa224..d220b76e52c 100644 --- a/turbo-ext/src/CalledOnTypeUnresolvedMethodPrototypeReflection.cpp +++ b/turbo-ext/src/CalledOnTypeUnresolvedMethodPrototypeReflection.cpp @@ -96,7 +96,7 @@ class CalledOnTypeUnresolvedMethodPrototypeReflection ZVAL_OBJ(&thisZv, self); zv::Val assertsCallback = pt_type_native_callback(transformStaticTypeCallback, &thisZv, NULL); if (UNEXPECTED(assertsCallback.isUndef())) return zv::Val(); - PrototypeTransformer transformer = { transformStaticType, self }; + PrototypeTransformer transformer = { transformStaticType, transformInputStaticType, self }; zv::Val result = pt_prototype_resolved_method(PT_PROTOTYPE_CALLED_ON_TYPE, transformer, Z_OBJ_P(resolvedDeclaringClass), Z_OBJ_P(methodReflection), zend_is_true(resolveToBounds), assertsCallback.raw()); if (UNEXPECTED(result.isUndef())) return zv::Val(); write(slots::transformedMethod, zv::Val::copyOf(zv::Ref(result.raw()))); @@ -143,9 +143,37 @@ class CalledOnTypeUnresolvedMethodPrototypeReflection return value; } + /* getCalledOnType($flag): $this->calledOnType, or for the types values + * are checked against TemplateTypeHelper::removeFinalByKeywordOverrides($this->calledOnType); + * UNDEF = pending exception */ + zv::Val calledOnTypeFor(zval *flag) const + { + zval *calledOnType = slot(slots::calledOnType, "calledOnType"); + if (UNEXPECTED(calledOnType == NULL)) return zv::Val(); + if (flag == NULL || Z_TYPE_P(flag) != IS_TRUE) return zv::Val::copyOf(zv::Ref(calledOnType)); + zv::Val stripped = pt_type_call_static_ce(pt_ce_template_type_helper, PT_LC("removefinalbykeywordoverrides"), 1, calledOnType); + if (UNEXPECTED(stripped.isUndef())) return zv::Val(); + if (UNEXPECTED(!zv::Ref(stripped.raw()).isObject())) { + zend_type_error("phpstan_turbo: TemplateTypeHelper::removeFinalByKeywordOverrides() must return %s", ptcls::type); + return zv::Val(); + } + return stripped; + } + /* transformStaticType($type): TypeTraverser::mapMemoized($type, the * closure below over $this); UNDEF = pending exception */ static zv::Val transformStaticType(void *context, zval *type) + { + return transformStaticTypeOf(context, type, false); + } + + /* transformStaticType($type, true) — for parameter types */ + static zv::Val transformInputStaticType(void *context, zval *type) + { + return transformStaticTypeOf(context, type, true); + } + + static zv::Val transformStaticTypeOf(void *context, zval *type, bool isParameterType) { /* the callback changes StaticTypes only: a type without one maps to * itself */ @@ -154,7 +182,10 @@ class CalledOnTypeUnresolvedMethodPrototypeReflection if (!containsStatic) return zv::Val::copyOf(zv::Ref(type)); zval thisZv; ZVAL_OBJ(&thisZv, (zend_object *) context); - zv::Val callback = pt_type_native_callback(traverse, &thisZv, NULL); + /* the closure's `use ($isParameterType)` */ + zval flag; + ZVAL_BOOL(&flag, isParameterType); + zv::Val callback = pt_type_native_callback(traverse, &thisZv, &flag); if (UNEXPECTED(callback.isUndef())) return zv::Val(); return pt_type_traverser_map_memoized_of(type, callback.raw()); } @@ -183,7 +214,6 @@ class CalledOnTypeUnresolvedMethodPrototypeReflection * the called-on type; everything else traverses */ static void traverse(zval *thisZv, zval *state1, uint32_t argc, zval *argv, zval *return_value) { - (void) state1; if (UNEXPECTED(argc < 2)) { zend_argument_count_error("Too few arguments to function %s::{closure}(), %u passed and exactly 2 expected", ZSTR_VAL(pt_ce_called_on_type_unresolved_method_prototype_reflection->name), argc); return; @@ -197,9 +227,11 @@ class CalledOnTypeUnresolvedMethodPrototypeReflection bool isGenericStatic; pt_type_instanceof_ce(type, pt_ce_generic_static_type, isGenericStatic); if (isGenericStatic) { - zval *calledOnType = self.slot(slots::calledOnType, "calledOnType"); - if (UNEXPECTED(calledOnType == NULL)) return; - /* $calledOnTypeReflections = $this->calledOnType->getObjectClassReflections(); */ + /* $calledOnType = $this->getCalledOnType($isParameterType); */ + zv::Val calledOnTypeVal = self.calledOnTypeFor(state1); + if (UNEXPECTED(calledOnTypeVal.isUndef())) return; + zval *calledOnType = calledOnTypeVal.raw(); + /* $calledOnTypeReflections = $calledOnType->getObjectClassReflections(); */ zv::Val reflections = pt_type_call(Z_OBJ_P(calledOnType), PT_LC("getobjectclassreflections"), 0, NULL); if (UNEXPECTED(reflections.isUndef())) return; if (UNEXPECTED(!zv::Ref(reflections.raw()).isArray())) { @@ -233,9 +265,9 @@ class CalledOnTypeUnresolvedMethodPrototypeReflection bool isStatic; pt_type_instanceof_ce(type, pt_ce_static_type, isStatic); if (isStatic) { - zval *calledOnType = self.slot(slots::calledOnType, "calledOnType"); - if (UNEXPECTED(calledOnType == NULL)) return; - ZVAL_COPY(return_value, calledOnType); + zv::Val calledOnType = self.calledOnTypeFor(state1); + if (UNEXPECTED(calledOnType.isUndef())) return; + calledOnType.intoReturnValue(return_value); return; } zv::Val traversed = pt_type_call_callable(&argv[1], 1, type); diff --git a/turbo-ext/src/CalledOnTypeUnresolvedPropertyPrototypeReflection.cpp b/turbo-ext/src/CalledOnTypeUnresolvedPropertyPrototypeReflection.cpp index 3d1e316de07..351845e831f 100644 --- a/turbo-ext/src/CalledOnTypeUnresolvedPropertyPrototypeReflection.cpp +++ b/turbo-ext/src/CalledOnTypeUnresolvedPropertyPrototypeReflection.cpp @@ -89,7 +89,7 @@ class CalledOnTypeUnresolvedPropertyPrototypeReflection zval *propertyReflection = resolvedDeclaringClass != NULL ? slot(slots::propertyReflection, "propertyReflection") : NULL; zval *resolveToBounds = propertyReflection != NULL ? slot(slots::resolveTemplateTypeMapToBounds, "resolveTemplateTypeMapToBounds") : NULL; if (UNEXPECTED(resolveToBounds == NULL)) return zv::Val(); - PrototypeTransformer transformer = { transformStaticType, self }; + PrototypeTransformer transformer = { transformStaticType, transformInputStaticType, self }; zv::Val result = pt_prototype_resolved_property(PT_PROTOTYPE_CALLED_ON_TYPE, transformer, Z_OBJ_P(resolvedDeclaringClass), Z_OBJ_P(propertyReflection), zend_is_true(resolveToBounds)); if (UNEXPECTED(result.isUndef())) return zv::Val(); write(slots::transformedProperty, zv::Val::copyOf(zv::Ref(result.raw()))); @@ -132,17 +132,49 @@ class CalledOnTypeUnresolvedPropertyPrototypeReflection return value; } + /* getFetchedOnType($flag): $this->fetchedOnType, or for the types values + * are checked against TemplateTypeHelper::removeFinalByKeywordOverrides($this->fetchedOnType); + * UNDEF = pending exception */ + zv::Val fetchedOnTypeFor(zval *flag) const + { + zval *fetchedOnType = slot(slots::fetchedOnType, "fetchedOnType"); + if (UNEXPECTED(fetchedOnType == NULL)) return zv::Val(); + if (flag == NULL || Z_TYPE_P(flag) != IS_TRUE) return zv::Val::copyOf(zv::Ref(fetchedOnType)); + zv::Val stripped = pt_type_call_static_ce(pt_ce_template_type_helper, PT_LC("removefinalbykeywordoverrides"), 1, fetchedOnType); + if (UNEXPECTED(stripped.isUndef())) return zv::Val(); + if (UNEXPECTED(!zv::Ref(stripped.raw()).isObject())) { + zend_type_error("phpstan_turbo: TemplateTypeHelper::removeFinalByKeywordOverrides() must return %s", ptcls::type); + return zv::Val(); + } + return stripped; + } + /* transformStaticType($type): TypeTraverser::map($type, the closure * below over $this); UNDEF = pending exception */ static zv::Val transformStaticType(void *context, zval *type) + { + return transformStaticTypeOf(context, type, false); + } + + /* transformStaticType($type, true) — for the writable type */ + static zv::Val transformInputStaticType(void *context, zval *type) + { + return transformStaticTypeOf(context, type, true); + } + + static zv::Val transformStaticTypeOf(void *context, zval *type, bool isWritableType) { zval thisZv; ZVAL_OBJ(&thisZv, (zend_object *) context); - zv::Val callback = pt_type_native_callback(traverse, &thisZv, NULL); + /* the closure's `use ($isWritableType)` */ + zval flag; + ZVAL_BOOL(&flag, isWritableType); + zv::Val callback = pt_type_native_callback(traverse, &thisZv, &flag); if (UNEXPECTED(callback.isUndef())) return zv::Val(); return pt_type_traverser_map_of(type, callback.raw()); } + /* the closure of transformStaticType(): `function (Type $type, callable * $traverse): Type` — a GenericStaticType becomes the static object * type of its base class changed to the single fetched-on class @@ -150,7 +182,6 @@ class CalledOnTypeUnresolvedPropertyPrototypeReflection * the fetched-on type; everything else traverses */ static void traverse(zval *thisZv, zval *state1, uint32_t argc, zval *argv, zval *return_value) { - (void) state1; if (UNEXPECTED(argc < 2)) { zend_argument_count_error("Too few arguments to function %s::{closure}(), %u passed and exactly 2 expected", ZSTR_VAL(pt_ce_called_on_type_unresolved_property_prototype_reflection->name), argc); return; @@ -163,9 +194,11 @@ class CalledOnTypeUnresolvedPropertyPrototypeReflection bool isGenericStatic; pt_type_instanceof_ce(type, pt_ce_generic_static_type, isGenericStatic); if (isGenericStatic) { - zval *fetchedOnType = CalledOnTypeUnresolvedPropertyPrototypeReflection(Z_OBJ_P(thisZv)).slot(slots::fetchedOnType, "fetchedOnType"); - if (UNEXPECTED(fetchedOnType == NULL)) return; - /* $fetchedOnTypeReflections = $this->fetchedOnType->getObjectClassReflections(); */ + /* $fetchedOnType = $this->getFetchedOnType($isWritableType); */ + zv::Val fetchedOnTypeVal = CalledOnTypeUnresolvedPropertyPrototypeReflection(Z_OBJ_P(thisZv)).fetchedOnTypeFor(state1); + if (UNEXPECTED(fetchedOnTypeVal.isUndef())) return; + zval *fetchedOnType = fetchedOnTypeVal.raw(); + /* $fetchedOnTypeReflections = $fetchedOnType->getObjectClassReflections(); */ zv::Val reflections = pt_type_call(Z_OBJ_P(fetchedOnType), PT_LC("getobjectclassreflections"), 0, NULL); if (UNEXPECTED(reflections.isUndef())) return; if (UNEXPECTED(!zv::Ref(reflections.raw()).isArray())) { @@ -199,9 +232,9 @@ class CalledOnTypeUnresolvedPropertyPrototypeReflection bool isStatic; pt_type_instanceof_ce(type, pt_ce_static_type, isStatic); if (isStatic) { - zval *fetchedOnType = CalledOnTypeUnresolvedPropertyPrototypeReflection(Z_OBJ_P(thisZv)).slot(slots::fetchedOnType, "fetchedOnType"); - if (UNEXPECTED(fetchedOnType == NULL)) return; - ZVAL_COPY(return_value, fetchedOnType); + zv::Val fetchedOnType = CalledOnTypeUnresolvedPropertyPrototypeReflection(Z_OBJ_P(thisZv)).fetchedOnTypeFor(state1); + if (UNEXPECTED(fetchedOnType.isUndef())) return; + fetchedOnType.intoReturnValue(return_value); return; } zv::Val traversed = pt_type_call_callable(&argv[1], 1, type); diff --git a/turbo-ext/src/TypeTraits.cpp b/turbo-ext/src/TypeTraits.cpp index e98a5b08685..049f93f293b 100644 --- a/turbo-ext/src/TypeTraits.cpp +++ b/turbo-ext/src/TypeTraits.cpp @@ -5146,14 +5146,14 @@ zv::Val protoTransformNullable(const PrototypeTransformer &transformer, zval *ty /* the Callback twins' `$original->equals($type) ? $transformedOriginal : * $this->transformStaticType($type)`; the CalledOnType twins transform * every type */ -zv::Val protoTransformUnlessEqual(PrototypeKind kind, const PrototypeTransformer &transformer, zval *original, zval *transformedOriginal, zval *type) +zv::Val protoTransformUnlessEqual(PrototypeKind kind, const PrototypeTransformer &transformer, zval *original, zval *transformedOriginal, zval *type, bool isInput = false) { if (kind == phpstanturbo::PT_PROTOTYPE_CALLBACK) { int equal = protoEquals(original, type); if (UNEXPECTED(equal < 0)) return zv::Val(); if (equal == 1) return zv::Val::copyOf(zv::Ref(transformedOriginal)); } - return transformer.transform(type); + return isInput ? transformer.transformInput(type) : transformer.transform(type); } /* array_map($fn, $array) over an array of objects, the keys kept as @@ -5190,7 +5190,7 @@ zv::Val protoParameter(PrototypeKind kind, const PrototypeTransformer &transform if (UNEXPECTED(name.isUndef())) return zv::Val(); zv::Val originalType = protoParameterCall(parameter, PT_PR_GET_TYPE, "gettype", PROTO_OBJECT); if (UNEXPECTED(originalType.isUndef())) return zv::Val(); - zv::Val transformedType = transformer.transform(originalType.raw()); + zv::Val transformedType = transformer.transformInput(originalType.raw()); if (UNEXPECTED(transformedType.isUndef())) return zv::Val(); zv::Val optional = protoParameterCall(parameter, PT_PR_IS_OPTIONAL, "isoptional", PROTO_ANY); if (UNEXPECTED(optional.isUndef())) return zv::Val(); @@ -5204,7 +5204,7 @@ zv::Val protoParameter(PrototypeKind kind, const PrototypeTransformer &transform if (UNEXPECTED(nativeType.isUndef())) return zv::Val(); zv::Val phpDocType = protoParameterCall(parameter, PT_PR_GET_PHPDOC_TYPE, "getphpdoctype", PROTO_OBJECT); if (UNEXPECTED(phpDocType.isUndef())) return zv::Val(); - zv::Val transformedPhpDocType = protoTransformUnlessEqual(kind, transformer, originalType.raw(), transformedType.raw(), phpDocType.raw()); + zv::Val transformedPhpDocType = protoTransformUnlessEqual(kind, transformer, originalType.raw(), transformedType.raw(), phpDocType.raw(), true); if (UNEXPECTED(transformedPhpDocType.isUndef())) return zv::Val(); zv::Val outType = protoParameterCall(parameter, PT_PR_GET_OUT_TYPE, "getouttype", PROTO_OBJECT_OR_NULL); if (UNEXPECTED(outType.isUndef())) return zv::Val(); @@ -5356,8 +5356,9 @@ zv::Val protoTransformProperty(PrototypeKind kind, const PrototypeTransformer &t if (UNEXPECTED(transformedReadableType.isUndef())) return zv::Val(); zv::Val writableType = protoPropertyCall(property, PT_PROP_GET_WRITABLE_TYPE, "getwritabletype", PROTO_OBJECT); if (UNEXPECTED(writableType.isUndef())) return zv::Val(); - /* the Callback twin: $readableType->equals($writableType) ? $transformedReadableType : transform($writableType) */ - zv::Val transformedWritableType = protoTransformUnlessEqual(kind, transformer, readableType.raw(), transformedReadableType.raw(), writableType.raw()); + /* the Callback twin: $readableType->equals($writableType) ? $transformedReadableType : transform($writableType); + * the CalledOnType twin: transformStaticType($writableType, true) */ + zv::Val transformedWritableType = protoTransformUnlessEqual(kind, transformer, readableType.raw(), transformedReadableType.raw(), writableType.raw(), true); if (UNEXPECTED(transformedWritableType.isUndef())) return zv::Val(); zv::Val phpDocType = protoPropertyCall(property, PT_PROP_GET_PHP_DOC_TYPE, "getphpdoctype", PROTO_OBJECT); if (UNEXPECTED(phpDocType.isUndef())) return zv::Val(); diff --git a/turbo-ext/src/TypeTraits.h b/turbo-ext/src/TypeTraits.h index 183c84abc1e..696f8c25b91 100644 --- a/turbo-ext/src/TypeTraits.h +++ b/turbo-ext/src/TypeTraits.h @@ -1459,12 +1459,17 @@ zv::Val pt_accepts_result_extreme_identity_spread(HashTable *args); namespace phpstanturbo { /* a prototype's transformStaticType($type): fn over its context (the - * prototype object, whose slots the body reads); UNDEF = pending exception */ + * prototype object, whose slots the body reads); inputFn transforms the + * types values are checked against (parameter types, a property's writable + * type) — the CalledOnType twins' transformStaticType($type, true), the + * same fn for the Callback twins; UNDEF = pending exception */ struct PrototypeTransformer { zv::Val (*fn)(void *context, zval *type); + zv::Val (*inputFn)(void *context, zval *type); void *context; zv::Val transform(zval *type) const { return fn(context, type); } + zv::Val transformInput(zval *type) const { return inputFn(context, type); } }; /* whose body: the CalledOnType twins transform every type, the Callback From 8063599cea4e4769cbca199525a0d329f81c831e Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 4 Nov 2026 12:52:23 +0000 Subject: [PATCH 2/3] Ignore the `new Foo()` exactness when comparing covariant and contravariant type arguments The "exactly this class" flavour cannot be written in a PHPDoc type argument, and the invariant branch of isValidVariance() already ignores it through equals(). Stripping it from both sides in the covariant and contravariant branches covers every way the flavour reaches a type argument - `static` resolved in a parameter type, `static` resolved in a return type passed on to another parameter, get_class() narrowing - in one place. Co-Authored-By: Claude Opus 5.5 --- src/Type/Generic/TemplateTypeVariance.php | 11 +- .../PHPStan/Rules/Methods/data/bug-15235.php | 167 ++++++++++++++++-- .../TypesAssignedToPropertiesRuleTest.php | 2 +- .../Rules/Properties/data/bug-15235.php | 30 +++- turbo-ext/src/TemplateTypeHelper.cpp | 5 + turbo-ext/src/TemplateTypeVariance.cpp | 14 +- turbo-ext/src/TypeTraits.h | 5 +- 7 files changed, 205 insertions(+), 29 deletions(-) diff --git a/src/Type/Generic/TemplateTypeVariance.php b/src/Type/Generic/TemplateTypeVariance.php index f0e478df7b9..55dfa3f4d43 100644 --- a/src/Type/Generic/TemplateTypeVariance.php +++ b/src/Type/Generic/TemplateTypeVariance.php @@ -233,12 +233,13 @@ public function isValidVariance(TemplateType $templateType, Type $a, Type $b, bo return new IsSuperTypeOfResult(TrinaryLogic::createFromBoolean($result), $reasons); } - if ($this->covariant()) { - return $a->isSuperTypeOf($b); - } + if ($this->covariant() || $this->contravariant()) { + // The "exactly this class" flavour of a `new Foo()` value cannot be written + // in a PHPDoc type argument - ignore it like equals() does for invariance. + $a = TemplateTypeHelper::removeFinalByKeywordOverrides($a); + $b = TemplateTypeHelper::removeFinalByKeywordOverrides($b); - if ($this->contravariant()) { - return $b->isSuperTypeOf($a); + return $this->covariant() ? $a->isSuperTypeOf($b) : $b->isSuperTypeOf($a); } if ($this->bivariant()) { diff --git a/tests/PHPStan/Rules/Methods/data/bug-15235.php b/tests/PHPStan/Rules/Methods/data/bug-15235.php index 39591a0b981..3d024e8c136 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-15235.php +++ b/tests/PHPStan/Rules/Methods/data/bug-15235.php @@ -1,5 +1,7 @@ = 8.1 +declare(strict_types = 1); + namespace Bug15235; /** @@ -13,29 +15,168 @@ class A public function __construct(public object $object) {} } -class B { - /** - * @param A $a - */ - public function method(A $a): void {} +class B +{ + /** + * @param A $a + */ + public function method(A $a): void {} /** * @param A $a */ public static function staticMethod(A $a): void {} + + /** + * @param A $a + */ + public function invariant(A $a): void {} + + /** + * @param A $a + */ + public function contravariant(A $a): void {} +} + +class D +{ + public function inconsistent(B $b): void + { + $b->method(new A($b)); + } +} + +function fromNew(): void +{ + $b = new B(); + $b->method(new A($b)); + $b->invariant(new A($b)); + $b->contravariant(new A($b)); + $b::staticMethod(new A($b)); + + $callable = $b->method(...); + $callable(new A($b)); +} + +function fromGetClass(B $b): void +{ + if (get_class($b) !== B::class) { + return; + } + + $b->method(new A($b)); } -class D { - public function inconsistent(B $b): void { - $b->method(new A($b)); // (!) No error. +/** @template-covariant T of object */ +class Covariant +{ + /** @param T $object */ + public function __construct(public object $object) {} +} + +/** @template-contravariant T of object */ +class Contravariant +{ + /** @param T $object */ + public function set(object $object): void {} +} + +class E +{ + /** @param Covariant $c */ + public function declaredCovariant(Covariant $c): void {} + + /** @param Contravariant $c */ + public function declaredContravariant(Contravariant $c): void {} + + /** @param \Traversable $it */ + public function traversable(\Traversable $it): void {} + + /** @return Covariant */ + public function makeCovariant(): Covariant + { + return new Covariant($this); + } + + /** @return Wrapper */ + public function makeWrapper(): Wrapper + { + return new Wrapper($this); } } -$b = new B(); +class Consumer +{ + /** @param Covariant $c */ + public function takesCovariant(Covariant $c): void {} + + /** @param Wrapper $wrapper */ + public function takesContravariantWrapper(Wrapper $wrapper): void {} + + /** @param Wrapper> $wrapper */ + public function takesNested(Wrapper $wrapper): void {} +} + +/** + * @param Covariant $covariant + * @param Contravariant $contravariant + * @param \Traversable $traversable + */ +function declaredVariances(Covariant $covariant, Contravariant $contravariant, \Traversable $traversable): void +{ + $e = new E(); + $e->declaredCovariant($covariant); + $e->declaredContravariant($contravariant); + $e->traversable($traversable); +} + +function flavourOnTheArgumentSide(Consumer $consumer): void +{ + $e = new E(); + $consumer->takesCovariant($e->makeCovariant()); + $consumer->takesContravariantWrapper($e->makeWrapper()); +} + +/** @template T of object */ +class Wrapper +{ + /** @param T $object */ + public function __construct(public object $object) {} +} + +function nestedInGenericType(Consumer $consumer): void +{ + $e = new E(); + $consumer->takesNested(new Wrapper($e->makeCovariant())); +} + +class Composite +{ + /** @param Covariant|null $c */ + public function inUnion(?Covariant $c): void {} + + /** @param array> $c */ + public function inArray(array $c): void {} -$b->method(new A($b)); // (!) Parameter #1 $a of method B::method() expects A, A given. + /** @param Covariant> $c */ + public function nested(Covariant $c): void {} -$b::staticMethod(new A($b)); + /** @param Wrapper> $c */ + public function nestedInvariant(Wrapper $c): void {} +} -$callable = $b->method(...); -$callable(new A($b)); +/** + * @param Covariant $covariant + * @param array> $array + * @param Covariant> $nested + * @param Wrapper> $nestedInvariant + */ +function compositeShapes(Covariant $covariant, array $array, Covariant $nested, Wrapper $nestedInvariant): void +{ + $composite = new Composite(); + $composite->inUnion($covariant); + $composite->inUnion(null); + $composite->inArray($array); + $composite->nested($nested); + $composite->nestedInvariant($nestedInvariant); +} diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index 26a204eb64b..774ab8e5f26 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -1101,7 +1101,7 @@ public function testBug15166(): void $this->analyse([__DIR__ . '/data/bug-15166.php'], []); } - #[RequiresPhp('>= 8.0.0')] + #[RequiresPhp('>= 8.1.0')] public function testBug15235(): void { $this->analyse([__DIR__ . '/data/bug-15235.php'], []); diff --git a/tests/PHPStan/Rules/Properties/data/bug-15235.php b/tests/PHPStan/Rules/Properties/data/bug-15235.php index af032b423fc..612d23cb87f 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-15235.php +++ b/tests/PHPStan/Rules/Properties/data/bug-15235.php @@ -1,4 +1,6 @@ -= 8.0 += 8.1 + +declare(strict_types = 1); namespace Bug15235Properties; @@ -13,13 +15,31 @@ class A public function __construct(public object $object) {} } +/** @template-covariant T of object */ +class Covariant +{ + /** @param T $object */ + public function __construct(public object $object) {} +} + class B { /** @var A|null */ - public $a = null; + public ?A $callSiteVariance = null; + + /** @var Covariant|null */ + public ?Covariant $declaredVariance = null; } -function (): void { +function fromParameter(B $b): void +{ + $b->callSiteVariance = new A($b); + $b->declaredVariance = new Covariant($b); +} + +function fromNew(): void +{ $b = new B(); - $b->a = new A($b); -}; + $b->callSiteVariance = new A($b); + $b->declaredVariance = new Covariant($b); +} diff --git a/turbo-ext/src/TemplateTypeHelper.cpp b/turbo-ext/src/TemplateTypeHelper.cpp index 94f40d42b31..2cf5e45c1a1 100644 --- a/turbo-ext/src/TemplateTypeHelper.cpp +++ b/turbo-ext/src/TemplateTypeHelper.cpp @@ -558,6 +558,11 @@ zv::Val pt_type_template_type_helper_to_argument(zval *type) return TemplateTypeHelper::toArgument(type); } +zv::Val pt_type_template_type_helper_remove_final_by_keyword_overrides(zval *type) +{ + return TemplateTypeHelper::removeFinalByKeywordOverrides(type); +} + /* }}} */ /* {{{ engine ABI glue: parameter parsing + registration */ diff --git a/turbo-ext/src/TemplateTypeVariance.cpp b/turbo-ext/src/TemplateTypeVariance.cpp index 3029efe8c59..73436893d96 100644 --- a/turbo-ext/src/TemplateTypeVariance.cpp +++ b/turbo-ext/src/TemplateTypeVariance.cpp @@ -436,9 +436,17 @@ zv::Val TemplateTypeVariance::isValidVariance(zval *templateType, zval *a, zval return pt_type_new_ce(pt_ce_is_super_type_of_result, 2, args); } - if (thisValue == COVARIANT) return pt_type_op(Z_OBJ_P(a), PT_OP_IS_SUPER_TYPE_OF, 1, b); - - if (thisValue == CONTRAVARIANT) return pt_type_op(Z_OBJ_P(b), PT_OP_IS_SUPER_TYPE_OF, 1, a); + if (thisValue == COVARIANT || thisValue == CONTRAVARIANT) { + /* the "exactly this class" flavour of a `new Foo()` value cannot be + * written in a PHPDoc type argument - ignore it like equals() does + * for invariance */ + zv::Val strippedA = pt_type_template_type_helper_remove_final_by_keyword_overrides(a); + if (UNEXPECTED(strippedA.isUndef())) return zv::Val(); + zv::Val strippedB = pt_type_template_type_helper_remove_final_by_keyword_overrides(b); + if (UNEXPECTED(strippedB.isUndef())) return zv::Val(); + if (thisValue == COVARIANT) return pt_type_op(Z_OBJ_P(strippedA.raw()), PT_OP_IS_SUPER_TYPE_OF, 1, strippedB.raw()); + return pt_type_op(Z_OBJ_P(strippedB.raw()), PT_OP_IS_SUPER_TYPE_OF, 1, strippedA.raw()); + } if (thisValue == BIVARIANT) return pt_type_is_super_type_of_result(PT_TRI_YES); diff --git a/turbo-ext/src/TypeTraits.h b/turbo-ext/src/TypeTraits.h index 696f8c25b91..e0d54f93d20 100644 --- a/turbo-ext/src/TypeTraits.h +++ b/turbo-ext/src/TypeTraits.h @@ -946,12 +946,13 @@ zv::Val pt_type_template_type_variance_map_new(zval *variances); zv::Val pt_type_template_type_map_empty(); zv::Val pt_type_template_type_map_new(zval *types, zval *lowerBoundTypes = NULL); /* TemplateTypeHelper::resolveTemplateTypes() / resolveToDefaults() / - * resolveToBounds() / toArgument() (the arguments borrowed); UNDEF = - * pending exception */ + * resolveToBounds() / toArgument() / removeFinalByKeywordOverrides() (the + * arguments borrowed); UNDEF = pending exception */ zv::Val pt_type_template_type_helper_resolve_template_types(zval *type, zval *standins, zval *callSiteVariances, zval *positionVariance, bool keepErrorTypes); zv::Val pt_type_template_type_helper_resolve_to_defaults(zval *type); zv::Val pt_type_template_type_helper_resolve_to_bounds(zval *type); zv::Val pt_type_template_type_helper_to_argument(zval *type); +zv::Val pt_type_template_type_helper_remove_final_by_keyword_overrides(zval *type); /* $reference->getType() / ->getPositionVariance() of a reference instance * (TemplateTypeReference.cpp): the slots of a native instance, the methods * of anything else (the PHP twin declared next to the native class in the From 75046c18ee58b5a23914afbb121ee02bb5cd0b84 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 4 Nov 2026 12:52:23 +0000 Subject: [PATCH 3/3] Revert resolving `static` in input-side types without the `new Foo()` exactness The variance comparison now ignores the flavour, so the prototype reflections (and their turbo-ext mirrors) no longer need a separate input-side transform. Co-Authored-By: Claude Opus 5.5 --- ...ypeUnresolvedMethodPrototypeReflection.php | 29 +++-------- ...eUnresolvedPropertyPrototypeReflection.php | 27 +++------- ...ackUnresolvedMethodPrototypeReflection.cpp | 2 +- ...kUnresolvedPropertyPrototypeReflection.cpp | 2 +- ...ypeUnresolvedMethodPrototypeReflection.cpp | 50 ++++-------------- ...eUnresolvedPropertyPrototypeReflection.cpp | 51 ++++--------------- turbo-ext/src/TypeTraits.cpp | 13 +++-- turbo-ext/src/TypeTraits.h | 7 +-- 8 files changed, 40 insertions(+), 141 deletions(-) diff --git a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php index e7c2d01e805..ba61cd9c1fc 100644 --- a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php @@ -12,7 +12,6 @@ use PHPStan\Reflection\ResolvedMethodReflection; use PHPStan\Turbo\ShadowedByTurboExtension; use PHPStan\Type\Generic\GenericStaticType; -use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\StaticType; use PHPStan\Type\ThisType; use PHPStan\Type\Type; @@ -97,13 +96,13 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass, array_map( fn (ExtendedParameterReflection $parameter): ExtendedParameterReflection => new ExtendedDummyParameter( $parameter->getName(), - $this->transformStaticType($parameter->getType(), true), + $this->transformStaticType($parameter->getType()), $parameter->isOptional(), $parameter->passedByReference(), $parameter->isVariadic(), $parameter->getDefaultValue(), $parameter->getNativeType(), - $this->transformStaticType($parameter->getPhpDocType(), true), + $this->transformStaticType($parameter->getPhpDocType()), $parameter->getOutType() !== null ? $this->transformStaticType($parameter->getOutType()) : null, $parameter->isImmediatelyInvokedCallable(), $parameter->getClosureThisType() !== null ? $this->transformStaticType($parameter->getClosureThisType()) : null, @@ -142,39 +141,25 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass, ); } - /** - * A parameter type is compared against argument types, whose template arguments never carry - * the "exactly this class" flavour of `new Foo()` - so `static` resolved inside it must not either. - */ - private function transformStaticType(Type $type, bool $isParameterType = false): Type + private function transformStaticType(Type $type): Type { - return TypeTraverser::mapMemoized($type, function (Type $type, callable $traverse) use ($isParameterType): Type { + return TypeTraverser::mapMemoized($type, function (Type $type, callable $traverse): Type { if ($type instanceof GenericStaticType) { - $calledOnType = $this->getCalledOnType($isParameterType); - $calledOnTypeReflections = $calledOnType->getObjectClassReflections(); + $calledOnTypeReflections = $this->calledOnType->getObjectClassReflections(); if (count($calledOnTypeReflections) === 1) { $calledOnTypeReflection = $calledOnTypeReflections[0]; return $traverse($type->changeBaseClass($calledOnTypeReflection)->getStaticObjectType()); } - return $calledOnType; + return $this->calledOnType; } if ($type instanceof StaticType) { - return $this->getCalledOnType($isParameterType); + return $this->calledOnType; } return $traverse($type); }); } - private function getCalledOnType(bool $isParameterType): Type - { - if ($isParameterType) { - return TemplateTypeHelper::removeFinalByKeywordOverrides($this->calledOnType); - } - - return $this->calledOnType; - } - } diff --git a/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php b/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php index 22aa71097cb..eda211fb11e 100644 --- a/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php +++ b/src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php @@ -8,7 +8,6 @@ use PHPStan\Reflection\ResolvedPropertyReflection; use PHPStan\Turbo\ShadowedByTurboExtension; use PHPStan\Type\Generic\GenericStaticType; -use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\StaticType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; @@ -78,44 +77,30 @@ public function withFechedOnType(Type $type): UnresolvedPropertyPrototypeReflect private function transformPropertyWithStaticType(ClassReflection $declaringClass, ExtendedPropertyReflection $property): ExtendedPropertyReflection { $readableType = $this->transformStaticType($property->getReadableType()); - $writableType = $this->transformStaticType($property->getWritableType(), true); + $writableType = $this->transformStaticType($property->getWritableType()); $phpDocType = $this->transformStaticType($property->getPhpDocType()); $nativeType = $this->transformStaticType($property->getNativeType()); return new ChangedTypePropertyReflection($declaringClass, $property, $readableType, $writableType, $phpDocType, $nativeType); } - /** - * A writable type is compared against assigned types, whose template arguments never carry - * the "exactly this class" flavour of `new Foo()` - so `static` resolved inside it must not either. - */ - private function transformStaticType(Type $type, bool $isWritableType = false): Type + private function transformStaticType(Type $type): Type { - return TypeTraverser::map($type, function (Type $type, callable $traverse) use ($isWritableType): Type { + return TypeTraverser::map($type, function (Type $type, callable $traverse): Type { if ($type instanceof GenericStaticType) { - $fetchedOnType = $this->getFetchedOnType($isWritableType); - $fetchedOnTypeReflections = $fetchedOnType->getObjectClassReflections(); + $fetchedOnTypeReflections = $this->fetchedOnType->getObjectClassReflections(); if (count($fetchedOnTypeReflections) === 1) { return $traverse($type->changeBaseClass($fetchedOnTypeReflections[0])->getStaticObjectType()); } - return $fetchedOnType; + return $this->fetchedOnType; } if ($type instanceof StaticType) { - return $this->getFetchedOnType($isWritableType); + return $this->fetchedOnType; } return $traverse($type); }); } - private function getFetchedOnType(bool $isWritableType): Type - { - if ($isWritableType) { - return TemplateTypeHelper::removeFinalByKeywordOverrides($this->fetchedOnType); - } - - return $this->fetchedOnType; - } - } diff --git a/turbo-ext/src/CallbackUnresolvedMethodPrototypeReflection.cpp b/turbo-ext/src/CallbackUnresolvedMethodPrototypeReflection.cpp index be30f84b0b7..8e595528a65 100644 --- a/turbo-ext/src/CallbackUnresolvedMethodPrototypeReflection.cpp +++ b/turbo-ext/src/CallbackUnresolvedMethodPrototypeReflection.cpp @@ -91,7 +91,7 @@ class CallbackUnresolvedMethodPrototypeReflection zval *resolveToBounds = methodReflection != NULL ? slot(slots::resolveTemplateTypeMapToBounds, "resolveTemplateTypeMapToBounds") : NULL; if (UNEXPECTED(resolveToBounds == NULL)) return zv::Val(); /* $method->getAsserts()->mapTypes($this->transformStaticTypeCallback) */ - PrototypeTransformer transformer = { transformStaticType, transformStaticType, self }; + PrototypeTransformer transformer = { transformStaticType, self }; zv::Val result = pt_prototype_resolved_method(PT_PROTOTYPE_CALLBACK, transformer, Z_OBJ_P(resolvedDeclaringClass), Z_OBJ_P(methodReflection), zend_is_true(resolveToBounds), OBJ_PROP_NUM(self, slots::transformStaticTypeCallback)); if (UNEXPECTED(result.isUndef())) return zv::Val(); write(slots::transformedMethod, zv::Val::copyOf(zv::Ref(result.raw()))); diff --git a/turbo-ext/src/CallbackUnresolvedPropertyPrototypeReflection.cpp b/turbo-ext/src/CallbackUnresolvedPropertyPrototypeReflection.cpp index c8ede4a32af..1256a2aa6a8 100644 --- a/turbo-ext/src/CallbackUnresolvedPropertyPrototypeReflection.cpp +++ b/turbo-ext/src/CallbackUnresolvedPropertyPrototypeReflection.cpp @@ -88,7 +88,7 @@ class CallbackUnresolvedPropertyPrototypeReflection zval *propertyReflection = resolvedDeclaringClass != NULL ? slot(slots::propertyReflection, "propertyReflection") : NULL; zval *resolveToBounds = propertyReflection != NULL ? slot(slots::resolveTemplateTypeMapToBounds, "resolveTemplateTypeMapToBounds") : NULL; if (UNEXPECTED(resolveToBounds == NULL)) return zv::Val(); - PrototypeTransformer transformer = { transformStaticType, transformStaticType, self }; + PrototypeTransformer transformer = { transformStaticType, self }; zv::Val result = pt_prototype_resolved_property(PT_PROTOTYPE_CALLBACK, transformer, Z_OBJ_P(resolvedDeclaringClass), Z_OBJ_P(propertyReflection), zend_is_true(resolveToBounds)); if (UNEXPECTED(result.isUndef())) return zv::Val(); write(slots::transformedProperty, zv::Val::copyOf(zv::Ref(result.raw()))); diff --git a/turbo-ext/src/CalledOnTypeUnresolvedMethodPrototypeReflection.cpp b/turbo-ext/src/CalledOnTypeUnresolvedMethodPrototypeReflection.cpp index d220b76e52c..0a1465aa224 100644 --- a/turbo-ext/src/CalledOnTypeUnresolvedMethodPrototypeReflection.cpp +++ b/turbo-ext/src/CalledOnTypeUnresolvedMethodPrototypeReflection.cpp @@ -96,7 +96,7 @@ class CalledOnTypeUnresolvedMethodPrototypeReflection ZVAL_OBJ(&thisZv, self); zv::Val assertsCallback = pt_type_native_callback(transformStaticTypeCallback, &thisZv, NULL); if (UNEXPECTED(assertsCallback.isUndef())) return zv::Val(); - PrototypeTransformer transformer = { transformStaticType, transformInputStaticType, self }; + PrototypeTransformer transformer = { transformStaticType, self }; zv::Val result = pt_prototype_resolved_method(PT_PROTOTYPE_CALLED_ON_TYPE, transformer, Z_OBJ_P(resolvedDeclaringClass), Z_OBJ_P(methodReflection), zend_is_true(resolveToBounds), assertsCallback.raw()); if (UNEXPECTED(result.isUndef())) return zv::Val(); write(slots::transformedMethod, zv::Val::copyOf(zv::Ref(result.raw()))); @@ -143,37 +143,9 @@ class CalledOnTypeUnresolvedMethodPrototypeReflection return value; } - /* getCalledOnType($flag): $this->calledOnType, or for the types values - * are checked against TemplateTypeHelper::removeFinalByKeywordOverrides($this->calledOnType); - * UNDEF = pending exception */ - zv::Val calledOnTypeFor(zval *flag) const - { - zval *calledOnType = slot(slots::calledOnType, "calledOnType"); - if (UNEXPECTED(calledOnType == NULL)) return zv::Val(); - if (flag == NULL || Z_TYPE_P(flag) != IS_TRUE) return zv::Val::copyOf(zv::Ref(calledOnType)); - zv::Val stripped = pt_type_call_static_ce(pt_ce_template_type_helper, PT_LC("removefinalbykeywordoverrides"), 1, calledOnType); - if (UNEXPECTED(stripped.isUndef())) return zv::Val(); - if (UNEXPECTED(!zv::Ref(stripped.raw()).isObject())) { - zend_type_error("phpstan_turbo: TemplateTypeHelper::removeFinalByKeywordOverrides() must return %s", ptcls::type); - return zv::Val(); - } - return stripped; - } - /* transformStaticType($type): TypeTraverser::mapMemoized($type, the * closure below over $this); UNDEF = pending exception */ static zv::Val transformStaticType(void *context, zval *type) - { - return transformStaticTypeOf(context, type, false); - } - - /* transformStaticType($type, true) — for parameter types */ - static zv::Val transformInputStaticType(void *context, zval *type) - { - return transformStaticTypeOf(context, type, true); - } - - static zv::Val transformStaticTypeOf(void *context, zval *type, bool isParameterType) { /* the callback changes StaticTypes only: a type without one maps to * itself */ @@ -182,10 +154,7 @@ class CalledOnTypeUnresolvedMethodPrototypeReflection if (!containsStatic) return zv::Val::copyOf(zv::Ref(type)); zval thisZv; ZVAL_OBJ(&thisZv, (zend_object *) context); - /* the closure's `use ($isParameterType)` */ - zval flag; - ZVAL_BOOL(&flag, isParameterType); - zv::Val callback = pt_type_native_callback(traverse, &thisZv, &flag); + zv::Val callback = pt_type_native_callback(traverse, &thisZv, NULL); if (UNEXPECTED(callback.isUndef())) return zv::Val(); return pt_type_traverser_map_memoized_of(type, callback.raw()); } @@ -214,6 +183,7 @@ class CalledOnTypeUnresolvedMethodPrototypeReflection * the called-on type; everything else traverses */ static void traverse(zval *thisZv, zval *state1, uint32_t argc, zval *argv, zval *return_value) { + (void) state1; if (UNEXPECTED(argc < 2)) { zend_argument_count_error("Too few arguments to function %s::{closure}(), %u passed and exactly 2 expected", ZSTR_VAL(pt_ce_called_on_type_unresolved_method_prototype_reflection->name), argc); return; @@ -227,11 +197,9 @@ class CalledOnTypeUnresolvedMethodPrototypeReflection bool isGenericStatic; pt_type_instanceof_ce(type, pt_ce_generic_static_type, isGenericStatic); if (isGenericStatic) { - /* $calledOnType = $this->getCalledOnType($isParameterType); */ - zv::Val calledOnTypeVal = self.calledOnTypeFor(state1); - if (UNEXPECTED(calledOnTypeVal.isUndef())) return; - zval *calledOnType = calledOnTypeVal.raw(); - /* $calledOnTypeReflections = $calledOnType->getObjectClassReflections(); */ + zval *calledOnType = self.slot(slots::calledOnType, "calledOnType"); + if (UNEXPECTED(calledOnType == NULL)) return; + /* $calledOnTypeReflections = $this->calledOnType->getObjectClassReflections(); */ zv::Val reflections = pt_type_call(Z_OBJ_P(calledOnType), PT_LC("getobjectclassreflections"), 0, NULL); if (UNEXPECTED(reflections.isUndef())) return; if (UNEXPECTED(!zv::Ref(reflections.raw()).isArray())) { @@ -265,9 +233,9 @@ class CalledOnTypeUnresolvedMethodPrototypeReflection bool isStatic; pt_type_instanceof_ce(type, pt_ce_static_type, isStatic); if (isStatic) { - zv::Val calledOnType = self.calledOnTypeFor(state1); - if (UNEXPECTED(calledOnType.isUndef())) return; - calledOnType.intoReturnValue(return_value); + zval *calledOnType = self.slot(slots::calledOnType, "calledOnType"); + if (UNEXPECTED(calledOnType == NULL)) return; + ZVAL_COPY(return_value, calledOnType); return; } zv::Val traversed = pt_type_call_callable(&argv[1], 1, type); diff --git a/turbo-ext/src/CalledOnTypeUnresolvedPropertyPrototypeReflection.cpp b/turbo-ext/src/CalledOnTypeUnresolvedPropertyPrototypeReflection.cpp index 351845e831f..3d1e316de07 100644 --- a/turbo-ext/src/CalledOnTypeUnresolvedPropertyPrototypeReflection.cpp +++ b/turbo-ext/src/CalledOnTypeUnresolvedPropertyPrototypeReflection.cpp @@ -89,7 +89,7 @@ class CalledOnTypeUnresolvedPropertyPrototypeReflection zval *propertyReflection = resolvedDeclaringClass != NULL ? slot(slots::propertyReflection, "propertyReflection") : NULL; zval *resolveToBounds = propertyReflection != NULL ? slot(slots::resolveTemplateTypeMapToBounds, "resolveTemplateTypeMapToBounds") : NULL; if (UNEXPECTED(resolveToBounds == NULL)) return zv::Val(); - PrototypeTransformer transformer = { transformStaticType, transformInputStaticType, self }; + PrototypeTransformer transformer = { transformStaticType, self }; zv::Val result = pt_prototype_resolved_property(PT_PROTOTYPE_CALLED_ON_TYPE, transformer, Z_OBJ_P(resolvedDeclaringClass), Z_OBJ_P(propertyReflection), zend_is_true(resolveToBounds)); if (UNEXPECTED(result.isUndef())) return zv::Val(); write(slots::transformedProperty, zv::Val::copyOf(zv::Ref(result.raw()))); @@ -132,49 +132,17 @@ class CalledOnTypeUnresolvedPropertyPrototypeReflection return value; } - /* getFetchedOnType($flag): $this->fetchedOnType, or for the types values - * are checked against TemplateTypeHelper::removeFinalByKeywordOverrides($this->fetchedOnType); - * UNDEF = pending exception */ - zv::Val fetchedOnTypeFor(zval *flag) const - { - zval *fetchedOnType = slot(slots::fetchedOnType, "fetchedOnType"); - if (UNEXPECTED(fetchedOnType == NULL)) return zv::Val(); - if (flag == NULL || Z_TYPE_P(flag) != IS_TRUE) return zv::Val::copyOf(zv::Ref(fetchedOnType)); - zv::Val stripped = pt_type_call_static_ce(pt_ce_template_type_helper, PT_LC("removefinalbykeywordoverrides"), 1, fetchedOnType); - if (UNEXPECTED(stripped.isUndef())) return zv::Val(); - if (UNEXPECTED(!zv::Ref(stripped.raw()).isObject())) { - zend_type_error("phpstan_turbo: TemplateTypeHelper::removeFinalByKeywordOverrides() must return %s", ptcls::type); - return zv::Val(); - } - return stripped; - } - /* transformStaticType($type): TypeTraverser::map($type, the closure * below over $this); UNDEF = pending exception */ static zv::Val transformStaticType(void *context, zval *type) - { - return transformStaticTypeOf(context, type, false); - } - - /* transformStaticType($type, true) — for the writable type */ - static zv::Val transformInputStaticType(void *context, zval *type) - { - return transformStaticTypeOf(context, type, true); - } - - static zv::Val transformStaticTypeOf(void *context, zval *type, bool isWritableType) { zval thisZv; ZVAL_OBJ(&thisZv, (zend_object *) context); - /* the closure's `use ($isWritableType)` */ - zval flag; - ZVAL_BOOL(&flag, isWritableType); - zv::Val callback = pt_type_native_callback(traverse, &thisZv, &flag); + zv::Val callback = pt_type_native_callback(traverse, &thisZv, NULL); if (UNEXPECTED(callback.isUndef())) return zv::Val(); return pt_type_traverser_map_of(type, callback.raw()); } - /* the closure of transformStaticType(): `function (Type $type, callable * $traverse): Type` — a GenericStaticType becomes the static object * type of its base class changed to the single fetched-on class @@ -182,6 +150,7 @@ class CalledOnTypeUnresolvedPropertyPrototypeReflection * the fetched-on type; everything else traverses */ static void traverse(zval *thisZv, zval *state1, uint32_t argc, zval *argv, zval *return_value) { + (void) state1; if (UNEXPECTED(argc < 2)) { zend_argument_count_error("Too few arguments to function %s::{closure}(), %u passed and exactly 2 expected", ZSTR_VAL(pt_ce_called_on_type_unresolved_property_prototype_reflection->name), argc); return; @@ -194,11 +163,9 @@ class CalledOnTypeUnresolvedPropertyPrototypeReflection bool isGenericStatic; pt_type_instanceof_ce(type, pt_ce_generic_static_type, isGenericStatic); if (isGenericStatic) { - /* $fetchedOnType = $this->getFetchedOnType($isWritableType); */ - zv::Val fetchedOnTypeVal = CalledOnTypeUnresolvedPropertyPrototypeReflection(Z_OBJ_P(thisZv)).fetchedOnTypeFor(state1); - if (UNEXPECTED(fetchedOnTypeVal.isUndef())) return; - zval *fetchedOnType = fetchedOnTypeVal.raw(); - /* $fetchedOnTypeReflections = $fetchedOnType->getObjectClassReflections(); */ + zval *fetchedOnType = CalledOnTypeUnresolvedPropertyPrototypeReflection(Z_OBJ_P(thisZv)).slot(slots::fetchedOnType, "fetchedOnType"); + if (UNEXPECTED(fetchedOnType == NULL)) return; + /* $fetchedOnTypeReflections = $this->fetchedOnType->getObjectClassReflections(); */ zv::Val reflections = pt_type_call(Z_OBJ_P(fetchedOnType), PT_LC("getobjectclassreflections"), 0, NULL); if (UNEXPECTED(reflections.isUndef())) return; if (UNEXPECTED(!zv::Ref(reflections.raw()).isArray())) { @@ -232,9 +199,9 @@ class CalledOnTypeUnresolvedPropertyPrototypeReflection bool isStatic; pt_type_instanceof_ce(type, pt_ce_static_type, isStatic); if (isStatic) { - zv::Val fetchedOnType = CalledOnTypeUnresolvedPropertyPrototypeReflection(Z_OBJ_P(thisZv)).fetchedOnTypeFor(state1); - if (UNEXPECTED(fetchedOnType.isUndef())) return; - fetchedOnType.intoReturnValue(return_value); + zval *fetchedOnType = CalledOnTypeUnresolvedPropertyPrototypeReflection(Z_OBJ_P(thisZv)).slot(slots::fetchedOnType, "fetchedOnType"); + if (UNEXPECTED(fetchedOnType == NULL)) return; + ZVAL_COPY(return_value, fetchedOnType); return; } zv::Val traversed = pt_type_call_callable(&argv[1], 1, type); diff --git a/turbo-ext/src/TypeTraits.cpp b/turbo-ext/src/TypeTraits.cpp index 049f93f293b..e98a5b08685 100644 --- a/turbo-ext/src/TypeTraits.cpp +++ b/turbo-ext/src/TypeTraits.cpp @@ -5146,14 +5146,14 @@ zv::Val protoTransformNullable(const PrototypeTransformer &transformer, zval *ty /* the Callback twins' `$original->equals($type) ? $transformedOriginal : * $this->transformStaticType($type)`; the CalledOnType twins transform * every type */ -zv::Val protoTransformUnlessEqual(PrototypeKind kind, const PrototypeTransformer &transformer, zval *original, zval *transformedOriginal, zval *type, bool isInput = false) +zv::Val protoTransformUnlessEqual(PrototypeKind kind, const PrototypeTransformer &transformer, zval *original, zval *transformedOriginal, zval *type) { if (kind == phpstanturbo::PT_PROTOTYPE_CALLBACK) { int equal = protoEquals(original, type); if (UNEXPECTED(equal < 0)) return zv::Val(); if (equal == 1) return zv::Val::copyOf(zv::Ref(transformedOriginal)); } - return isInput ? transformer.transformInput(type) : transformer.transform(type); + return transformer.transform(type); } /* array_map($fn, $array) over an array of objects, the keys kept as @@ -5190,7 +5190,7 @@ zv::Val protoParameter(PrototypeKind kind, const PrototypeTransformer &transform if (UNEXPECTED(name.isUndef())) return zv::Val(); zv::Val originalType = protoParameterCall(parameter, PT_PR_GET_TYPE, "gettype", PROTO_OBJECT); if (UNEXPECTED(originalType.isUndef())) return zv::Val(); - zv::Val transformedType = transformer.transformInput(originalType.raw()); + zv::Val transformedType = transformer.transform(originalType.raw()); if (UNEXPECTED(transformedType.isUndef())) return zv::Val(); zv::Val optional = protoParameterCall(parameter, PT_PR_IS_OPTIONAL, "isoptional", PROTO_ANY); if (UNEXPECTED(optional.isUndef())) return zv::Val(); @@ -5204,7 +5204,7 @@ zv::Val protoParameter(PrototypeKind kind, const PrototypeTransformer &transform if (UNEXPECTED(nativeType.isUndef())) return zv::Val(); zv::Val phpDocType = protoParameterCall(parameter, PT_PR_GET_PHPDOC_TYPE, "getphpdoctype", PROTO_OBJECT); if (UNEXPECTED(phpDocType.isUndef())) return zv::Val(); - zv::Val transformedPhpDocType = protoTransformUnlessEqual(kind, transformer, originalType.raw(), transformedType.raw(), phpDocType.raw(), true); + zv::Val transformedPhpDocType = protoTransformUnlessEqual(kind, transformer, originalType.raw(), transformedType.raw(), phpDocType.raw()); if (UNEXPECTED(transformedPhpDocType.isUndef())) return zv::Val(); zv::Val outType = protoParameterCall(parameter, PT_PR_GET_OUT_TYPE, "getouttype", PROTO_OBJECT_OR_NULL); if (UNEXPECTED(outType.isUndef())) return zv::Val(); @@ -5356,9 +5356,8 @@ zv::Val protoTransformProperty(PrototypeKind kind, const PrototypeTransformer &t if (UNEXPECTED(transformedReadableType.isUndef())) return zv::Val(); zv::Val writableType = protoPropertyCall(property, PT_PROP_GET_WRITABLE_TYPE, "getwritabletype", PROTO_OBJECT); if (UNEXPECTED(writableType.isUndef())) return zv::Val(); - /* the Callback twin: $readableType->equals($writableType) ? $transformedReadableType : transform($writableType); - * the CalledOnType twin: transformStaticType($writableType, true) */ - zv::Val transformedWritableType = protoTransformUnlessEqual(kind, transformer, readableType.raw(), transformedReadableType.raw(), writableType.raw(), true); + /* the Callback twin: $readableType->equals($writableType) ? $transformedReadableType : transform($writableType) */ + zv::Val transformedWritableType = protoTransformUnlessEqual(kind, transformer, readableType.raw(), transformedReadableType.raw(), writableType.raw()); if (UNEXPECTED(transformedWritableType.isUndef())) return zv::Val(); zv::Val phpDocType = protoPropertyCall(property, PT_PROP_GET_PHP_DOC_TYPE, "getphpdoctype", PROTO_OBJECT); if (UNEXPECTED(phpDocType.isUndef())) return zv::Val(); diff --git a/turbo-ext/src/TypeTraits.h b/turbo-ext/src/TypeTraits.h index e0d54f93d20..faf10c006ce 100644 --- a/turbo-ext/src/TypeTraits.h +++ b/turbo-ext/src/TypeTraits.h @@ -1460,17 +1460,12 @@ zv::Val pt_accepts_result_extreme_identity_spread(HashTable *args); namespace phpstanturbo { /* a prototype's transformStaticType($type): fn over its context (the - * prototype object, whose slots the body reads); inputFn transforms the - * types values are checked against (parameter types, a property's writable - * type) — the CalledOnType twins' transformStaticType($type, true), the - * same fn for the Callback twins; UNDEF = pending exception */ + * prototype object, whose slots the body reads); UNDEF = pending exception */ struct PrototypeTransformer { zv::Val (*fn)(void *context, zval *type); - zv::Val (*inputFn)(void *context, zval *type); void *context; zv::Val transform(zval *type) const { return fn(context, type); } - zv::Val transformInput(zval *type) const { return inputFn(context, type); } }; /* whose body: the CalledOnType twins transform every type, the Callback