Skip to content

Commit 4f2af3b

Browse files
committed
Enable usage of ReflectionClass::isSubclassOf() with invariant @template T
1 parent 54a5136 commit 4f2af3b

File tree

5 files changed

+164
-15
lines changed

5 files changed

+164
-15
lines changed

phpstan-baseline.neon

-6
Original file line numberDiff line numberDiff line change
@@ -1593,12 +1593,6 @@ parameters:
15931593
count: 2
15941594
path: src/Type/Php/RangeFunctionReturnTypeExtension.php
15951595

1596-
-
1597-
message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#'
1598-
identifier: phpstanApi.instanceofType
1599-
count: 1
1600-
path: src/Type/Php/ReflectionClassIsSubclassOfTypeSpecifyingExtension.php
1601-
16021596
-
16031597
message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#'
16041598
identifier: phpstanApi.instanceofType

src/Type/Php/ReflectionClassIsSubclassOfTypeSpecifyingExtension.php

+31-9
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@
99
use PHPStan\Analyser\TypeSpecifierAwareExtension;
1010
use PHPStan\Analyser\TypeSpecifierContext;
1111
use PHPStan\Reflection\MethodReflection;
12-
use PHPStan\Type\Constant\ConstantStringType;
1312
use PHPStan\Type\Generic\GenericObjectType;
1413
use PHPStan\Type\MethodTypeSpecifyingExtension;
15-
use PHPStan\Type\ObjectType;
14+
use PHPStan\Type\ObjectWithoutClassType;
1615
use ReflectionClass;
1716

1817
final class ReflectionClassIsSubclassOfTypeSpecifyingExtension implements MethodTypeSpecifyingExtension, TypeSpecifierAwareExtension
@@ -34,24 +33,47 @@ public function isMethodSupported(MethodReflection $methodReflection, MethodCall
3433
{
3534
return $methodReflection->getName() === 'isSubclassOf'
3635
&& isset($node->getArgs()[0])
37-
&& $context->true();
36+
&& !$context->null();
3837
}
3938

4039
public function specifyTypes(MethodReflection $methodReflection, MethodCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
4140
{
41+
$calledOnType = $scope->getType($node->var);
42+
$reflectionType = $calledOnType->getTemplateType(ReflectionClass::class, 'T');
43+
if (!(new ObjectWithoutClassType())->isSuperTypeOf($reflectionType)->yes()) {
44+
return new SpecifiedTypes();
45+
}
46+
4247
$valueType = $scope->getType($node->getArgs()[0]->value);
43-
if (!$valueType instanceof ConstantStringType) {
44-
return new SpecifiedTypes([], []);
48+
$objectType = $valueType->getClassStringObjectType();
49+
$narrowingType = new GenericObjectType(ReflectionClass::class, [$objectType]);
50+
51+
if (!$reflectionType->isSuperTypeOf($objectType)->yes()) {
52+
// cause "always false" error
53+
return $this->typeSpecifier->create(
54+
$node->var,
55+
$narrowingType,
56+
$context,
57+
$scope,
58+
);
59+
}
60+
61+
if ($objectType->isSuperTypeOf($reflectionType)->yes()) {
62+
// cause "always true" error
63+
return $this->typeSpecifier->create(
64+
$node->var,
65+
$narrowingType,
66+
$context,
67+
$scope,
68+
);
4569
}
4670

4771
return $this->typeSpecifier->create(
4872
$node->var,
49-
new GenericObjectType(ReflectionClass::class, [
50-
new ObjectType($valueType->getValue()),
51-
]),
73+
$narrowingType,
5274
$context,
5375
$scope,
54-
);
76+
)->setAlwaysOverwriteTypes();
5577
}
5678

5779
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php // lint >= 8.4
2+
3+
namespace Bug12473Types;
4+
5+
use ReflectionClass;
6+
use function PHPStan\Testing\assertType;
7+
8+
class Picture
9+
{
10+
}
11+
12+
class PictureUser extends Picture
13+
{
14+
}
15+
16+
class PictureProduct extends Picture
17+
{
18+
}
19+
20+
/**
21+
* @param class-string $a
22+
*/
23+
function doFoo(string $a): void
24+
{
25+
$r = new ReflectionClass($a);
26+
assertType('ReflectionClass<object>', $r);
27+
if ($r->isSubclassOf(Picture::class)) {
28+
assertType('ReflectionClass<Bug12473Types\\Picture>', $r);
29+
} else {
30+
assertType('ReflectionClass<object>', $r);
31+
}
32+
assertType('ReflectionClass<Bug12473Types\Picture>|ReflectionClass<object>', $r);
33+
}
34+
35+
/**
36+
* @param class-string<Picture> $a
37+
*/
38+
function doFoo2(string $a): void
39+
{
40+
$r = new ReflectionClass($a);
41+
assertType('ReflectionClass<Bug12473Types\\Picture>', $r);
42+
if ($r->isSubclassOf(Picture::class)) {
43+
assertType('ReflectionClass<Bug12473Types\\Picture>', $r);
44+
} else {
45+
assertType('*NEVER*', $r);
46+
}
47+
assertType('ReflectionClass<Bug12473Types\\Picture>', $r);
48+
}

tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php

+23
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,29 @@ public function testReportAlwaysTrueInLastCondition(bool $reportAlwaysTrueInLast
254254
$this->analyse([__DIR__ . '/data/impossible-method-report-always-true-last-condition.php'], $expectedErrors);
255255
}
256256

257+
public function testBug12473(): void
258+
{
259+
$this->treatPhpDocTypesAsCertain = true;
260+
$tip = 'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.';
261+
$this->analyse([__DIR__ . '/data/bug-12473.php'], [
262+
[
263+
'Call to method ReflectionClass<Bug12473\\Picture>::isSubclassOf() with \'Bug12473\\\\Picture\' will always evaluate to true.',
264+
39,
265+
$tip,
266+
],
267+
[
268+
'Call to method ReflectionClass<Bug12473\\PictureUser>::isSubclassOf() with \'Bug12473\\\\PictureProduct\' will always evaluate to false.',
269+
49,
270+
$tip,
271+
],
272+
[
273+
'Call to method ReflectionClass<Bug12473\\PictureUser>::isSubclassOf() with \'Bug12473\\\\PictureUser\' will always evaluate to true.',
274+
59,
275+
$tip,
276+
],
277+
]);
278+
}
279+
257280
public static function getAdditionalConfigFiles(): array
258281
{
259282
return [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
namespace Bug12473;
4+
5+
use ReflectionClass;
6+
7+
class Picture
8+
{
9+
}
10+
11+
class PictureUser extends Picture
12+
{
13+
}
14+
15+
class PictureProduct extends Picture
16+
{
17+
}
18+
19+
function getPictureFqn(string $pictureType): ?string
20+
{
21+
/** @var class-string<Picture|object> $fqn */
22+
$fqn = $pictureType;
23+
if ($fqn === Picture::class) {
24+
return Picture::class;
25+
}
26+
$refl = new \ReflectionClass($fqn);
27+
if (!$refl->isSubclassOf(Picture::class)) {
28+
return null;
29+
}
30+
31+
return $fqn;
32+
}
33+
34+
/**
35+
* @param class-string<Picture> $a
36+
*/
37+
function doFoo(string $a): void {
38+
$r = new ReflectionClass($a);
39+
if ($r->isSubclassOf(Picture::class)) {
40+
41+
}
42+
}
43+
44+
/**
45+
* @param class-string<PictureUser> $a
46+
*/
47+
function doFoo2(string $a): void {
48+
$r = new ReflectionClass($a);
49+
if ($r->isSubclassOf(PictureProduct::class)) {
50+
51+
}
52+
}
53+
54+
/**
55+
* @param class-string<PictureUser> $a
56+
*/
57+
function doFoo3(string $a): void {
58+
$r = new ReflectionClass($a);
59+
if ($r->isSubclassOf(PictureUser::class)) {
60+
61+
}
62+
}

0 commit comments

Comments
 (0)