Skip to content

Commit 6803e54

Browse files
authored
Merge branch refs/heads/2.1.x into 2.2.x
2 parents 79eb04f + d0b7d5e commit 6803e54

File tree

8 files changed

+121
-24
lines changed

8 files changed

+121
-24
lines changed

phpstan-baseline.neon

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1569,12 +1569,6 @@ parameters:
15691569
count: 1
15701570
path: src/Type/Php/IsAFunctionTypeSpecifyingExtension.php
15711571

1572-
-
1573-
rawMessage: 'Doing instanceof PHPStan\Type\Generic\GenericClassStringType is error-prone and deprecated. Use Type::isClassStringType() and Type::getClassStringObjectType() instead.'
1574-
identifier: phpstanApi.instanceofType
1575-
count: 2
1576-
path: src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php
1577-
15781572
-
15791573
rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.'
15801574
identifier: phpstanApi.instanceofType

src/Type/Php/IsAFunctionTypeSpecifyingExtension.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n
5151
$allowString = !$allowStringType->equals(new ConstantBooleanType(false));
5252

5353
$resultType = $this->isAFunctionTypeSpecifyingHelper->determineType($objectOrClassType, $classType, $allowString, true);
54-
55-
// prevent false-positives in IsAFunctionTypeSpecifyingHelper
56-
if ($classType->getConstantStrings() === [] && $resultType->isSuperTypeOf($objectOrClassType)->yes()) {
54+
if ($resultType === null) {
5755
return new SpecifiedTypes([], []);
5856
}
5957

src/Type/Php/IsAFunctionTypeSpecifyingHelper.php

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use PHPStan\Type\UnionType;
1717
use function array_unique;
1818
use function array_values;
19+
use function in_array;
1920

2021
#[AutowiredService]
2122
final class IsAFunctionTypeSpecifyingHelper
@@ -26,7 +27,7 @@ public function determineType(
2627
Type $classType,
2728
bool $allowString,
2829
bool $allowSameClass,
29-
): Type
30+
): ?Type
3031
{
3132
$objectOrClassTypeClassNames = $objectOrClassType->getObjectClassNames();
3233
if ($allowString) {
@@ -36,15 +37,39 @@ public function determineType(
3637
$objectOrClassTypeClassNames = array_values(array_unique($objectOrClassTypeClassNames));
3738
}
3839

39-
return TypeTraverser::map(
40+
$isUncertain = $classType->getConstantStrings() === [];
41+
42+
$resultType = TypeTraverser::map(
4043
$classType,
41-
static function (Type $type, callable $traverse) use ($objectOrClassTypeClassNames, $allowString, $allowSameClass): Type {
44+
static function (Type $type, callable $traverse) use ($objectOrClassType, $objectOrClassTypeClassNames, $allowString, $allowSameClass, &$isUncertain): Type {
4245
if ($type instanceof UnionType || $type instanceof IntersectionType) {
4346
return $traverse($type);
4447
}
4548
if ($type instanceof ConstantStringType) {
46-
if (!$allowSameClass && $objectOrClassTypeClassNames === [$type->getValue()]) {
47-
return new NeverType();
49+
if (!$allowSameClass) {
50+
if ($objectOrClassTypeClassNames === [$type->getValue()]) {
51+
$isSameClass = true;
52+
foreach ($objectOrClassType->getObjectClassReflections() as $classReflection) {
53+
if (!$classReflection->isFinal()) {
54+
$isSameClass = false;
55+
break;
56+
}
57+
}
58+
59+
if ($isSameClass) {
60+
return new NeverType();
61+
}
62+
}
63+
64+
if (
65+
// For object, as soon as the exact same type is provided
66+
// in the list we cannot be sure of the result
67+
in_array($type->getValue(), $objectOrClassTypeClassNames, true)
68+
// This also occurs for generic class string
69+
|| ($allowString && $objectOrClassTypeClassNames === [] && $objectOrClassType->isSuperTypeOf($type)->yes())
70+
) {
71+
$isUncertain = true;
72+
}
4873
}
4974
if ($allowString) {
5075
return new UnionType([
@@ -75,6 +100,13 @@ static function (Type $type, callable $traverse) use ($objectOrClassTypeClassNam
75100
return new ObjectWithoutClassType();
76101
},
77102
);
103+
104+
// prevent false-positives
105+
if ($isUncertain && $resultType->isSuperTypeOf($objectOrClassType)->yes()) {
106+
return null;
107+
}
108+
109+
return $resultType;
78110
}
79111

80112
}

src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
use PHPStan\Reflection\FunctionReflection;
1313
use PHPStan\Type\Constant\ConstantBooleanType;
1414
use PHPStan\Type\FunctionTypeSpecifyingExtension;
15-
use PHPStan\Type\Generic\GenericClassStringType;
1615
use function count;
1716
use function strtolower;
1817

@@ -46,15 +45,8 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n
4645
$allowStringType = isset($args[2]) ? $scope->getType($args[2]->value) : new ConstantBooleanType(true);
4746
$allowString = !$allowStringType->equals(new ConstantBooleanType(false));
4847

49-
// prevent false-positives in IsAFunctionTypeSpecifyingHelper
50-
if ($objectOrClassType instanceof GenericClassStringType && $classType instanceof GenericClassStringType) {
51-
return new SpecifiedTypes([], []);
52-
}
53-
5448
$resultType = $this->isAFunctionTypeSpecifyingHelper->determineType($objectOrClassType, $classType, $allowString, false);
55-
56-
// prevent false-positives in IsAFunctionTypeSpecifyingHelper
57-
if ($classType->getConstantStrings() === [] && $resultType->isSuperTypeOf($objectOrClassType)->yes()) {
49+
if ($resultType === null) {
5850
return new SpecifiedTypes([], []);
5951
}
6052

tests/PHPStan/Analyser/nsrt/is-subclass-of.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
function (Bar $a, Bar $b, Bar $c, Bar $d) {
66
if (is_subclass_of($a, Bar::class)) {
7-
\PHPStan\Testing\assertType('*NEVER*', $a);
7+
\PHPStan\Testing\assertType('IsSubclassOf\Bar', $a); // Can still be a Bar child
88
}
99

1010
if (is_subclass_of($b, Foo::class)) {
@@ -53,3 +53,11 @@ function (string $a, string $b, string $c, string $d) {
5353
class Foo {}
5454

5555
class Bar extends Foo {}
56+
57+
final class FinalFoo {}
58+
59+
function (FinalFoo $a) {
60+
if (is_subclass_of($a, FinalFoo::class)) {
61+
\PHPStan\Testing\assertType('*NEVER*', $a);
62+
}
63+
};

tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,28 @@ public function testBug6305(): void
400400
]);
401401
}
402402

403+
public function testBug6305b(): void
404+
{
405+
$this->treatPhpDocTypesAsCertain = true;
406+
$this->analyse([__DIR__ . '/data/bug-6305b.php'], []);
407+
}
408+
409+
public function testBug13713(): void
410+
{
411+
$this->treatPhpDocTypesAsCertain = true;
412+
$this->analyse([__DIR__ . '/data/bug-13713.php'], [
413+
[
414+
"Call to function is_subclass_of() with arguments Bug13713\\test, 'stdClass' and false will always evaluate to true.",
415+
12,
416+
],
417+
[
418+
"Call to function is_subclass_of() with arguments class-string<Bug13713\\test>, 'stdClass' and true will always evaluate to true.",
419+
25,
420+
'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%</>.',
421+
],
422+
]);
423+
}
424+
403425
public function testBug6698(): void
404426
{
405427
$this->treatPhpDocTypesAsCertain = true;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug13713;
6+
7+
function debug(object $object): void {
8+
if ($object instanceof \stdClass) {
9+
echo var_export(\is_subclass_of($object, \stdClass::class, false), true) . \PHP_EOL;
10+
}
11+
if ($object instanceof test) {
12+
echo var_export(\is_subclass_of($object, \stdClass::class, false), true) . \PHP_EOL;
13+
}
14+
}
15+
16+
class test extends \stdClass {}
17+
debug(new test);
18+
19+
/**
20+
* @param class-string<\stdClass> $stdClass
21+
* @param class-string<test> $test
22+
*/
23+
function debugWithClass(string $stdClass, string $test): void {
24+
echo var_export(\is_subclass_of($stdClass, \stdClass::class, true), true) . \PHP_EOL;
25+
echo var_export(\is_subclass_of($test, \stdClass::class, true), true) . \PHP_EOL;
26+
}
27+
28+
debugWithClass(test::class, test::class);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace Bug6305b;
4+
5+
class A {}
6+
7+
class B extends A {}
8+
9+
$b = mt_rand(0, 1) === 0 ? new B() : new A();
10+
11+
if (is_subclass_of($b, A::class)) {
12+
}
13+
14+
if (is_subclass_of($b, B::class)) {
15+
}
16+
17+
$b = mt_rand(0, 1) === 0 ? A::class : B::class;
18+
19+
if (is_subclass_of($b, A::class)) {
20+
}
21+
22+
if (is_subclass_of($b, B::class)) {
23+
}

0 commit comments

Comments
 (0)