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
43 changes: 41 additions & 2 deletions src/Type/Php/GetParentClassDynamicFunctionReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\Generic\GenericClassStringType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\StaticType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeUtils;
Expand Down Expand Up @@ -63,9 +66,45 @@ public function getTypeFromFunctionCall(
return TypeCombinator::union(...array_map(fn (ConstantStringType $stringType): Type => $this->findParentClassNameType($stringType->getValue()), $constantStrings));
}

$classNames = $argType->getObjectClassNames();
// A `static::class` string refers to the same late-static-bound type as `$this`/`static`,
// so unwrap it and reuse the object handling below. Non-class-string types resolve to
// an ErrorType here, so they are left alone.
$valueType = $argType;
$classStringObjectType = $argType->getClassStringObjectType();
if ($classStringObjectType instanceof StaticType) {
$valueType = $classStringObjectType;
}

// Inside a trait a late-static-bound value cannot be resolved to a concrete parent,
// same as `$this` above.
if ($scope->isInTrait() && $valueType instanceof StaticType) {
return null;
}

$classNames = $valueType->getObjectClassNames();
if (count($classNames) > 0) {
return TypeCombinator::union(...array_map(fn (string $classNames): Type => $this->findParentClassNameType($classNames), $classNames));
// A `$this`/`static` value can be an instance of a subclass through late static
Comment thread
staabm marked this conversation as resolved.
// binding. For a non-final class the parent class is then not pinned to the declared
// parent: a direct child's parent is the class itself, a deeper descendant's parent is
// some subclass. So the result also includes `class-string<Class>`.
$isLateStaticBound = $valueType instanceof StaticType;

$types = [];
foreach ($classNames as $className) {
$types[] = $this->findParentClassNameType($className);

if (
!$isLateStaticBound
|| !$this->reflectionProvider->hasClass($className)
|| $this->reflectionProvider->getClass($className)->isFinal()
) {
continue;
}

$types[] = new GenericClassStringType(new ObjectType($className));
}

return TypeCombinator::union(...$types);
}

return null;
Expand Down
95 changes: 95 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14951.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php declare(strict_types = 1);

namespace Bug14951;

use function PHPStan\Testing\assertType;

class A
{

public function isDirectChildOfA(): bool
{
// $this can be an instance of a subclass, so get_parent_class($this)
// may return A::class (for a direct child) — not just false.
assertType('class-string<Bug14951\A>|false', get_parent_class($this));

// The sibling functions already model late static binding, so they do not have the
// same problem: they keep the subclass possibility instead of pinning to this class.
assertType('class-string<static(Bug14951\A)>', get_called_class());
assertType('class-string<$this(Bug14951\A)>', get_class($this));

// A class-string argument (e.g. self::class) names an exact class, not a runtime
// value, so it keeps the exact parent — no subclass widening.
assertType('false', get_parent_class(self::class));

// static::class is late static bound just like $this, so it is widened the same way.
assertType('class-string<Bug14951\A>|false', get_parent_class(static::class));

return get_parent_class($this) === self::class;
}

}

class B extends A
{

public function parentOfThis(): void
{
assertType('\'Bug14951\\\\A\'|class-string<Bug14951\B>', get_parent_class($this));
// self::class is the exact class B, so its parent is exactly A.
assertType("'Bug14951\\\\A'", get_parent_class(self::class));
// static::class is late static bound, matching get_parent_class($this).
assertType('\'Bug14951\\\\A\'|class-string<Bug14951\B>', get_parent_class(static::class));
}

}

final class FinalNoParent
{

public function parentOfThis(): void
{
// Final class cannot be subclassed, so the parent is exactly false.
assertType('false', get_parent_class($this));
assertType('false', get_parent_class(static::class));
// ... and get_called_class() is pinned to the final class.
assertType("'Bug14951\\\\FinalNoParent'", get_called_class());
}

}

final class FinalWithParent extends A
{

public function parentOfThis(): void
{
assertType("'Bug14951\\\\A'", get_parent_class($this));
assertType("'Bug14951\\\\A'", get_parent_class(static::class));
}

}

/** @final */
class PhpDocFinalNoParent
{

public function parentOfThis(): void
{
// A @final class cannot be subclassed either, so there is no subclass widening.
assertType('false', get_parent_class($this));
assertType('false', get_parent_class(static::class));
}

}

/** @final */
class PhpDocFinalWithParent extends A
{

public function parentOfThis(): void
{
assertType("'Bug14951\\\\A'", get_parent_class($this));
assertType("'Bug14951\\\\A'", get_parent_class(static::class));
}

}
12 changes: 10 additions & 2 deletions tests/PHPStan/Analyser/nsrt/get-parent-class.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class Foo
public function doFoo()
{
assertType('false', get_parent_class());
assertType('false', get_parent_class($this));
assertType('class-string<ParentClass\Foo>|false', get_parent_class($this));
assertType('class-string<$this(ParentClass\Foo)>', get_class($this));
assertType('\'ParentClass\\\\Foo\'', get_class());
}
Expand All @@ -25,7 +25,7 @@ class Bar extends Foo
public function doBar()
{
assertType('\'ParentClass\\\\Foo\'', get_parent_class());
assertType('\'ParentClass\\\\Foo\'', get_parent_class($this));
assertType('\'ParentClass\\\\Foo\'|class-string<ParentClass\Bar>', get_parent_class($this));
}

}
Expand All @@ -45,8 +45,16 @@ trait FooTrait

public function doBaz()
{
// Inside a trait a late-static-bound argument is not resolved to a concrete parent.
assertType('class-string|false', get_parent_class());
assertType('class-string|false', get_parent_class($this));
assertType('class-string|false', get_parent_class(static::class));

// self::class is not late static bound - it is the using class, so it is already a
// constant class-string here, indistinguishable from writing Bar::class.
assertType('\'ParentClass\\\\Bar\'', self::class);
assertType('\'ParentClass\\\\Foo\'', get_parent_class(self::class));
assertType('\'ParentClass\\\\Foo\'', get_parent_class(Bar::class));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,11 @@ public function testBug2835(): void
$this->analyse([__DIR__ . '/data/bug-2835.php'], []);
}

public function testBug14951(): void
{
$this->analyse([__DIR__ . '/data/bug-14951.php'], []);
}

public function testBug1860(): void
{
$this->analyse([__DIR__ . '/data/bug-1860.php'], [
Expand Down
23 changes: 23 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-14951.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php declare(strict_types = 1);

namespace Bug14951StrictComparison;

class A
{

public function isDirectChildOfA(): bool
{
// $this can be an instance of a subclass, so get_parent_class($this)
// can equal self::class for a direct child — this comparison is not always false.
return get_parent_class($this) === self::class;
}

}
Comment thread
staabm marked this conversation as resolved.

class B extends A
{
}

class C extends B
{
}
Loading