diff --git a/src/Turbo/TurboExtensionEnabler.php b/src/Turbo/TurboExtensionEnabler.php index e77f9fdcba9..621eac414e5 100644 --- a/src/Turbo/TurboExtensionEnabler.php +++ b/src/Turbo/TurboExtensionEnabler.php @@ -33,7 +33,7 @@ final class TurboExtensionEnabler { - public const EXPECTED_EXTENSION_VERSION = '6888887'; + public const EXPECTED_EXTENSION_VERSION = '6f9d639'; private static bool $active = false; diff --git a/src/Type/Generic/TemplateTypeHelper.php b/src/Type/Generic/TemplateTypeHelper.php index 90c983455cd..c3662134b41 100644 --- a/src/Type/Generic/TemplateTypeHelper.php +++ b/src/Type/Generic/TemplateTypeHelper.php @@ -52,6 +52,17 @@ public static function resolveTemplateTypes( return $traverse($type); } + // the template type standing in for itself, e.g. in the class that declares it, + // keeps the bound narrowed at this occurrence, like `int|string` of a `K of int|string|object` + // in the key of `array` + if ( + $newType instanceof TemplateType + && $newType->getName() === $type->getName() + && $newType->getScope()->equals($type->getScope()) + ) { + return $traverse($newType->isArgument() ? $type->toArgument() : $type); + } + if ($newType instanceof ErrorType && !$keepErrorTypes) { return $traverse($type->getDefault() ?? $type->getBound()); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-15321.php b/tests/PHPStan/Analyser/nsrt/bug-15321.php new file mode 100644 index 00000000000..f7fc3026709 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-15321.php @@ -0,0 +1,127 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug15321; + +use function PHPStan\Testing\assertType; + +/** + * @template K of string|int|object + * @template V + */ +interface Map +{ + /** @return (K is array-key ? array : array) */ + public function toArray(): array; + + /** @return array */ + public function toArrayPlain(): array; + + /** @return non-empty-array */ + public function toNonEmptyArray(): array; + + /** @param array $array */ + public function merge(array $array): void; +} + +/** @param Map $map */ +function test(Map $map): void +{ + assertType('array', $map->toArray()); + assertType('array', $map->toArrayPlain()); + assertType('non-empty-array', $map->toNonEmptyArray()); +} + +/** + * @template K of string|int|object + * @template V + */ +final class ArrayMap +{ + + /** @var array */ + private array $items; + + /** + * @param array $promoted + * @param array $items + */ + public function __construct(private array $promoted, array $items) + { + $this->items = $items; + } + + /** @param array $array */ + public function merge(array $array): void + { + assertType('array', $array); + assertType('array', $this->items); + assertType('array', $this->promoted); + } + + /** @return array */ + public function toArray(): array + { + return $this->items; + } + +} + +/** @param ArrayMap $map */ +function testArrayMap(ArrayMap $map): void +{ + assertType('array', $map->toArray()); +} + +/** + * @template K of string|int|object + * @template V + */ +trait MapTrait +{ + + /** @return array */ + public function traitToArray(): array + { + return []; + } + +} + +/** + * @template K of string|int|object + * @template V + * @extends Map + */ +interface SortedMap extends Map +{ + + /** @return array */ + public function toSortedArray(): array; + +} + +/** + * @template K of string|int|object + * @template V + */ +abstract class MapWithTrait +{ + + /** @use MapTrait */ + use MapTrait; + +} + +/** + * @param SortedMap $sortedMap + * @param MapWithTrait $mapWithTrait + */ +function testInherited(SortedMap $sortedMap, MapWithTrait $mapWithTrait): void +{ + assertType('array', $sortedMap->toArray()); + assertType('array', $sortedMap->toArrayPlain()); + assertType('array', $sortedMap->toSortedArray()); + assertType('array', $mapWithTrait->traitToArray()); +} diff --git a/turbo-ext/src/TemplateTypeHelper.cpp b/turbo-ext/src/TemplateTypeHelper.cpp index ecc2a561e53..7dc93b002d3 100644 --- a/turbo-ext/src/TemplateTypeHelper.cpp +++ b/turbo-ext/src/TemplateTypeHelper.cpp @@ -260,6 +260,39 @@ void TemplateTypeHelper::resolveTemplateTypesCallback(zval *state0, zval *state1 return; } + /* the template type standing in for itself keeps the bound + * narrowed at this occurrence - see the twin */ + bool newIsTemplate; + if (UNEXPECTED(!pt_tth_is_template(newType.raw(), newIsTemplate))) return; + if (newIsTemplate) { + zv::Val newName = pt_tth_name(newType.raw()); + if (UNEXPECTED(newName.isUndef())) return; + if (zend_string_equals(Z_STR_P(newName.raw()), Z_STR_P(name.raw()))) { + zv::Val newScope = pt_type_call(Z_OBJ_P(newType.raw()), PT_LC("getscope"), 0, NULL); + if (UNEXPECTED(newScope.isUndef())) return; + zv::Val scope = pt_type_call(Z_OBJ_P(type), PT_LC("getscope"), 0, NULL); + if (UNEXPECTED(scope.isUndef())) return; + if (UNEXPECTED(!zv::Ref(newScope.raw()).isObject())) { + zend_type_error("phpstan_turbo: getScope() must return an object"); + return; + } + zv::Val scopesEqual = pt_type_call(Z_OBJ_P(newScope.raw()), PT_LC("equals"), 1, scope.raw()); + if (UNEXPECTED(scopesEqual.isUndef())) return; + if (zend_is_true(scopesEqual.raw())) { + zv::Val newIsArgument = pt_type_call(Z_OBJ_P(newType.raw()), PT_LC("isargument"), 0, NULL); + if (UNEXPECTED(newIsArgument.isUndef())) return; + if (!zend_is_true(newIsArgument.raw())) { + (void) pt_type_traverser_traverse(return_value, traverse, type); + return; + } + zv::Val argument = pt_type_call(Z_OBJ_P(type), PT_LC("toargument"), 0, NULL); + if (UNEXPECTED(argument.isUndef())) return; + (void) pt_type_traverser_traverse(return_value, traverse, argument.raw()); + return; + } + } + } + bool isError; if (UNEXPECTED(!pt_type_instanceof_ce(newType.raw(), pt_ce_error_type, isError))) return; if (isError && !zend_is_true(keepErrorTypes)) {