Skip to content

Commit 5a95e52

Browse files
committed
Merge branch 2.1.x into 2.2.x
2 parents 393a9a4 + eac23f0 commit 5a95e52

6 files changed

Lines changed: 251 additions & 14 deletions

File tree

‎src/Rules/Comparison/ImpossibleCheckTypeHelper.php‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,11 @@ public function findSpecifiedType(
211211

212212
if ($objectType->getObjectClassNames() !== []) {
213213
if ($objectType->hasMethod($methodType->getValue())->yes()) {
214+
foreach ($objectType->getObjectClassReflections() as $classReflection) {
215+
if (!$classReflection->hasNativeMethod($methodType->getValue())) {
216+
return null;
217+
}
218+
}
214219
return true;
215220
}
216221

@@ -231,7 +236,15 @@ public function findSpecifiedType(
231236

232237
if ($genericType instanceof TypeWithClassName) {
233238
if ($genericType->hasMethod($methodType->getValue())->yes()) {
234-
return true;
239+
$classReflection = $genericType->getClassReflection();
240+
if (
241+
$classReflection !== null
242+
&& $classReflection->hasNativeMethod($methodType->getValue())
243+
) {
244+
return true;
245+
}
246+
247+
return null;
235248
}
236249

237250
$classReflection = $genericType->getClassReflection();

‎src/Type/Php/MethodExistsTypeSpecifyingExtension.php‎

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,18 @@ public function specifyTypes(
5353
$args = $node->getArgs();
5454
$methodNameType = $scope->getType($args[1]->value);
5555
if (!$methodNameType instanceof ConstantStringType) {
56-
return $this->typeSpecifier->create(
57-
new FuncCall(new FullyQualified('method_exists'), $node->getRawArgs()),
58-
new ConstantBooleanType(true),
59-
$context,
60-
$scope,
61-
);
56+
return $this->createFuncCallSpec($node, $context, $scope);
6257
}
6358

6459
$objectType = $scope->getType($args[0]->value);
6560
if ($objectType->isString()->yes()) {
6661
if ($objectType->isClassString()->yes()) {
62+
foreach ($objectType->getClassStringObjectType()->getObjectClassReflections() as $classReflection) {
63+
if ($classReflection->hasMethod($methodNameType->getValue()) && !$classReflection->hasNativeMethod($methodNameType->getValue())) {
64+
return $this->createFuncCallSpec($node, $context, $scope);
65+
}
66+
}
67+
6768
return $this->typeSpecifier->create(
6869
$args[0]->value,
6970
new IntersectionType([
@@ -78,6 +79,12 @@ public function specifyTypes(
7879
return new SpecifiedTypes([], []);
7980
}
8081

82+
foreach ($objectType->getObjectClassReflections() as $classReflection) {
83+
if ($classReflection->hasMethod($methodNameType->getValue()) && !$classReflection->hasNativeMethod($methodNameType->getValue())) {
84+
return $this->createFuncCallSpec($node, $context, $scope);
85+
}
86+
}
87+
8188
return $this->typeSpecifier->create(
8289
$args[0]->value,
8390
new UnionType([
@@ -92,4 +99,14 @@ public function specifyTypes(
9299
);
93100
}
94101

102+
private function createFuncCallSpec(FuncCall $node, TypeSpecifierContext $context, Scope $scope): SpecifiedTypes
103+
{
104+
return $this->typeSpecifier->create(
105+
new FuncCall(new FullyQualified('method_exists'), $node->getRawArgs()),
106+
new ConstantBooleanType(true),
107+
$context,
108+
$scope,
109+
);
110+
}
111+
95112
}

‎src/Type/Php/PropertyExistsTypeSpecifyingExtension.php‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,7 @@ public function specifyTypes(
5858
$args = $node->getArgs();
5959
$propertyNameType = $scope->getType($args[1]->value);
6060
if (!$propertyNameType instanceof ConstantStringType) {
61-
return $this->typeSpecifier->create(
62-
new FuncCall(new FullyQualified('property_exists'), $node->getRawArgs()),
63-
new ConstantBooleanType(true),
64-
$context,
65-
$scope,
66-
);
61+
return $this->createFuncCallSpec($node, $context, $scope);
6762
}
6863

6964
if ($propertyNameType->getValue() === '') {
@@ -85,7 +80,7 @@ public function specifyTypes(
8580
$propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($propertyNode, $scope);
8681
if ($propertyReflection !== null) {
8782
if (!$propertyReflection->isNative()) {
88-
return new SpecifiedTypes([], []);
83+
return $this->createFuncCallSpec($node, $context, $scope);
8984
}
9085
}
9186

@@ -100,4 +95,14 @@ public function specifyTypes(
10095
);
10196
}
10297

98+
private function createFuncCallSpec(FuncCall $node, TypeSpecifierContext $context, Scope $scope): SpecifiedTypes
99+
{
100+
return $this->typeSpecifier->create(
101+
new FuncCall(new FullyQualified('property_exists'), $node->getRawArgs()),
102+
new ConstantBooleanType(true),
103+
$context,
104+
$scope,
105+
);
106+
}
107+
103108
}

‎tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,25 @@ public function testBug6822(): void
253253
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-6822.php'], []);
254254
}
255255

256+
public function testBug6211(): void
257+
{
258+
$this->treatPhpDocTypesAsCertain = true;
259+
$this->analyse([__DIR__ . '/data/bug-6211.php'], [
260+
[
261+
'If condition is always true.',
262+
93,
263+
],
264+
[
265+
'If condition is always true.',
266+
100,
267+
],
268+
[
269+
'If condition is always true.',
270+
114,
271+
],
272+
]);
273+
}
274+
256275
public function testBug5020(): void
257276
{
258277
$this->treatPhpDocTypesAsCertain = true;

‎tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1292,4 +1292,48 @@ public function testBug8217(): void
12921292
$this->analyse([__DIR__ . '/data/bug-8217.php'], []);
12931293
}
12941294

1295+
public function testBug6211(): void
1296+
{
1297+
$this->treatPhpDocTypesAsCertain = true;
1298+
$this->analyse([__DIR__ . '/data/bug-6211.php'], [
1299+
[
1300+
'Call to function method_exists() with Bug6211\Hell and \'test\' will always evaluate to true.',
1301+
34,
1302+
],
1303+
[
1304+
'Call to function method_exists() with \'Bug6211\\\\Hell\' and \'test\' will always evaluate to true.',
1305+
39,
1306+
],
1307+
[
1308+
'Call to function method_exists() with Bug6211\Bar and \'realMethod\' will always evaluate to true.',
1309+
62,
1310+
],
1311+
[
1312+
'Call to function property_exists() with Bug6211\Baz and \'realProp\' will always evaluate to true.',
1313+
87,
1314+
],
1315+
[
1316+
'Call to function method_exists() with Bug6211\Hell and \'test\' will always evaluate to true.',
1317+
106,
1318+
],
1319+
[
1320+
'Call to function method_exists() with Bug6211\Hell and \'test\' will always evaluate to true.',
1321+
107,
1322+
],
1323+
[
1324+
'Call to function property_exists() with Bug6211\Baz and \'realProp\' will always evaluate to true.',
1325+
120,
1326+
],
1327+
[
1328+
'Call to function property_exists() with Bug6211\Baz and \'realProp\' will always evaluate to true.',
1329+
121,
1330+
],
1331+
[
1332+
'Call to function method_exists() with class-string<Bug6211\Foo> and \'test\' will always evaluate to true.',
1333+
136,
1334+
'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%</>.',
1335+
],
1336+
]);
1337+
}
1338+
12951339
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug6211;
4+
5+
/**
6+
* @method bool isTrue()
7+
*/
8+
interface Foo
9+
{
10+
public function test(): bool;
11+
}
12+
13+
class Hell implements Foo
14+
{
15+
public function test(): bool
16+
{
17+
return true;
18+
}
19+
}
20+
21+
$hell = new Hell();
22+
23+
// @method should not make method_exists always true
24+
if (\method_exists($hell, 'isTrue')) {
25+
26+
}
27+
28+
// @method with class string should not make method_exists always true
29+
if (\method_exists(Hell::class, 'isTrue')) {
30+
31+
}
32+
33+
// native method should still be always true
34+
if (\method_exists($hell, 'test')) {
35+
36+
}
37+
38+
// native method via class string should still be always true
39+
if (\method_exists(Hell::class, 'test')) {
40+
41+
}
42+
43+
/**
44+
* @method bool magicMethod()
45+
*/
46+
class Bar
47+
{
48+
public function realMethod(): bool
49+
{
50+
return true;
51+
}
52+
}
53+
54+
$bar = new Bar();
55+
56+
// @method on the class itself (not interface) should not make method_exists always true
57+
if (\method_exists($bar, 'magicMethod')) {
58+
59+
}
60+
61+
// native method should still be always true
62+
if (\method_exists($bar, 'realMethod')) {
63+
64+
}
65+
66+
/**
67+
* @property int $magicProp
68+
*/
69+
class Baz
70+
{
71+
public int $realProp = 1;
72+
73+
public function __get(string $name): mixed
74+
{
75+
return null;
76+
}
77+
}
78+
79+
$baz = new Baz();
80+
81+
// @property should not make property_exists always true
82+
if (\property_exists($baz, 'magicProp')) {
83+
84+
}
85+
86+
// native property should still be always true
87+
if (\property_exists($baz, 'realProp')) {
88+
89+
}
90+
91+
// Nested method_exists with @method should report the inner as always-true
92+
if (\method_exists($hell, 'isTrue')) {
93+
if (\method_exists($hell, 'isTrue')) { // if condition always true
94+
95+
}
96+
}
97+
98+
// Nested method_exists with @method via class-string
99+
if (\method_exists(Hell::class, 'isTrue')) {
100+
if (\method_exists(Hell::class, 'isTrue')) { // if condition always true
101+
102+
}
103+
}
104+
105+
// Nested method_exists with native method (already always-true, inner is also)
106+
if (\method_exists($hell, 'test')) {
107+
if (\method_exists($hell, 'test')) {
108+
109+
}
110+
}
111+
112+
// Nested property_exists with @property should report the inner as always-true
113+
if (\property_exists($baz, 'magicProp')) {
114+
if (\property_exists($baz, 'magicProp')) { // if condition always true
115+
116+
}
117+
}
118+
119+
// Nested property_exists with native property (already always-true, inner is also)
120+
if (\property_exists($baz, 'realProp')) {
121+
if (\property_exists($baz, 'realProp')) {
122+
123+
}
124+
}
125+
126+
/**
127+
* @param class-string<Foo> $classString
128+
*/
129+
function testGenericClassString(string $classString): void {
130+
// @method via generic class-string should not make method_exists always true
131+
if (\method_exists($classString, 'isTrue')) {
132+
133+
}
134+
135+
// native method via generic class-string should still be always true
136+
if (\method_exists($classString, 'test')) {
137+
138+
}
139+
}

0 commit comments

Comments
 (0)