Skip to content

Commit 0711bec

Browse files
Fix ImpossibleCheckTypeFunctionCallRule for is_subclass_of and is_a
1 parent 65be2b2 commit 0711bec

6 files changed

+168
-9
lines changed

src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php

-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
use PHPStan\Rules\Rule;
99
use PHPStan\Rules\RuleErrorBuilder;
1010
use function sprintf;
11-
use function strtolower;
1211

1312
/**
1413
* @implements Rule<Node\Expr\FuncCall>
@@ -38,9 +37,6 @@ public function processNode(Node $node, Scope $scope): array
3837
}
3938

4039
$functionName = (string) $node->name;
41-
if (strtolower($functionName) === 'is_a') {
42-
return [];
43-
}
4440
$isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $node);
4541
if ($isAlways === null) {
4642
return [];

src/Type/Php/IsAFunctionTypeSpecifyingExtension.php

+15-1
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99
use PHPStan\Analyser\TypeSpecifierAwareExtension;
1010
use PHPStan\Analyser\TypeSpecifierContext;
1111
use PHPStan\Reflection\FunctionReflection;
12+
use PHPStan\Type\ClassStringType;
1213
use PHPStan\Type\Constant\ConstantBooleanType;
1314
use PHPStan\Type\Constant\ConstantStringType;
1415
use PHPStan\Type\FunctionTypeSpecifyingExtension;
16+
use PHPStan\Type\ObjectWithoutClassType;
17+
use PHPStan\Type\TypeCombinator;
1518
use function count;
1619
use function strtolower;
1720

@@ -47,9 +50,20 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n
4750
$allowStringType = isset($node->getArgs()[2]) ? $scope->getType($node->getArgs()[2]->value) : new ConstantBooleanType(false);
4851
$allowString = !$allowStringType->equals(new ConstantBooleanType(false));
4952

53+
$superType = $allowString
54+
? TypeCombinator::union(new ObjectWithoutClassType(), new ClassStringType())
55+
: new ObjectWithoutClassType();
56+
57+
$resultType = $this->isAFunctionTypeSpecifyingHelper->determineType($objectOrClassType, $classType, $allowString, true);
58+
59+
// prevent false-positives in IsAFunctionTypeSpecifyingHelper
60+
if ($resultType->equals($superType) && $resultType->isSuperTypeOf($objectOrClassType)->yes()) {
61+
return new SpecifiedTypes([], []);
62+
}
63+
5064
return $this->typeSpecifier->create(
5165
$node->getArgs()[0]->value,
52-
$this->isAFunctionTypeSpecifyingHelper->determineType($objectOrClassType, $classType, $allowString, true),
66+
$resultType,
5367
$context,
5468
false,
5569
$scope,

src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php

+15-1
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99
use PHPStan\Analyser\TypeSpecifierAwareExtension;
1010
use PHPStan\Analyser\TypeSpecifierContext;
1111
use PHPStan\Reflection\FunctionReflection;
12+
use PHPStan\Type\ClassStringType;
1213
use PHPStan\Type\Constant\ConstantBooleanType;
1314
use PHPStan\Type\FunctionTypeSpecifyingExtension;
1415
use PHPStan\Type\Generic\GenericClassStringType;
16+
use PHPStan\Type\ObjectWithoutClassType;
17+
use PHPStan\Type\TypeCombinator;
1518
use function count;
1619
use function strtolower;
1720

@@ -48,9 +51,20 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n
4851
return new SpecifiedTypes([], []);
4952
}
5053

54+
$superType = $allowString
55+
? TypeCombinator::union(new ObjectWithoutClassType(), new ClassStringType())
56+
: new ObjectWithoutClassType();
57+
58+
$resultType = $this->isAFunctionTypeSpecifyingHelper->determineType($objectOrClassType, $classType, $allowString, false);
59+
60+
// prevent false-positives in IsAFunctionTypeSpecifyingHelper
61+
if ($resultType->equals($superType) && $resultType->isSuperTypeOf($objectOrClassType)->yes()) {
62+
return new SpecifiedTypes([], []);
63+
}
64+
5165
return $this->typeSpecifier->create(
5266
$node->getArgs()[0]->value,
53-
$this->isAFunctionTypeSpecifyingHelper->determineType($objectOrClassType, $classType, $allowString, false),
67+
$resultType,
5468
$context,
5569
false,
5670
$scope,

tests/PHPStan/Analyser/TypeSpecifierTest.php

+1-3
Original file line numberDiff line numberDiff line change
@@ -1133,9 +1133,7 @@ public function dataCondition(): iterable
11331133
new Arg(new Variable('stringOrNull')),
11341134
new Arg(new Expr\ConstFetch(new Name('false'))),
11351135
]),
1136-
[
1137-
'$object' => 'object',
1138-
],
1136+
[],
11391137
[],
11401138
],
11411139
[

tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php

+7
Original file line numberDiff line numberDiff line change
@@ -1102,4 +1102,11 @@ public function testAlwaysTruePregMatch(): void
11021102
$this->analyse([__DIR__ . '/data/always-true-preg-match.php'], []);
11031103
}
11041104

1105+
public function testBug3979(): void
1106+
{
1107+
$this->checkAlwaysTrueCheckTypeFunctionCall = true;
1108+
$this->treatPhpDocTypesAsCertain = true;
1109+
$this->analyse([__DIR__ . '/data/bug-3979.php'], []);
1110+
}
1111+
11051112
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?php
2+
3+
namespace Bug3979;
4+
5+
class A { }
6+
class B extends A { }
7+
class C { }
8+
9+
/**
10+
* @param mixed $value
11+
* @param string $class_type
12+
*/
13+
function check_class($value, $class_type): bool
14+
{
15+
if (!is_string($value) || !class_exists($value) ||
16+
($class_type && !is_subclass_of($value, $class_type)))
17+
return false;
18+
return true;
19+
}
20+
21+
var_dump(check_class("B", "A")); // true
22+
var_dump(check_class("C", "A")); // false
23+
24+
/**
25+
* @param class-string $value
26+
* @param string $class_type
27+
*/
28+
function check_class2($value, $class_type): bool
29+
{
30+
if (is_a($value, $class_type, true)) {
31+
return true;
32+
}
33+
return false;
34+
}
35+
36+
/**
37+
* @param class-string|object $value
38+
* @param string $class_type
39+
*/
40+
function check_class3($value, $class_type): bool
41+
{
42+
if (is_a($value, $class_type, true)) {
43+
return true;
44+
}
45+
return false;
46+
}
47+
48+
/**
49+
* @param class-string|object $value
50+
* @param string $class_type
51+
*/
52+
function check_class4($value, $class_type): bool
53+
{
54+
if (is_subclass_of($value, $class_type, true)) {
55+
return true;
56+
}
57+
return false;
58+
}
59+
60+
/**
61+
* @param object $value
62+
* @param string $class_type
63+
*/
64+
function check_class5($value, $class_type): bool
65+
{
66+
if (is_a($value, $class_type, true)) {
67+
return true;
68+
}
69+
return false;
70+
}
71+
72+
/**
73+
* @param object $value
74+
* @param string $class_type
75+
*/
76+
function check_class6($value, $class_type): bool
77+
{
78+
if (is_subclass_of($value, $class_type, true)) {
79+
return true;
80+
}
81+
return false;
82+
}
83+
84+
/**
85+
* @param object $value
86+
* @param class-string $class_type
87+
*/
88+
function check_class7($value, $class_type): bool
89+
{
90+
if (is_a($value, $class_type, true)) {
91+
return true;
92+
}
93+
return false;
94+
}
95+
96+
/**
97+
* @param object $value
98+
* @param class-string $class_type
99+
*/
100+
function check_class8($value, $class_type): bool
101+
{
102+
if (is_subclass_of($value, $class_type, true)) {
103+
return true;
104+
}
105+
return false;
106+
}
107+
108+
/**
109+
* @param class-string $value
110+
* @param class-string $class_type
111+
*/
112+
function check_class9($value, $class_type): bool
113+
{
114+
if (is_a($value, $class_type, true)) {
115+
return true;
116+
}
117+
return false;
118+
}
119+
120+
/**
121+
* @param class-string $value
122+
* @param class-string $class_type
123+
*/
124+
function check_class10($value, $class_type): bool
125+
{
126+
if (is_subclass_of($value, $class_type, true)) {
127+
return true;
128+
}
129+
return false;
130+
}

0 commit comments

Comments
 (0)