Do not add HasMethodType to class-string types when the method is native#5738
Do not add HasMethodType to class-string types when the method is native#5738phpstan-bot wants to merge 2 commits into
HasMethodType to class-string types when the method is native#5738Conversation
VincentLanglet
left a comment
There was a problem hiding this comment.
I don't think this solution will report
class X {
final static private function privateFoo():void {}
}
class Y {
}
/** @param class-string<X|Y> $row */
function testClassString($row): void
{
if (method_exists($row, 'privateFoo')) {
$row::privateFoo();
}
}
While it does for
class X {
final private function privateFoo():void {}
}
class Y {
}
function testClassString(X|Y $row): void
{
if (method_exists($row, 'privateFoo')) {
$row->privateFoo();
}
}
…ative - 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
065df84 to
af78744
Compare
VincentLanglet
left a comment
There was a problem hiding this comment.
I don't think this solution will report
class X {
final static private function privateFoo():void {}
}
class Y {
}
/** @param class-string<X|Y> $row */
function testClassString($row): void
{
if (method_exists($row, 'privateFoo')) {
$row::privateFoo();
}
}
While it does for
class X {
final private function privateFoo():void {}
}
class Y {
}
function testClassString(X|Y $row): void
{
if (method_exists($row, 'privateFoo')) {
$row->privateFoo();
}
}
…intersection When method_exists() narrowing adds HasMethodType to a class-string type (e.g. class-string<X|Y>&hasMethod(foo)), StaticMethodCallCheck previously returned early at the isString()->yes() check, skipping visibility checking entirely. This also affected the union case class-string<X|Y> where only some classes have the method natively. Fix: when the type is a class-string (isClassString()->yes()), extract the concrete class types that have the method natively and proceed with visibility checking on those, rather than returning early. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Here's a summary of the changes made to address the review: Problem: The reviewer pointed out that for Root cause in Fix: Added a new
Test: Added the reviewer's exact test case — |
staabm
left a comment
There was a problem hiding this comment.
resolve the conflict and rebase
Summary
When
method_exists()was used to guard a static method call on a class-string type, PHPStan failed to report visibility errors for private and protected methods. This affected both literal class name calls (X::privateFoo()) and class-string variable calls ($row::privateFoo()).Changes
src/Type/Php/MethodExistsTypeSpecifyingExtension.phpto usecreateFuncCallSpecinstead of addingHasMethodTypeto the class-string type when all class reflections have the method natively. This preserves the originalGenericClassStringType/ConstantStringTyperepresentation.tests/PHPStan/Rules/Methods/data/bug-14684.phpandtests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.phpcovering:class-stringparameterclass-stringparameterX::privateFoo()) guarded bymethod_exists()Analogous cases probed
$obj->privateFoo()withmethod_exists($obj, 'privateFoo')): Already reports visibility errors correctly —MethodCallCheckalways reaches its visibility check without early returns.property_exists()narrowing: UsescreateFuncCallSpecfor string inputs already, so class-string types are not modified. Not affected.MethodExistsTypeSpecifyingExtension: Instance method calls through this path reach the visibility check inMethodCallCheck. Not affected.Root cause
MethodExistsTypeSpecifyingExtensionwas addingHasMethodTypeto class-string types even when the method already existed natively on the class. This changed the type representation fromGenericClassStringType(orConstantStringType) to anIntersectionType([GenericClassStringType, HasMethodType]). This corrupted representation causedStaticMethodCallCheckto skip visibility checking through two paths:X::privateFoo()): The$classStringType->hasMethod()check returnedyesdue toHasMethodTypein the intersection (viaTrinaryLogic::lazyMaxMin), triggering an early return that skipped all further checks including visibility.$row::privateFoo()): The intersection type was not recognized asGenericClassStringTypeby theinstanceofcheck, falling through toisString()->yes()which also returnedyes(vialazyMaxMin), causing another early return before visibility checking.The fix prevents the type corruption by not adding redundant
HasMethodTypewhen the method is already native. ThecreateFuncCallSpecapproach instead records that themethod_exists()call evaluates totrue, without modifying the class-string type.Test
testBug14684inCallStaticMethodsRuleTest.php: Tests that private and protected static method calls report errors when guarded bymethod_exists(), for bothclass-string<X>parameters and literal class name calls.testHasMethodStaticCall,testBug1267,testBug6147,testBug4550) continue to pass — these use non-native methods or public methods, so the fix does not affect them.Fixes phpstan/phpstan#14684