Skip to content

Commit af78744

Browse files
phpstan-botVincentLanglet
authored andcommitted
Do not add HasMethodType to class-string types when the method is native
- In `MethodExistsTypeSpecifyingExtension`, when narrowing class-string types after `method_exists()`, use `createFuncCallSpec` instead of adding `HasMethodType` when all class reflections have the method natively. - Previously, adding `HasMethodType` to the class-string type changed its representation from `GenericClassStringType` to an `IntersectionType`, which caused `StaticMethodCallCheck` to skip visibility checking via two paths: the early return at line 72-74 for Name nodes, and the `isString()->yes()` bail-out at line 203-205 for expression nodes. - With this fix, the class-string type is preserved, and the normal visibility checking flow in `StaticMethodCallCheck` correctly reports calls to private/protected static methods even when guarded by `method_exists()`. Closes phpstan/phpstan#14684
1 parent e71a222 commit af78744

3 files changed

Lines changed: 110 additions & 1 deletion

File tree

src/Type/Php/MethodExistsTypeSpecifyingExtension.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,21 @@ public function specifyTypes(
5959
$objectOrStringType = $scope->getType($args[0]->value);
6060
if ($objectOrStringType->isString()->yes()) {
6161
if ($objectOrStringType->isClassString()->yes()) {
62-
foreach ($objectOrStringType->getClassStringObjectType()->getObjectClassReflections() as $classReflection) {
62+
$allNative = true;
63+
$classReflections = $objectOrStringType->getClassStringObjectType()->getObjectClassReflections();
64+
foreach ($classReflections as $classReflection) {
6365
if ($classReflection->hasMethod($methodNameType->getValue()) && !$classReflection->hasNativeMethod($methodNameType->getValue())) {
6466
return $this->createFuncCallSpec($node, $context, $scope);
6567
}
68+
if ($classReflection->hasNativeMethod($methodNameType->getValue())) {
69+
continue;
70+
}
71+
72+
$allNative = false;
73+
}
74+
75+
if ($allNative && $classReflections !== []) {
76+
return $this->createFuncCallSpec($node, $context, $scope);
6677
}
6778

6879
return $this->typeSpecifier->create(

tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,4 +1033,40 @@ public function testBug14596(): void
10331033
]);
10341034
}
10351035

1036+
public function testBug14684(): void
1037+
{
1038+
$this->checkThisOnly = false;
1039+
$this->checkExplicitMixed = false;
1040+
$this->analyse([__DIR__ . '/data/bug-14684.php'], [
1041+
[
1042+
'Call to private static method privateFoo() of class Bug14684\X.',
1043+
25,
1044+
],
1045+
[
1046+
'Call to protected static method protectedFoo() of class Bug14684\X.',
1047+
29,
1048+
],
1049+
[
1050+
'Call to private static method privateFoo() of class Bug14684\SubX.',
1051+
41,
1052+
],
1053+
[
1054+
'Call to protected static method protectedFoo() of class Bug14684\X.',
1055+
45,
1056+
],
1057+
[
1058+
'Call to private static method privateFoo() of class Bug14684\X.',
1059+
52,
1060+
],
1061+
[
1062+
'Call to protected static method protectedFoo() of class Bug14684\X.',
1063+
56,
1064+
],
1065+
[
1066+
'Call to private static method privateFoo() of class Bug14684\SubX.',
1067+
60,
1068+
],
1069+
]);
1070+
}
1071+
10361072
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14684;
4+
5+
class X {
6+
static public function publicFoo():void {}
7+
8+
final static private function privateFoo():void {}
9+
10+
static protected function protectedFoo():void {}
11+
}
12+
13+
final class SubX extends X {
14+
static private function privateFoo():void {}
15+
}
16+
17+
/** @param class-string<X> $row */
18+
function testClassStringFinalMethod(string $row): void
19+
{
20+
if (method_exists($row, 'publicFoo')) {
21+
$row::publicFoo();
22+
}
23+
24+
if (method_exists($row, 'privateFoo')) {
25+
$row::privateFoo();
26+
}
27+
28+
if (method_exists($row, 'protectedFoo')) {
29+
$row::protectedFoo();
30+
}
31+
}
32+
33+
/** @param class-string<SubX> $row */
34+
function testClassStringFinalClass(string $row): void
35+
{
36+
if (method_exists($row, 'publicFoo')) {
37+
$row::publicFoo();
38+
}
39+
40+
if (method_exists($row, 'privateFoo')) {
41+
$row::privateFoo();
42+
}
43+
44+
if (method_exists($row, 'protectedFoo')) {
45+
$row::protectedFoo();
46+
}
47+
}
48+
49+
function testLiteralClassCall(): void
50+
{
51+
if (method_exists(X::class, 'privateFoo')) {
52+
X::privateFoo();
53+
}
54+
55+
if (method_exists(X::class, 'protectedFoo')) {
56+
X::protectedFoo();
57+
}
58+
59+
if (method_exists(SubX::class, 'privateFoo')) {
60+
SubX::privateFoo();
61+
}
62+
}

0 commit comments

Comments
 (0)