Skip to content

Commit f61d90f

Browse files
github-actions[bot]phpstan-bot
authored andcommitted
Fix method_exists() with string literal not narrowing static method calls
- When method_exists() receives a string literal class name (not ClassName::class), the type narrowing was only applied to the string expression, not to the equivalent ClassConstFetch expression that StaticMethodCallCheck uses - Added logic in MethodExistsTypeSpecifyingExtension to also narrow the ClassConstFetch expression when the first argument is a string literal that is a class-string - New regression tests in tests/PHPStan/Rules/Methods/data/bug-9592.php and tests/PHPStan/Analyser/nsrt/bug-9592.php Closes phpstan/phpstan#9592
1 parent 2681e50 commit f61d90f

4 files changed

Lines changed: 85 additions & 1 deletion

File tree

src/Type/Php/MethodExistsTypeSpecifyingExtension.php

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace PHPStan\Type\Php;
44

5+
use PhpParser\Node\Expr;
6+
use PhpParser\Node\Expr\ClassConstFetch;
57
use PhpParser\Node\Expr\FuncCall;
68
use PhpParser\Node\Name\FullyQualified;
79
use PHPStan\Analyser\Scope;
@@ -20,6 +22,7 @@
2022
use PHPStan\Type\ObjectWithoutClassType;
2123
use PHPStan\Type\UnionType;
2224
use function count;
25+
use function ltrim;
2326

2427
#[AutowiredService]
2528
final class MethodExistsTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension
@@ -64,7 +67,7 @@ public function specifyTypes(
6467
$objectType = $scope->getType($args[0]->value);
6568
if ($objectType->isString()->yes()) {
6669
if ($objectType->isClassString()->yes()) {
67-
return $this->typeSpecifier->create(
70+
$result = $this->typeSpecifier->create(
6871
$args[0]->value,
6972
new IntersectionType([
7073
$objectType,
@@ -73,6 +76,31 @@ public function specifyTypes(
7376
$context,
7477
$scope,
7578
);
79+
80+
if (!$args[0]->value instanceof ClassConstFetch) {
81+
foreach ($objectType->getConstantStrings() as $constantString) {
82+
$className = ltrim($constantString->getValue(), '\\');
83+
if ($className === '') {
84+
continue;
85+
}
86+
$classConstFetch = new Expr\ClassConstFetch(
87+
new FullyQualified($className),
88+
'class',
89+
);
90+
$classConstFetchType = $scope->getType($classConstFetch);
91+
$result = $result->unionWith($this->typeSpecifier->create(
92+
$classConstFetch,
93+
new IntersectionType([
94+
$classConstFetchType,
95+
new HasMethodType($methodNameType->getValue()),
96+
]),
97+
$context,
98+
$scope,
99+
));
100+
}
101+
}
102+
103+
return $result;
76104
}
77105

78106
return new SpecifiedTypes([], []);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug9592Nsrt;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class rex_response {}
8+
9+
class HelloWorld
10+
{
11+
public function sayHello(): void
12+
{
13+
if (!method_exists('Bug9592Nsrt\rex_response', 'getNonce')) {
14+
return;
15+
}
16+
// The ClassConstFetch expression gets narrowed via the fix
17+
assertType("'Bug9592Nsrt\\\\rex_response'&hasMethod(getNonce)", rex_response::class);
18+
}
19+
}

tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,13 @@ public function testHasMethodStaticCall(): void
600600
]);
601601
}
602602

603+
public function testBug9592(): void
604+
{
605+
$this->checkThisOnly = false;
606+
$this->checkExplicitMixed = false;
607+
$this->analyse([__DIR__ . '/data/bug-9592.php'], []);
608+
}
609+
603610
public function testBug1267(): void
604611
{
605612
$this->checkThisOnly = false;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug9592;
4+
5+
class rex_response {}
6+
7+
class HelloWorld
8+
{
9+
public function sayHello(): void
10+
{
11+
// method_exists with string literal, then static call
12+
$nonce = method_exists('Bug9592\rex_response', 'getNonce') ? rex_response::getNonce() : '';
13+
}
14+
15+
public function sayHello2(): void
16+
{
17+
if (!method_exists('Bug9592\rex_response', 'getNonce')) {
18+
return;
19+
}
20+
21+
rex_response::getNonce();
22+
}
23+
24+
public function sayHello3(): void
25+
{
26+
if (method_exists('Bug9592\rex_response', 'getNonce')) {
27+
rex_response::getNonce();
28+
}
29+
}
30+
}

0 commit comments

Comments
 (0)