From 77e3649a117c12bc2df424dc792542fb494d9d8a Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Sat, 26 Sep 2026 23:11:15 +0200 Subject: [PATCH 1/2] Generalize an inferred template type by what a conditional branch knows about it In the else branch of `(TNewKey is \UnitEnum ? array-key : TNewKey)` with TNewKey of array-key|\UnitEnum, the branch knows TNewKey is an array-key. Since the branches hold NarrowedSubjectType references, the resolver traversed into them and generalized the inferred type against the subject's own bound, which is not scalar, so a literal key such as 'foo' became string. 2.2.14 kept 'foo'. The resolver now resolves the subject inside a NarrowedSubjectType without generalizing it, and generalizes the narrowed result against the reference, whose bound is the narrowed one. A template type in the target stays generalized, as the condition sees it: otherwise `(T is U ? T : int)` narrowed 'foo' & 'bar' to never. With featureToggles.unresolvedTemplateArguments (bleedingEdge) the inferred template arguments are solved after the call instead of generalized here, so the test runs without bleedingEdge: NarrowedSubjectInferredLiteralTest. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../ResolvedFunctionVariantWithOriginal.php | 128 +++++++++++------ .../NarrowedSubjectInferredLiteralTest.php | 36 +++++ .../narrowed-subject-inferred-literal.php | 136 ++++++++++++++++++ .../ResolvedFunctionVariantWithOriginal.cpp | 106 ++++++++++---- .../ResolvedFunctionVariantWithOriginal.h | 58 +++++--- 5 files changed, 368 insertions(+), 96 deletions(-) create mode 100644 tests/PHPStan/Analyser/NarrowedSubjectInferredLiteralTest.php create mode 100644 tests/PHPStan/Analyser/data/narrowed-subject-inferred-literal.php diff --git a/src/Reflection/ResolvedFunctionVariantWithOriginal.php b/src/Reflection/ResolvedFunctionVariantWithOriginal.php index af85439f365..eac8b2b1a01 100644 --- a/src/Reflection/ResolvedFunctionVariantWithOriginal.php +++ b/src/Reflection/ResolvedFunctionVariantWithOriginal.php @@ -13,6 +13,7 @@ use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\Generic\TemplateTypeReference; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Generic\TemplateTypeVarianceMap; use PHPStan\Type\Generic\UnresolvedTemplateArgumentType; @@ -290,51 +291,7 @@ private function resolveResolvableTemplateTypes(Type $type, TemplateTypeVariance } } - $objectCb = function (Type $type, callable $traverse) use ($references, $onlyCovariant, $site, $frame, $allowUnresolved): Type { - if ( - $type instanceof TemplateType - && !$type instanceof NarrowedSubjectType - && !$type->isArgument() - && $type->getScope()->getFunctionName() !== null - ) { - $newType = $this->resolvedTemplateTypeMap->getType($type->getName()); - if ($newType === null || $newType instanceof ErrorType) { - return $traverse($type); - } - - if ($site !== null && $frame !== null) { - $newType = $this->unresolvedOrResolvedTemplateArgument($type, $newType, $site, $frame, $allowUnresolved, $onlyCovariant[$type->getName()] ?? false); - } else { - $newType = TemplateTypeHelper::generalizeInferredTemplateType($type, $newType); - } - $variance = TemplateTypeVariance::createInvariant(); - foreach ($references as $reference) { - // this uses identity to distinguish between different occurrences of the same template type - // see https://github.com/phpstan/phpstan-src/pull/2485#discussion_r1328555397 for details - if ($reference->getType() === $type) { - $variance = $reference->getPositionVariance(); - break; - } - } - - $callSiteVariance = $this->callSiteVarianceMap->getVariance($type->getName()); - if ($callSiteVariance === null || $callSiteVariance->invariant()) { - return $newType; - } - - if (!$callSiteVariance->covariant() && $variance->covariant()) { - return $traverse($type->getBound()); - } - - if (!$callSiteVariance->contravariant() && $variance->contravariant()) { - return new NonAcceptingNeverType(); - } - - return $newType; - } - - return $traverse($type); - }; + $objectCb = fn (Type $type, callable $traverse): Type => $this->resolveTemplateTypeInGenericType($type, $traverse, $references, $onlyCovariant, $site, $frame, $allowUnresolved, null); return TypeTraverser::map($type, function (Type $type, callable $traverse) use ($references, $objectCb): Type { if ($type instanceof GenericObjectType || $type instanceof GenericStaticType) { @@ -383,6 +340,87 @@ private function resolveResolvableTemplateTypes(Type $type, TemplateTypeVariance }); } + /** + * @param callable(Type): Type $traverse + * @param list $references + * @param array $onlyCovariant + * @param string|null $keepInferredName a template type whose inferred type is not generalized here + */ + private function resolveTemplateTypeInGenericType( + Type $type, + callable $traverse, + array $references, + array $onlyCovariant, + ?Expr $site, + ?TemplateArgumentFrame $frame, + bool $allowUnresolved, + ?string $keepInferredName, + ): Type + { + if ( + $type instanceof NarrowedSubjectType + && $keepInferredName === null + && !$type->isArgument() + && $type->getScope()->getFunctionName() !== null + ) { + // generalize what the branch knows about the subject, not the subject: in the + // else branch of `(T is \UnitEnum ? array-key : T)` with T of array-key|\UnitEnum, + // T is a scalar and an inferred 'foo' stays 'foo'. Only the subject is taken as + // inferred - a template type in the target is generalized as the condition sees it. + $subjectName = $type->getName(); + $resolved = TypeTraverser::map($type, fn (Type $type, callable $traverse): Type => $this->resolveTemplateTypeInGenericType($type, $traverse, $references, $onlyCovariant, $site, $frame, $allowUnresolved, $subjectName)); + + return TemplateTypeHelper::generalizeInferredTemplateType($type, $resolved); + } + + if ( + $type instanceof TemplateType + && !$type instanceof NarrowedSubjectType + && !$type->isArgument() + && $type->getScope()->getFunctionName() !== null + ) { + $newType = $this->resolvedTemplateTypeMap->getType($type->getName()); + if ($newType === null || $newType instanceof ErrorType) { + return $traverse($type); + } + + // the subject of a narrowed subject is generalized as a whole by the caller + if ($type->getName() !== $keepInferredName) { + if ($site !== null && $frame !== null) { + $newType = $this->unresolvedOrResolvedTemplateArgument($type, $newType, $site, $frame, $allowUnresolved, $onlyCovariant[$type->getName()] ?? false); + } else { + $newType = TemplateTypeHelper::generalizeInferredTemplateType($type, $newType); + } + } + $variance = TemplateTypeVariance::createInvariant(); + foreach ($references as $reference) { + // this uses identity to distinguish between different occurrences of the same template type + // see https://github.com/phpstan/phpstan-src/pull/2485#discussion_r1328555397 for details + if ($reference->getType() === $type) { + $variance = $reference->getPositionVariance(); + break; + } + } + + $callSiteVariance = $this->callSiteVarianceMap->getVariance($type->getName()); + if ($callSiteVariance === null || $callSiteVariance->invariant()) { + return $newType; + } + + if (!$callSiteVariance->covariant() && $variance->covariant()) { + return $traverse($type->getBound()); + } + + if (!$callSiteVariance->contravariant() && $variance->contravariant()) { + return new NonAcceptingNeverType(); + } + + return $newType; + } + + return $traverse($type); + } + /** * An inferred template argument inside a generic object of the return type: * during the body's observation pass a marker keyed by the call (an inferred diff --git a/tests/PHPStan/Analyser/NarrowedSubjectInferredLiteralTest.php b/tests/PHPStan/Analyser/NarrowedSubjectInferredLiteralTest.php new file mode 100644 index 00000000000..6af48c09abd --- /dev/null +++ b/tests/PHPStan/Analyser/NarrowedSubjectInferredLiteralTest.php @@ -0,0 +1,36 @@ += 8.1')] +class NarrowedSubjectInferredLiteralTest extends TypeInferenceTestCase +{ + + public static function dataFileAsserts(): iterable + { + yield from self::gatherAssertTypes(__DIR__ . '/data/narrowed-subject-inferred-literal.php'); + } + + /** + * @param mixed ...$args + */ + #[DataProvider('dataFileAsserts')] + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + +} diff --git a/tests/PHPStan/Analyser/data/narrowed-subject-inferred-literal.php b/tests/PHPStan/Analyser/data/narrowed-subject-inferred-literal.php new file mode 100644 index 00000000000..fda128d6256 --- /dev/null +++ b/tests/PHPStan/Analyser/data/narrowed-subject-inferred-literal.php @@ -0,0 +1,136 @@ += 8.1 + +declare(strict_types = 1); + +namespace NarrowedSubjectInferredLiteral; + +use function PHPStan\Testing\assertType; + +class User +{ + +} + +enum Digit: int +{ + + case One = 1; + +} + +/** + * @template TKey of array-key + * @template TValue + */ +class Collection +{ + + /** + * @template TNewKey of array-key|\UnitEnum + * + * @param (callable(TValue, TKey): TNewKey)|array|string $keyBy + * @return static<($keyBy is (array|string) ? array-key : (TNewKey is \UnitEnum ? array-key : TNewKey)), TValue> + */ + public function keyBy($keyBy) + { + throw new \Exception(); + } + + /** + * @template TNewKey of array-key|\UnitEnum + * + * @param callable(TValue, TKey): TNewKey $keyBy + * @return static<(TNewKey is \UnitEnum ? array-key : TNewKey), TValue> + */ + public function keyByCallback($keyBy) + { + throw new \Exception(); + } + +} + +/** + * @template T of array-key + * @template U of array-key + * @param T $a + * @param U $b + * @return Collection<(T is U ? T : int), int> + */ +function templateTarget($a, $b) +{ + throw new \Exception(); +} + +/** + * @template T of array-key + * @template U of array-key + * @param T $a + * @param U $b + * @return Collection<(T is not U ? int : T), int> + */ +function negatedTemplateTarget($a, $b) +{ + throw new \Exception(); +} + +/** @param Collection $collection */ +function test(Collection $collection): void +{ + assertType("NarrowedSubjectInferredLiteral\\Collection<'foo', NarrowedSubjectInferredLiteral\\User>", $collection->keyBy(fn ($user) => 'foo')); + assertType('NarrowedSubjectInferredLiteral\\Collection<0, NarrowedSubjectInferredLiteral\\User>', $collection->keyBy(static fn ($user): int => 0)); + assertType('NarrowedSubjectInferredLiteral\\Collection<(int|string), NarrowedSubjectInferredLiteral\\User>', $collection->keyBy('name')); + assertType('NarrowedSubjectInferredLiteral\\Collection<(int|string), NarrowedSubjectInferredLiteral\\User>', $collection->keyBy(static fn ($user) => Digit::One)); + + assertType("NarrowedSubjectInferredLiteral\\Collection<'foo', NarrowedSubjectInferredLiteral\\User>", $collection->keyByCallback(fn ($user) => 'foo')); + assertType('NarrowedSubjectInferredLiteral\\Collection<(int|string), NarrowedSubjectInferredLiteral\\User>', $collection->keyByCallback(static fn ($user) => Digit::One)); + + // the condition compares the generalized T and U, the branch keeps the inferred T + assertType("NarrowedSubjectInferredLiteral\\Collection<'foo', int>", templateTarget('foo', 'bar')); + assertType("NarrowedSubjectInferredLiteral\\Collection<'foo', int>", negatedTemplateTarget('foo', 'bar')); +} + +/** + * @template TKey of array-key + * @template TValue + */ +interface Enumerable +{ + + /** + * @template TNewKey of array-key|\UnitEnum + * + * @param (callable(TValue, TKey): TNewKey)|array|string $keyBy + * @return static<($keyBy is (array|string) ? array-key : (TNewKey is \UnitEnum ? array-key : TNewKey)), TValue> + */ + public function keyBy($keyBy); + +} + +/** + * @template TKey of array-key + * @template TValue + * @implements Enumerable + */ +class InheritedCollection implements Enumerable +{ + + /** + * {@inheritDoc} + */ + public function keyBy($keyBy) + { + throw new \Exception(); + } + +} + +/** @param InheritedCollection $collection */ +function testInherited(InheritedCollection $collection): void +{ + assertType("NarrowedSubjectInferredLiteral\\InheritedCollection<'foo', NarrowedSubjectInferredLiteral\\User>", $collection->keyBy(function ($user, $int) { + return 'foo'; + })); + assertType('NarrowedSubjectInferredLiteral\\InheritedCollection<0, NarrowedSubjectInferredLiteral\\User>', $collection->keyBy(static fn ($user): int => 0)); + assertType('NarrowedSubjectInferredLiteral\\InheritedCollection<(int|string), NarrowedSubjectInferredLiteral\\User>', $collection->keyBy('name')); + assertType('NarrowedSubjectInferredLiteral\\InheritedCollection<(int|string), NarrowedSubjectInferredLiteral\\User>', $collection->keyBy(static fn ($user) => Digit::One)); +} diff --git a/turbo-ext/src/ResolvedFunctionVariantWithOriginal.cpp b/turbo-ext/src/ResolvedFunctionVariantWithOriginal.cpp index 1bffe1006fa..01a9b3fa80c 100644 --- a/turbo-ext/src/ResolvedFunctionVariantWithOriginal.cpp +++ b/turbo-ext/src/ResolvedFunctionVariantWithOriginal.cpp @@ -133,7 +133,7 @@ zv::Val weakReferenceCreate(zval *object) #define PT_RFV_CLASS "PHPStan\\Reflection\\ResolvedFunctionVariantWithOriginal" /* the closure names differ: PHP 8.3 names a closure by its namespace alone */ #define PT_RFV_CLOSURE(method, line) PT_ABI_SINCE(80400, PT_RFV_CLASS "::{closure:" PT_RFV_CLASS "::" method "():" line "}", PT_RFV_CLASS "::PHPStan\\Reflection\\{closure}") -#define PT_RFV_PARAMETERS_CLOSURE PT_RFV_CLOSURE("getParameters", "90") +#define PT_RFV_PARAMETERS_CLOSURE PT_RFV_CLOSURE("getParameters", "91") /* }}} */ @@ -342,7 +342,7 @@ class ResolvedFunctionVariantWithOriginal zval null = {}; ZVAL_NULL(&null); - zv::Val objectCallback = pt_native_closure(&objectCallbackBody, self, references.raw(), onlyCovariant.raw(), site != NULL ? site : &null, frame != NULL ? frame : &null, allowUnresolved); + zv::Val objectCallback = pt_native_closure(&objectCallbackBody, self, references.raw(), onlyCovariant.raw(), site != NULL ? site : &null, frame != NULL ? frame : &null, allowUnresolved, &null); zv::Val callback = pt_native_closure(&typeCallbackBody, self, references.raw(), objectCallback.raw()); zval mapped; if (UNEXPECTED(!pt_type_traverser_map(&mapped, type, callback.raw()))) return zv::Val(); @@ -683,7 +683,7 @@ class ResolvedFunctionVariantWithOriginal static void typeCallbackBody(zval *captures, uint32_t argc, zval *argv, zval *return_value) { if (UNEXPECTED(argc < 2)) { - zend_throw_error(zend_ce_argument_count_error, "Too few arguments to function %s(), %u passed and exactly 2 expected", PT_RFV_CLOSURE("resolveResolvableTemplateTypes", "338"), argc); + zend_throw_error(zend_ce_argument_count_error, "Too few arguments to function %s(), %u passed and exactly 2 expected", PT_RFV_CLOSURE("resolveResolvableTemplateTypes", "296"), argc); return; } zend_object *self = Z_OBJ(captures[0]); @@ -741,31 +741,50 @@ class ResolvedFunctionVariantWithOriginal (void) pt_type_traverser_traverse(return_value, traverse, type); } - /* the $objectCb of resolveResolvableTemplateTypes() — captures: $this, - * $references, $onlyCovariant, $site, $frame, $allowUnresolved */ + /* the $objectCb of resolveResolvableTemplateTypes(), and the nested one of + * resolveTemplateTypeInGenericType() — captures: $this, $references, + * $onlyCovariant, $site, $frame, $allowUnresolved, $keepInferredName */ static void objectCallbackBody(zval *captures, uint32_t argc, zval *argv, zval *return_value) { if (UNEXPECTED(argc < 2)) { - zend_throw_error(zend_ce_argument_count_error, "Too few arguments to function %s(), %u passed and exactly 2 expected", PT_RFV_CLOSURE("resolveResolvableTemplateTypes", "292"), argc); + zend_throw_error(zend_ce_argument_count_error, "Too few arguments to function %s(), %u passed and exactly 2 expected", PT_RFV_CLOSURE("resolveResolvableTemplateTypes", "294"), argc); return; } - zend_object *self = Z_OBJ(captures[0]); - zval *references = &captures[1]; - zval *onlyCovariant = &captures[2]; - zval *site = &captures[3]; - zval *frame = &captures[4]; - bool allowUnresolved = Z_TYPE(captures[5]) == IS_TRUE; - zval *type = &argv[0]; - zval *traverse = &argv[1]; + resolveTemplateTypeInGenericType(Z_OBJ(captures[0]), &argv[0], &argv[1], &captures[1], &captures[2], &captures[3], &captures[4], Z_TYPE(captures[5]) == IS_TRUE, &captures[6], return_value); + } +public: + /* Mirrors resolveTemplateTypeInGenericType() (private); $site / $frame / + * $keepInferredName as null zvals for null. */ + static void resolveTemplateTypeInGenericType(zend_object *self, zval *type, zval *traverse, zval *references, zval *onlyCovariant, zval *site, zval *frame, bool allowUnresolved, zval *keepInferredName, zval *return_value) + { bool isTemplate = false; if (Z_TYPE_P(type) == IS_OBJECT && UNEXPECTED(!pt_type_instanceof(type, PT_CLASS_TEMPLATE_TYPE, isTemplate))) return; - if (isTemplate) { - bool isNarrowedSubject; - if (UNEXPECTED(!pt_type_instanceof(type, PT_CLASS_NARROWED_SUBJECT_TYPE, isNarrowedSubject))) return; - if (isNarrowedSubject) isTemplate = false; + bool isNarrowedSubject = false; + if (isTemplate && UNEXPECTED(!pt_type_instanceof(type, PT_CLASS_NARROWED_SUBJECT_TYPE, isNarrowedSubject))) return; + + if (isNarrowedSubject && Z_TYPE_P(keepInferredName) == IS_NULL) { + bool isArgument; + if (UNEXPECTED(!templateIsArgument(type, isArgument))) return; + bool functionScope = false; + if (!isArgument && UNEXPECTED(!templateHasFunctionScope(type, functionScope))) return; + if (!isArgument && functionScope) { + /* generalize what the branch knows about the subject, not the subject */ + zv::Val subjectName = templateName(type); + if (UNEXPECTED(subjectName.isUndef())) return; + zv::Val inner = pt_native_closure(&objectCallbackBody, self, references, onlyCovariant, site, frame, allowUnresolved, subjectName.raw()); + zval resolved; + if (UNEXPECTED(!pt_type_traverser_map(&resolved, type, inner.raw()))) return; + zv::Val resolvedHold = zv::Val::adopt(resolved); + zv::Args generalizeArgs{type, resolvedHold.raw()}; + zv::Val generalized = pt_type_call_static_ce(pt_ce_template_type_helper, PT_LC("generalizeinferredtemplatetype"), 2, generalizeArgs); + if (UNEXPECTED(generalized.isUndef())) return; + ZVAL_COPY(return_value, generalized.raw()); + return; + } } - if (isTemplate) { + + if (isTemplate && !isNarrowedSubject) { bool isArgument; if (UNEXPECTED(!templateIsArgument(type, isArgument))) return; bool functionScope = false; @@ -780,15 +799,19 @@ class ResolvedFunctionVariantWithOriginal return; } - if (Z_TYPE_P(site) != IS_NULL && Z_TYPE_P(frame) != IS_NULL) { - /* $onlyCovariant[$type->getName()] ?? false */ - zval *covariantSlot = Z_TYPE_P(name.raw()) == IS_STRING ? zend_symtable_find(Z_ARRVAL_P(onlyCovariant), Z_STR_P(name.raw())) : NULL; - newType = unresolvedOrResolvedTemplateArgument(type, newType.raw(), site, frame, allowUnresolved, covariantSlot != NULL && Z_TYPE_P(covariantSlot) == IS_TRUE); - } else { - zv::Args generalizeArgs{type, newType.raw()}; - newType = pt_type_call_static_ce(pt_ce_template_type_helper, PT_LC("generalizeinferredtemplatetype"), 2, generalizeArgs); + /* the subject of a narrowed subject is generalized as a whole by the caller */ + bool keep = Z_TYPE_P(keepInferredName) == IS_STRING && Z_TYPE_P(name.raw()) == IS_STRING && zend_string_equals(Z_STR_P(keepInferredName), Z_STR_P(name.raw())); + if (!keep) { + if (Z_TYPE_P(site) != IS_NULL && Z_TYPE_P(frame) != IS_NULL) { + /* $onlyCovariant[$type->getName()] ?? false */ + zval *covariantSlot = Z_TYPE_P(name.raw()) == IS_STRING ? zend_symtable_find(Z_ARRVAL_P(onlyCovariant), Z_STR_P(name.raw())) : NULL; + newType = unresolvedOrResolvedTemplateArgument(type, newType.raw(), site, frame, allowUnresolved, covariantSlot != NULL && Z_TYPE_P(covariantSlot) == IS_TRUE); + } else { + zv::Args generalizeArgs{type, newType.raw()}; + newType = pt_type_call_static_ce(pt_ce_template_type_helper, PT_LC("generalizeinferredtemplatetype"), 2, generalizeArgs); + } + if (UNEXPECTED(newType.isUndef())) return; } - if (UNEXPECTED(newType.isUndef())) return; zv::Val varianceHold; zval *variance = referenceVarianceOf(references, type, varianceHold); @@ -802,12 +825,13 @@ class ResolvedFunctionVariantWithOriginal (void) pt_type_traverser_traverse(return_value, traverse, type); } +private: /* the TypeTraverser callback of narrowTemplateTypesInConditionalTypesForParameter() * — captures: $this */ static void narrowCallbackBody(zval *captures, uint32_t argc, zval *argv, zval *return_value) { if (UNEXPECTED(argc < 2)) { - zend_throw_error(zend_ce_argument_count_error, "Too few arguments to function %s(), %u passed and exactly 2 expected", PT_RFV_CLOSURE("narrowTemplateTypesInConditionalTypesForParameter", "427"), argc); + zend_throw_error(zend_ce_argument_count_error, "Too few arguments to function %s(), %u passed and exactly 2 expected", PT_RFV_CLOSURE("narrowTemplateTypesInConditionalTypesForParameter", "466"), argc); return; } zend_object *self = Z_OBJ(captures[0]); @@ -864,7 +888,7 @@ class ResolvedFunctionVariantWithOriginal static void referencesCallbackBody(zval *references, zval *templateType, uint32_t argc, zval *argv, zval *return_value) { if (UNEXPECTED(argc < 2)) { - zend_throw_error(zend_ce_argument_count_error, "Too few arguments to function %s(), %u passed and exactly 2 expected", PT_RFV_CLOSURE("referencesTemplateType", "490"), argc); + zend_throw_error(zend_ce_argument_count_error, "Too few arguments to function %s(), %u passed and exactly 2 expected", PT_RFV_CLOSURE("referencesTemplateType", "529"), argc); return; } zval *type = &argv[0]; @@ -1006,6 +1030,30 @@ PT_MINIT_REGISTRATION(pt_register_resolved_function_variant_with_original) PT_RETURN_VAL(PT_RFV_THIS.resolveResolvableTemplateTypes(type, positionVariance, site, frame, allowUnresolved)); }); + cls.method(sigs::resolveTemplateTypeInGenericType, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *type, *traverse, *references, *onlyCovariant, *site, *frame; + bool allowUnresolved; + zend_string *keepInferredName; + ZEND_PARSE_PARAMETERS_START(8, 8) + Z_PARAM_OBJECT_OF_CLASS(type, pt_class(PT_CLASS_TYPE)) + Z_PARAM_ZVAL(traverse) + Z_PARAM_ARRAY(references) + Z_PARAM_ARRAY(onlyCovariant) + Z_PARAM_OBJECT_OF_CLASS_OR_NULL(site, pt_class(PT_CLASS_EXPR)) + Z_PARAM_OBJECT_OF_CLASS_OR_NULL(frame, pt_ce_template_argument_frame) + Z_PARAM_BOOL(allowUnresolved) + Z_PARAM_STR_OR_NULL(keepInferredName) + ZEND_PARSE_PARAMETERS_END(); + zval null, keep; + ZVAL_NULL(&null); + if (keepInferredName != NULL) { + ZVAL_STR(&keep, keepInferredName); + } else { + ZVAL_NULL(&keep); + } + ResolvedFunctionVariantWithOriginal::resolveTemplateTypeInGenericType(Z_OBJ_P(ZEND_THIS), type, traverse, references, onlyCovariant, site != NULL ? site : &null, frame != NULL ? frame : &null, allowUnresolved, &keep, return_value); + }); + cls.method(sigs::unresolvedOrResolvedTemplateArgument, [](INTERNAL_FUNCTION_PARAMETERS) { zval *templateType, *inferred, *site, *frame; bool allowUnresolved, onlyCovariant; diff --git a/turbo-ext/src/generated/ResolvedFunctionVariantWithOriginal.h b/turbo-ext/src/generated/ResolvedFunctionVariantWithOriginal.h index 47db27a51a7..ddccc084378 100644 --- a/turbo-ext/src/generated/ResolvedFunctionVariantWithOriginal.h +++ b/turbo-ext/src/generated/ResolvedFunctionVariantWithOriginal.h @@ -84,17 +84,21 @@ inline constexpr char strings[] = "null\0" /* 782 */ "true\0" /* 787 */ "resolveResolvableTemplateTypes\0" /* 792 */ - "template\0" /* 823 */ - "PHPStan\\Type\\Generic\\TemplateType\0" /* 832 */ - "inferred\0" /* 866 */ - "onlyCovariant\0" /* 875 */ - "unresolvedOrResolvedTemplateArgument\0" /* 889 */ - "narrowTemplateTypesInConditionalTypesForParameter\0" /* 926 */ - "parameterName\0" /* 976 */ - "getTemplateTypeBoundOnlyByParameter\0" /* 990 */ - "templateType\0" /* 1026 */ - "referencesTemplateType\0" /* 1039 */ - "resolveConditionalTypesForParameter"; /* 1062 */ + "traverse\0" /* 823 */ + "references\0" /* 832 */ + "onlyCovariant\0" /* 843 */ + "keepInferredName\0" /* 857 */ + "resolveTemplateTypeInGenericType\0" /* 874 */ + "template\0" /* 907 */ + "PHPStan\\Type\\Generic\\TemplateType\0" /* 916 */ + "inferred\0" /* 950 */ + "unresolvedOrResolvedTemplateArgument\0" /* 959 */ + "narrowTemplateTypesInConditionalTypesForParameter\0" /* 996 */ + "parameterName\0" /* 1046 */ + "getTemplateTypeBoundOnlyByParameter\0" /* 1060 */ + "templateType\0" /* 1096 */ + "referencesTemplateType\0" /* 1109 */ + "resolveConditionalTypesForParameter"; /* 1132 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 19), /* __construct $parametersAcceptor */ reg::packed(65, 0, 89), /* __construct $resolvedTemplateTypeMap */ @@ -124,19 +128,28 @@ inline constexpr reg::PackedArg args[] = { reg::packed(526, MAY_BE_NULL, 532, false, false, 782), /* resolveResolvableTemplateTypes $frame */ reg::packed(580, MAY_BE_BOOL, reg::NoString, false, false, 787), /* resolveResolvableTemplateTypes $allowUnresolved */ reg::packed(214, 0, 377), /* resolveResolvableTemplateTypes return */ - reg::packed(823, 0, 832), /* unresolvedOrResolvedTemplateArgument $template */ - reg::packed(866, 0, 377), /* unresolvedOrResolvedTemplateArgument $inferred */ + reg::packed(694, 0, 377), /* resolveTemplateTypeInGenericType $type */ + reg::packed(823, MAY_BE_CALLABLE), /* resolveTemplateTypeInGenericType $traverse */ + reg::packed(832, MAY_BE_ARRAY), /* resolveTemplateTypeInGenericType $references */ + reg::packed(843, MAY_BE_ARRAY), /* resolveTemplateTypeInGenericType $onlyCovariant */ + reg::packed(501, MAY_BE_NULL, 506), /* resolveTemplateTypeInGenericType $site */ + reg::packed(526, MAY_BE_NULL, 532), /* resolveTemplateTypeInGenericType $frame */ + reg::packed(580, MAY_BE_BOOL), /* resolveTemplateTypeInGenericType $allowUnresolved */ + reg::packed(857, MAY_BE_NULL | MAY_BE_STRING), /* resolveTemplateTypeInGenericType $keepInferredName */ + reg::packed(214, 0, 377), /* resolveTemplateTypeInGenericType return */ + reg::packed(907, 0, 916), /* unresolvedOrResolvedTemplateArgument $template */ + reg::packed(950, 0, 377), /* unresolvedOrResolvedTemplateArgument $inferred */ reg::packed(501, 0, 506), /* unresolvedOrResolvedTemplateArgument $site */ reg::packed(526, 0, 532), /* unresolvedOrResolvedTemplateArgument $frame */ reg::packed(580, MAY_BE_BOOL), /* unresolvedOrResolvedTemplateArgument $allowUnresolved */ - reg::packed(875, MAY_BE_BOOL), /* unresolvedOrResolvedTemplateArgument $onlyCovariant */ + reg::packed(843, MAY_BE_BOOL), /* unresolvedOrResolvedTemplateArgument $onlyCovariant */ reg::packed(214, 0, 377), /* unresolvedOrResolvedTemplateArgument return */ reg::packed(694, 0, 377), /* narrowTemplateTypesInConditionalTypesForParameter $type */ reg::packed(214, 0, 377), /* narrowTemplateTypesInConditionalTypesForParameter return */ - reg::packed(976, MAY_BE_STRING), /* getTemplateTypeBoundOnlyByParameter $parameterName */ - reg::packed(214, MAY_BE_NULL, 832), /* getTemplateTypeBoundOnlyByParameter return */ + reg::packed(1046, MAY_BE_STRING), /* getTemplateTypeBoundOnlyByParameter $parameterName */ + reg::packed(214, MAY_BE_NULL, 916), /* getTemplateTypeBoundOnlyByParameter return */ reg::packed(694, 0, 377), /* referencesTemplateType $type */ - reg::packed(1026, 0, 832), /* referencesTemplateType $templateType */ + reg::packed(1096, 0, 916), /* referencesTemplateType $templateType */ reg::packed(214, MAY_BE_BOOL), /* referencesTemplateType return */ reg::packed(694, 0, 377), /* resolveConditionalTypesForParameter $type */ reg::packed(214, 0, 377), /* resolveConditionalTypesForParameter return */ @@ -162,11 +175,12 @@ inline constexpr sigtab::Sig getNativeReturnType = { { 661 /* getNativeReturnTyp inline constexpr sigtab::Sig hasBoundArgs = { { 681 /* hasBoundArgs */, 0, 19, 0, 19, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig resolveConditionalTypes = { { 699 /* resolveConditionalTypes */, 1, 20, 1, 21, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig resolveResolvableTemplateTypes = { { 792 /* resolveResolvableTemplateTypes */, 2, 22, 5, 27, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig unresolvedOrResolvedTemplateArgument = { { 889 /* unresolvedOrResolvedTemplateArgument */, 6, 28, 6, 34, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig narrowTemplateTypesInConditionalTypesForParameter = { { 926 /* narrowTemplateTypesInConditionalTypesForParameter */, 1, 35, 1, 36, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig getTemplateTypeBoundOnlyByParameter = { { 990 /* getTemplateTypeBoundOnlyByParameter */, 1, 37, 1, 38, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig referencesTemplateType = { { 1039 /* referencesTemplateType */, 2, 39, 2, 41, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig resolveConditionalTypesForParameter = { { 1062 /* resolveConditionalTypesForParameter */, 1, 42, 1, 43, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig resolveTemplateTypeInGenericType = { { 874 /* resolveTemplateTypeInGenericType */, 8, 28, 8, 36, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig unresolvedOrResolvedTemplateArgument = { { 959 /* unresolvedOrResolvedTemplateArgument */, 6, 37, 6, 43, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig narrowTemplateTypesInConditionalTypesForParameter = { { 996 /* narrowTemplateTypesInConditionalTypesForParameter */, 1, 44, 1, 45, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getTemplateTypeBoundOnlyByParameter = { { 1060 /* getTemplateTypeBoundOnlyByParameter */, 1, 46, 1, 47, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig referencesTemplateType = { { 1109 /* referencesTemplateType */, 2, 48, 2, 50, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig resolveConditionalTypesForParameter = { { 1132 /* resolveConditionalTypesForParameter */, 1, 51, 1, 52, ZEND_ACC_PRIVATE } }; } // namespace sig } // namespace ptdecl::ResolvedFunctionVariantWithOriginal From 220af56ef20d5b1de8cddc29ac7b3bb7983ec485 Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Mon, 5 Oct 2026 18:17:25 +0200 Subject: [PATCH 2/2] Give RequiresPhp a full version in NarrowedSubjectInferredLiteralTest Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/PHPStan/Analyser/NarrowedSubjectInferredLiteralTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/NarrowedSubjectInferredLiteralTest.php b/tests/PHPStan/Analyser/NarrowedSubjectInferredLiteralTest.php index 6af48c09abd..948141c309f 100644 --- a/tests/PHPStan/Analyser/NarrowedSubjectInferredLiteralTest.php +++ b/tests/PHPStan/Analyser/NarrowedSubjectInferredLiteralTest.php @@ -11,7 +11,7 @@ * (featureToggles.unresolvedTemplateArguments is off), which is where a conditional * branch's subject has to keep what the branch knows about it. */ -#[RequiresPhp('>= 8.1')] +#[RequiresPhp('>= 8.1.0')] class NarrowedSubjectInferredLiteralTest extends TypeInferenceTestCase {