diff --git a/src/Rules/Methods/StaticMethodCallCheck.php b/src/Rules/Methods/StaticMethodCallCheck.php index aaabe3d1e7..b3da13562a 100644 --- a/src/Rules/Methods/StaticMethodCallCheck.php +++ b/src/Rules/Methods/StaticMethodCallCheck.php @@ -26,6 +26,7 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\ObjectType; use PHPStan\Type\StaticType; use PHPStan\Type\StringType; use PHPStan\Type\Type; @@ -200,6 +201,18 @@ public function check( if (!$classType->isObject()->yes()) { return [[], null]; } + } elseif ($classType->isClassString()->yes()) { + $nativeMethodTypes = []; + foreach ($classType->getClassStringObjectType()->getObjectClassReflections() as $classReflection) { + if ($classReflection->hasNativeMethod($methodName)) { + $nativeMethodTypes[] = new ObjectType($classReflection->getName()); + } + } + if ($nativeMethodTypes !== []) { + $classType = TypeCombinator::union(...$nativeMethodTypes); + } else { + return [[], null]; + } } elseif ($classType->isString()->yes()) { return [[], null]; } diff --git a/src/Type/Php/MethodExistsTypeSpecifyingExtension.php b/src/Type/Php/MethodExistsTypeSpecifyingExtension.php index 29621c1342..f66db635ea 100644 --- a/src/Type/Php/MethodExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/MethodExistsTypeSpecifyingExtension.php @@ -59,10 +59,21 @@ public function specifyTypes( $objectOrStringType = $scope->getType($args[0]->value); if ($objectOrStringType->isString()->yes()) { if ($objectOrStringType->isClassString()->yes()) { - foreach ($objectOrStringType->getClassStringObjectType()->getObjectClassReflections() as $classReflection) { + $allNative = true; + $classReflections = $objectOrStringType->getClassStringObjectType()->getObjectClassReflections(); + foreach ($classReflections as $classReflection) { if ($classReflection->hasMethod($methodNameType->getValue()) && !$classReflection->hasNativeMethod($methodNameType->getValue())) { return $this->createFuncCallSpec($node, $context, $scope); } + if ($classReflection->hasNativeMethod($methodNameType->getValue())) { + continue; + } + + $allNative = false; + } + + if ($allNative && $classReflections !== []) { + return $this->createFuncCallSpec($node, $context, $scope); } return $this->typeSpecifier->create( diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index 47a70c9d02..5b4255282a 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -1033,4 +1033,48 @@ public function testBug14596(): void ]); } + public function testBug14684(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/bug-14684.php'], [ + [ + 'Call to private static method privateFoo() of class Bug14684\X.', + 25, + ], + [ + 'Call to protected static method protectedFoo() of class Bug14684\X.', + 29, + ], + [ + 'Call to private static method privateFoo() of class Bug14684\SubX.', + 41, + ], + [ + 'Call to protected static method protectedFoo() of class Bug14684\X.', + 45, + ], + [ + 'Call to private static method privateFoo() of class Bug14684\X.', + 52, + ], + [ + 'Call to protected static method protectedFoo() of class Bug14684\X.', + 56, + ], + [ + 'Call to private static method privateFoo() of class Bug14684\SubX.', + 60, + ], + [ + 'Call to private static method privateFoo() of class Bug14684\X.', + 71, + ], + [ + 'Call to protected static method protectedFoo() of class Bug14684\X.', + 75, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-14684.php b/tests/PHPStan/Rules/Methods/data/bug-14684.php new file mode 100644 index 0000000000..720bd5ce6e --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-14684.php @@ -0,0 +1,81 @@ + $row */ +function testClassStringFinalMethod(string $row): void +{ + if (method_exists($row, 'publicFoo')) { + $row::publicFoo(); + } + + if (method_exists($row, 'privateFoo')) { + $row::privateFoo(); + } + + if (method_exists($row, 'protectedFoo')) { + $row::protectedFoo(); + } +} + +/** @param class-string $row */ +function testClassStringFinalClass(string $row): void +{ + if (method_exists($row, 'publicFoo')) { + $row::publicFoo(); + } + + if (method_exists($row, 'privateFoo')) { + $row::privateFoo(); + } + + if (method_exists($row, 'protectedFoo')) { + $row::protectedFoo(); + } +} + +function testLiteralClassCall(): void +{ + if (method_exists(X::class, 'privateFoo')) { + X::privateFoo(); + } + + if (method_exists(X::class, 'protectedFoo')) { + X::protectedFoo(); + } + + if (method_exists(SubX::class, 'privateFoo')) { + SubX::privateFoo(); + } +} + +class Y { +} + +/** @param class-string $row */ +function testClassStringUnion(string $row): void +{ + if (method_exists($row, 'privateFoo')) { + $row::privateFoo(); + } + + if (method_exists($row, 'protectedFoo')) { + $row::protectedFoo(); + } + + if (method_exists($row, 'publicFoo')) { + $row::publicFoo(); + } +}