Skip to content

Fix get_class() on HasMethodType #2350

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: 1.10.x
Choose a base branch
from
Open
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
35 changes: 17 additions & 18 deletions src/Type/Php/GetClassDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\Enum\EnumCaseObjectType;
use PHPStan\Type\Generic\GenericClassStringType;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectShapeType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\StaticType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\TypeUtils;
use PHPStan\Type\UnionType;
Expand Down Expand Up @@ -64,27 +64,26 @@ static function (Type $type, callable $traverse): Type {
return new GenericClassStringType(new ObjectType($type->getClassName()));
}

$objectClassNames = $type->getObjectClassNames();
if ($type instanceof TemplateType && $objectClassNames === []) {
if ($type instanceof ObjectWithoutClassType) {
return new GenericClassStringType($type);
$isObject = $type->isObject();
if ($isObject->yes() || $isObject->maybe()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For which type here it's going to be a maybe?

if ($type instanceof ObjectShapeType) {
return new ClassStringType();
}

$objectType = TypeCombinator::intersect($type, new ObjectWithoutClassType());
if ($objectType instanceof StaticType) {
$objectType = $objectType->getStaticObjectType();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you checking this after an intersection?

}
$classStringType = new GenericClassStringType($objectType);

if ($isObject->yes()) {
return $classStringType;
}

return new UnionType([
new GenericClassStringType($type),
new ConstantBooleanType(false),
]);
} elseif ($type instanceof MixedType) {
return new UnionType([
new ClassStringType(),
$classStringType,
new ConstantBooleanType(false),
]);
} elseif ($type instanceof StaticType) {
return new GenericClassStringType($type->getStaticObjectType());
} elseif ($objectClassNames !== []) {
return new GenericClassStringType($type);
} elseif ($type instanceof ObjectWithoutClassType) {
return new ClassStringType();
}

return new ConstantBooleanType(false);
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,7 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/constant-array-union-unshift.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7987.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7963-three.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4890.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8017.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/global-namespace.php');

Expand Down
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/data/bug-3158.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ function (): void {
/** @var object $object */
$object = doFoo();
$objectClass = get_class($object);
assertType('class-string', $objectClass);
assertType('class-string<object>', $objectClass);
$proxy = createProxy($objectClass, function(AParent $o):void {});
assertType('object', $proxy);
};
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/data/bug-4741.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class Foo

public function isAnObject($object): void {
$class = get_class($object);
assertType('class-string|false', $class);
assertType('class-string<object>|false', $class);
}

}
53 changes: 53 additions & 0 deletions tests/PHPStan/Analyser/data/bug-4890.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace Bug4890;

use function PHPStan\Testing\assertType;

interface Proxy {}

class HelloWorld
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see https://phpstan.org/r/b6b95d5a-a3fb-4c29-be47-4e7832f00ddc regarding how this test fails before this PR

{
public function update(object $entity): void
{
assertType('class-string<object>', get_class($entity));
assert(method_exists($entity, 'getId'));
assertType('class-string<object&hasMethod(getId)>', get_class($entity));

if ($entity instanceof Proxy) {
assertType('class-string<Bug4890\Proxy&hasMethod(getId)>', get_class($entity));
}

$class = $entity instanceof Proxy
? get_parent_class($entity)
: get_class($entity);
assert(is_string($class));

}

public function updateProp(object $entity): void
{
assertType('class-string<object>', get_class($entity));
assert(property_exists($entity, 'myProp'));
assertType('class-string<object&hasProperty(myProp)>', get_class($entity));

if ($entity instanceof Proxy) {
assertType('class-string<Bug4890\Proxy&hasProperty(myProp)>', get_class($entity));
}

$class = $entity instanceof Proxy
? get_parent_class($entity)
: get_class($entity);
assert(is_string($class));
}

/**
* @param object{foo: self, bar: int, baz?: string} $entity
*/
public function updateObjectShape($entity): void
{
assertType('class-string', get_class($entity));
assert(property_exists($entity, 'foo'));
assertType('class-string', get_class($entity));
}
}
6 changes: 3 additions & 3 deletions tests/PHPStan/Analyser/data/generics.php
Original file line number Diff line number Diff line change
Expand Up @@ -1301,7 +1301,7 @@ function arrayOfGenericClassStrings(array $a): void
function getClassOnTemplateType($a, $b, $c, $d, $object, $mixed, $tObject)
{
assertType(
'class-string<T (function PHPStan\Generics\FunctionsAssertType\getClassOnTemplateType(), argument)>|false',
'class-string<object&T (function PHPStan\Generics\FunctionsAssertType\getClassOnTemplateType(), argument)>|false',
get_class($a)
);
assertType(
Expand All @@ -1325,8 +1325,8 @@ function getClassOnTemplateType($a, $b, $c, $d, $object, $mixed, $tObject)
get_class($objectB)
);

assertType('class-string', get_class($object));
assertType('class-string|false', get_class($mixed));
assertType('class-string<object>', get_class($object));
assertType('class-string<object>|false', get_class($mixed));
assertType('class-string<W of object (function PHPStan\Generics\FunctionsAssertType\getClassOnTemplateType(), argument)>', get_class($tObject));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1036,4 +1036,11 @@ public function testLooseComparisonAgainstEnumsNoPhpdoc(): void
$this->analyse([__DIR__ . '/data/loose-comparison-against-enums.php'], $issues);
}

public function testBug4890b(): void
{
$this->checkAlwaysTrueCheckTypeFunctionCall = true;
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-4890b.php'], []);
}

}
20 changes: 20 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-4890b.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace Bug4890b;

interface Proxy {}

class HelloWorld
{
public function update(object $entity): void
{
assert(method_exists($entity, 'getId'));

$class = $entity instanceof Proxy
? get_parent_class($entity)
: get_class($entity);
assert(is_string($class));
}
}