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
40 changes: 34 additions & 6 deletions src/Reflection/InitializerExprTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
use PHPStan\Type\FloatType;
use PHPStan\Type\GeneralizePrecision;
use PHPStan\Type\Generic\GenericClassStringType;
use PHPStan\Type\Generic\GenericStaticType;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
Expand All @@ -94,6 +95,7 @@
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypehintHelper;
use PHPStan\Type\TypeResult;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\TypeUtils;
use PHPStan\Type\TypeWithClassName;
use PHPStan\Type\UnaryOperatorTypeSpecifyingExtensionRegistry;
Expand Down Expand Up @@ -2734,10 +2736,12 @@ public function getClassConstFetchTypeByReflection(Name|Expr $class, string $con
}

$types = [];
foreach ($constantClassType->getObjectClassNames() as $referencedClass) {
$referencedClasses = $constantClassType->getObjectClassNames();
foreach ($referencedClasses as $referencedClass) {
if (!$this->getReflectionProvider()->hasClass($referencedClass)) {
continue;
}
$fetchedOnType = count($referencedClasses) === 1 ? $constantClassType : new ObjectType($referencedClass);

$constantClassReflection = $this->getReflectionProvider()->getClass($referencedClass);
if (!$constantClassReflection->hasConstant($constantName)) {
Expand Down Expand Up @@ -2777,13 +2781,13 @@ public function getClassConstFetchTypeByReflection(Name|Expr $class, string $con
if ($reflectionConstant->getType() !== null) {
$nativeType = TypehintHelper::decideTypeFromReflection($reflectionConstant->getType(), selfClass: $constantClassReflection);
}
$resolvedType = $this->constantResolver->resolveClassConstantType(
$resolvedType = $this->transformStaticType($this->constantResolver->resolveClassConstantType(
$constantClassReflection->getName(),
$constantName,
$constantType,
$nativeType,
$constantClassReflection->getConstantPhpDocType($constantName),
);
), $fetchedOnType);
$this->classConstantValueTypeCache[$resolvingName] = $resolvedType;
$types[] = $resolvedType;
unset($this->currentlyResolvingClassConstant[$resolvingName]);
Expand All @@ -2808,13 +2812,13 @@ public function getClassConstFetchTypeByReflection(Name|Expr $class, string $con
}

$nativeType = $constantReflection->getNativeType();
$constantType = $this->constantResolver->resolveClassConstantType(
$constantType = $this->transformStaticType($this->constantResolver->resolveClassConstantType(
$constantClassReflection->getName(),
$constantName,
$constantType,
$nativeType,
$constantClassReflection->getConstantPhpDocType($constantName),
);
), $fetchedOnType);
unset($this->currentlyResolvingClassConstant[$resolvingName]);
$types[] = $constantType;
}
Expand All @@ -2827,7 +2831,31 @@ public function getClassConstFetchTypeByReflection(Name|Expr $class, string $con
return new ErrorType();
}

return $constantClassType->getConstant($constantName)->getValueType();
return $this->transformStaticType($constantClassType->getConstant($constantName)->getValueType(), $constantClassType);
}

/**
* Resolves `static` in the type of a class constant against the class
* the constant is fetched on.
*/
private function transformStaticType(Type $type, Type $fetchedOnType): Type
{
return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($fetchedOnType): Type {
if (!$type instanceof StaticType) {
return $traverse($type);
}
if ($fetchedOnType instanceof StaticType) {
return $traverse($type->changeBaseClass($fetchedOnType->getClassReflection()));
}
if ($type instanceof GenericStaticType) {
$fetchedOnTypeReflections = $fetchedOnType->getObjectClassReflections();
if (count($fetchedOnTypeReflections) === 1) {
return $traverse($type->changeBaseClass($fetchedOnTypeReflections[0])->getStaticObjectType());
}
}

return $fetchedOnType;
});
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
use PHPStan\Reflection\Php\ExtendedDummyParameter;
use PHPStan\Reflection\ResolvedMethodReflection;
use PHPStan\Turbo\ShadowedByTurboExtension;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Generic\GenericStaticType;
use PHPStan\Type\StaticType;
use PHPStan\Type\ThisType;
Expand Down Expand Up @@ -150,19 +149,7 @@ private function transformStaticType(Type $type): Type
if (count($calledOnTypeReflections) === 1) {
$calledOnTypeReflection = $calledOnTypeReflections[0];

$staticObjectType = $type->changeBaseClass($calledOnTypeReflection)->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 $traverse($type->changeBaseClass($calledOnTypeReflection)->getStaticObjectType());
}

return $this->calledOnType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
use PHPStan\Reflection\ExtendedPropertyReflection;
use PHPStan\Reflection\ResolvedPropertyReflection;
use PHPStan\Turbo\ShadowedByTurboExtension;
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 +87,14 @@ 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) {
return $traverse($type->changeBaseClass($fetchedOnTypeReflections[0])->getStaticObjectType());
}

return $this->fetchedOnType;
}
if ($type instanceof StaticType) {
return $this->fetchedOnType;
}
Expand Down
38 changes: 37 additions & 1 deletion src/Type/Generic/GenericStaticType.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class GenericStaticType extends StaticType

private ?ObjectType $staticObjectType = null;

private ?ClassReflection $resolvedClassReflection = null;

/**
* @api
* @param array<int, Type> $types
Expand Down Expand Up @@ -60,6 +62,12 @@ public function getVariances(): array
return $this->variances;
}

/** The class reflection with the type arguments of this type. */
public function getClassReflection(): ClassReflection
{
return $this->resolvedClassReflection ??= $this->classReflection->withTypes($this->types)->withVariances($this->variances);
}

public function getStaticObjectType(): ObjectType
{
if ($this->staticObjectType === null) {
Expand All @@ -68,7 +76,7 @@ public function getStaticObjectType(): ObjectType
$this->classReflection->getName(),
$this->types,
$this->subtractedType,
$this->classReflection,
$this->getClassReflection(),
$this->variances,
);
}
Expand All @@ -79,6 +87,34 @@ public function getStaticObjectType(): ObjectType
return $this->staticObjectType;
}

/**
* The ancestor keeps the type arguments the static object type has for
* that class - changeBaseClass() can only carry them down to a subclass.
*/
public function getAncestorWithClassName(string $className): ?TypeWithClassName
{
$ancestor = $this->getStaticObjectType()->getAncestorWithClassName($className);
if ($ancestor === null) {
return null;
}

$classReflection = $ancestor->getClassReflection();
if ($classReflection === null) {
return null;
}

if ($classReflection->getName() === $this->getClassName() || !$classReflection->isGeneric()) {
return $this->changeBaseClass($classReflection);
}

return new self(
$classReflection,
$classReflection->typeMapToList($classReflection->getActiveTemplateTypeMap()),
$this->subtractedType,
$classReflection->varianceMapToList($classReflection->getCallSiteVarianceMap()),
);
}

public function changeBaseClass(ClassReflection $classReflection): StaticType
{
if ($classReflection->getName() === $this->getClassName()) {
Expand Down
3 changes: 1 addition & 2 deletions tests/PHPStan/Analyser/AnalyserIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1714,8 +1714,7 @@ public function testPr5880(): void
{
// endless loop
$errors = $this->runAnalyse(__DIR__ . '/data/pr-5880.php');
$this->assertCount(1, $errors);
$this->assertSame('Call to an undefined method (T of PR5880EndlessRecursion\A = PR5880EndlessRecursion\A|PR5880EndlessRecursion\B)|T of PR5880EndlessRecursion\B = PR5880EndlessRecursion\A|PR5880EndlessRecursion\B::a().', $errors[0]->getMessage());
$this->assertNoErrors($errors);
}

public function testBug14707(): void
Expand Down
4 changes: 2 additions & 2 deletions tests/PHPStan/Analyser/nsrt/bug-12575.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ public function doFoo(): void
{
assertType('$this(Bug12575\Bar<T of object (class Bug12575\Bar, argument)>)&static(Bug12575\Bar<Bug12575\A&T of object (class Bug12575\Bar, argument)>)', $this->add(A::class));
assertType('$this(Bug12575\Bar<T of object (class Bug12575\Bar, argument)>)&static(Bug12575\Bar<Bug12575\A&T of object (class Bug12575\Bar, argument)>)', $this);
assertType('T of object (class Bug12575\Bar, parameter)', $this->getT());
assertType('Bug12575\A&T of object (class Bug12575\Bar, argument)', $this->getT());
}

public function doBar(): void
{
$this->add(B::class);
assertType('$this(Bug12575\Bar<T of object (class Bug12575\Bar, argument)>)&static(Bug12575\Bar<Bug12575\B&T of object (class Bug12575\Bar, argument)>)', $this);
assertType('T of object (class Bug12575\Bar, parameter)', $this->getT());
assertType('Bug12575\B&T of object (class Bug12575\Bar, argument)', $this->getT());
}

/**
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);
64 changes: 64 additions & 0 deletions tests/PHPStan/Analyser/nsrt/class-constant-static-type.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php declare(strict_types = 1);

namespace ClassConstantStaticType;

use function PHPStan\Testing\assertType;

class Base
{

/** @var class-string<static> */
const IMPL = Base::class;

/** @var list<class-string<static>> */
const IMPLS = [Base::class];

public function inBase(): void
{
assertType('class-string<static(ClassConstantStaticType\Base)>', static::IMPL);
assertType('class-string<static(ClassConstantStaticType\Base)>', $this::IMPL);
assertType('\'ClassConstantStaticType\\\\Base\'', self::IMPL);
}

}

class Child extends Base
{

public function inChild(): void
{
assertType('class-string<static(ClassConstantStaticType\Child)>', static::IMPL);
assertType('list<class-string<static(ClassConstantStaticType\Child)>>', static::IMPLS);
assertType('class-string<static(ClassConstantStaticType\Child)>', $this::IMPL);
}

}

/**
* @template T
*/
class GenericBase
{

/** @var class-string<static<int>> */
const IMPL = GenericBase::class; // @phpstan-ignore classConstant.phpDocType

}

/** @extends GenericBase<string> */
class GenericChild extends GenericBase
{

}

/**
* @param GenericBase<string> $genericBase
*/
function test(Base $base, Child $child, GenericBase $genericBase, GenericChild $genericChild): void
{
assertType('class-string<ClassConstantStaticType\Base>', $base::IMPL);
assertType('class-string<ClassConstantStaticType\Child>', $child::IMPL);
assertType('list<class-string<ClassConstantStaticType\Child>>', $child::IMPLS);
assertType('class-string<ClassConstantStaticType\GenericBase<int>>', $genericBase::IMPL);
assertType('class-string<ClassConstantStaticType\GenericChild>', $genericChild::IMPL);
}
Loading
Loading