Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Turbo/TurboExtensionEnabler.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
final class TurboExtensionEnabler
{

public const EXPECTED_EXTENSION_VERSION = '6888887';
public const EXPECTED_EXTENSION_VERSION = '6f9d639';

private static bool $active = false;

Expand Down
11 changes: 11 additions & 0 deletions src/Type/Generic/TemplateTypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<K, V>`
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());
}
Expand Down
127 changes: 127 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15321.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?php // lint >= 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<K, V> : array<array-key, V>) */
public function toArray(): array;

/** @return array<K, V> */
public function toArrayPlain(): array;

/** @return non-empty-array<K, V> */
public function toNonEmptyArray(): array;

/** @param array<K, V> $array */
public function merge(array $array): void;
}

/** @param Map<string, int> $map */
function test(Map $map): void
{
assertType('array<string, int>', $map->toArray());
assertType('array<string, int>', $map->toArrayPlain());
assertType('non-empty-array<string, int>', $map->toNonEmptyArray());
}

/**
* @template K of string|int|object
* @template V
*/
final class ArrayMap
{

/** @var array<K, V> */
private array $items;

/**
* @param array<K, V> $promoted
* @param array<K, V> $items
*/
public function __construct(private array $promoted, array $items)
{
$this->items = $items;
}

/** @param array<K, V> $array */
public function merge(array $array): void
{
assertType('array<K of int|string (class Bug15321\ArrayMap, argument), V (class Bug15321\ArrayMap, argument)>', $array);
assertType('array<K of int|string (class Bug15321\ArrayMap, argument), V (class Bug15321\ArrayMap, argument)>', $this->items);
assertType('array<K of int|string (class Bug15321\ArrayMap, argument), V (class Bug15321\ArrayMap, argument)>', $this->promoted);
}

/** @return array<K, V> */
public function toArray(): array
{
return $this->items;
}

}

/** @param ArrayMap<int, string> $map */
function testArrayMap(ArrayMap $map): void
{
assertType('array<int, string>', $map->toArray());
}

/**
* @template K of string|int|object
* @template V
*/
trait MapTrait
{

/** @return array<K, V> */
public function traitToArray(): array
{
return [];
}

}

/**
* @template K of string|int|object
* @template V
* @extends Map<K, V>
*/
interface SortedMap extends Map
{

/** @return array<K, V> */
public function toSortedArray(): array;

}

/**
* @template K of string|int|object
* @template V
*/
abstract class MapWithTrait
{

/** @use MapTrait<K, V> */
use MapTrait;

}

/**
* @param SortedMap<string, int> $sortedMap
* @param MapWithTrait<string, int> $mapWithTrait
*/
function testInherited(SortedMap $sortedMap, MapWithTrait $mapWithTrait): void
{
assertType('array<string, int>', $sortedMap->toArray());
assertType('array<string, int>', $sortedMap->toArrayPlain());
assertType('array<string, int>', $sortedMap->toSortedArray());
assertType('array<string, int>', $mapWithTrait->traitToArray());
}
33 changes: 33 additions & 0 deletions turbo-ext/src/TemplateTypeHelper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
Loading