Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@
use PHPStan\Reflection\ExtendedPropertyReflection;
use PHPStan\Reflection\ResolvedPropertyReflection;
use PHPStan\Turbo\ShadowedByTurboExtension;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Generic\GenericStaticType;
use PHPStan\Type\StaticType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverser;
use function count;

#[ShadowedByTurboExtension(implementation: __DIR__ . '/../../../turbo-ext/src/CalledOnTypeUnresolvedPropertyPrototypeReflection.cpp')]
final class CalledOnTypeUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection
Expand Down Expand Up @@ -85,6 +88,26 @@ private function transformPropertyWithStaticType(ClassReflection $declaringClass
private function transformStaticType(Type $type): Type
{
return TypeTraverser::map($type, function (Type $type, callable $traverse): Type {
if ($type instanceof GenericStaticType) {
$fetchedOnTypeReflections = $this->fetchedOnType->getObjectClassReflections();
if (count($fetchedOnTypeReflections) === 1) {
$staticObjectType = $type->changeBaseClass($fetchedOnTypeReflections[0])->getStaticObjectType();
if ($staticObjectType instanceof GenericObjectType) {
// do not carry over the class reflection of the declaring class,
// it would not know the type arguments of the resolved type
$staticObjectType = new GenericObjectType(
$staticObjectType->getClassName(),
$staticObjectType->getTypes(),
$staticObjectType->getSubtractedType(),
variances: $staticObjectType->getVariances(),
);
}

return $traverse($staticObjectType);
}

return $this->fetchedOnType;
}
if ($type instanceof StaticType) {
return $this->fetchedOnType;
}
Expand Down
45 changes: 45 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15378.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php // lint >= 8.1

declare(strict_types = 1);

namespace Bug15378;

use function PHPStan\Testing\assertType;

/**
* @template TNode of object
*/
class NodeCursor {
/**
* @var static<ANode>
*/
public ?self $a = null;

/**
* @var static
*/
public ?self $b = null;

final public function __construct(
/**
* @var TNode
*/
public readonly object $node,
) {
// empty
}
}

class ANode {

}

class BNode {

}

$node = new BNode();
$cursor = new NodeCursor($node);

assertType('Bug15378\NodeCursor<Bug15378\ANode>|null', $cursor->a);
assertType('Bug15378\NodeCursor<Bug15378\BNode>|null', $cursor->b);
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,10 @@ class CalledOnTypeUnresolvedPropertyPrototypeReflection
}

/* the closure of transformStaticType(): `function (Type $type, callable
* $traverse): Type` — a StaticType becomes the fetched-on type,
* everything else traverses */
* $traverse): Type` — a GenericStaticType becomes the static object
* type of its base class changed to the single fetched-on class
* (traversed on), or the fetched-on type itself; any other StaticType
* the fetched-on type; everything else traverses */
static void traverse(zval *thisZv, zval *state1, uint32_t argc, zval *argv, zval *return_value)
{
(void) state1;
Expand All @@ -158,6 +160,65 @@ class CalledOnTypeUnresolvedPropertyPrototypeReflection
return;
}
zval *type = &argv[0];
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(); */
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())) {
zend_type_error("phpstan_turbo: getObjectClassReflections() must return an array");
return;
}
if (zend_hash_num_elements(Z_ARRVAL_P(reflections.raw())) == 1) {
/* $type->changeBaseClass($fetchedOnTypeReflections[0])->getStaticObjectType() */
zval *first = zend_hash_index_find(Z_ARRVAL_P(reflections.raw()), 0);
if (UNEXPECTED(first == NULL)) {
zend_throw_error(NULL, "phpstan_turbo: getObjectClassReflections() must return a list");
return;
}
ZVAL_DEREF(first);
zv::Val changed = pt_type_call(Z_OBJ_P(type), PT_LC("changebaseclass"), 1, first);
if (UNEXPECTED(changed.isUndef())) return;
if (UNEXPECTED(!zv::Ref(changed.raw()).isObject())) {
zend_type_error("phpstan_turbo: changeBaseClass() must return an object");
return;
}
zv::Val staticObjectType = pt_type_call(Z_OBJ_P(changed.raw()), PT_LC("getstaticobjecttype"), 0, NULL);
if (UNEXPECTED(staticObjectType.isUndef())) return;
/* a GenericObjectType is rebuilt without the class reflection of the
* declaring class, which would not know the resolved type arguments:
* new GenericObjectType($t->getClassName(), $t->getTypes(), $t->getSubtractedType(), variances: $t->getVariances()) */
bool isGenericObject;
pt_type_instanceof_ce(staticObjectType.raw(), pt_ce_generic_object_type, isGenericObject);
if (isGenericObject) {
zend_object *objectType = Z_OBJ_P(staticObjectType.raw());
zv::Val className = pt_type_call(objectType, PT_LC("getclassname"), 0, NULL);
if (UNEXPECTED(className.isUndef())) return;
zv::Val objectTypes = pt_type_call(objectType, PT_LC("gettypes"), 0, NULL);
if (UNEXPECTED(objectTypes.isUndef())) return;
zv::Val subtracted = pt_type_call(objectType, PT_LC("getsubtractedtype"), 0, NULL);
if (UNEXPECTED(subtracted.isUndef())) return;
zv::Val objectVariances = pt_type_call(objectType, PT_LC("getvariances"), 0, NULL);
if (UNEXPECTED(objectVariances.isUndef())) return;
if (UNEXPECTED(Z_TYPE_P(className.raw()) != IS_STRING)) {
zend_type_error("phpstan_turbo: getClassName() must return a string");
return;
}
zval rebuiltRaw;
if (UNEXPECTED(!pt_generic_object_type_new(&rebuiltRaw, Z_STR_P(className.raw()), objectTypes.raw(), subtracted.raw(), NULL, objectVariances.raw()))) return;
staticObjectType = zv::Val::adopt(rebuiltRaw);
}
zv::Val traversed = pt_type_call_callable(&argv[1], 1, staticObjectType.raw());
if (UNEXPECTED(traversed.isUndef())) return;
traversed.intoReturnValue(return_value);
return;
}
ZVAL_COPY(return_value, fetchedOnType);
return;
}
bool isStatic;
pt_type_instanceof_ce(type, pt_ce_static_type, isStatic);
if (isStatic) {
Expand Down
Loading