From 38cb83abd25566633586679e099a51914d0e3dbc Mon Sep 17 00:00:00 2001 From: staabm <120441+staabm@users.noreply.github.com> Date: Fri, 27 Mar 2026 07:23:23 +0000 Subject: [PATCH 1/8] Fix static method call on non-generic class-string returning ErrorType - Static method calls on non-generic class-string (e.g. $class::foo() where $class is class-string) now return mixed instead of *ERROR* - The root cause was that ClassStringType::getObjectTypeOrClassStringObjectType() returns ObjectWithoutClassType, which has hasMethod() returning maybe, causing filterTypeWithMethod() to return null - Added check in StaticCallHandler: when the resolved type is a classless object type (from class-string), return MixedType instead of ErrorType - New regression test in tests/PHPStan/Analyser/nsrt/bug-9844.php --- src/Analyser/ExprHandler/StaticCallHandler.php | 7 +++++-- tests/PHPStan/Analyser/nsrt/bug-9844.php | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-9844.php diff --git a/src/Analyser/ExprHandler/StaticCallHandler.php b/src/Analyser/ExprHandler/StaticCallHandler.php index 2c06152b2cf..cc798af209c 100644 --- a/src/Analyser/ExprHandler/StaticCallHandler.php +++ b/src/Analyser/ExprHandler/StaticCallHandler.php @@ -335,14 +335,17 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type $staticMethodCalledOnType = TypeCombinator::removeNull($scope->getType($expr->class))->getObjectTypeOrClassStringObjectType(); } + $methodName = $expr->name->toString(); $callType = $this->methodCallReturnTypeHelper->methodCallReturnType( $scope, $staticMethodCalledOnType, - $expr->name->toString(), + $methodName, $expr, ); if ($callType === null) { - $callType = new ErrorType(); + $callType = $staticMethodCalledOnType->isObject()->yes() && $staticMethodCalledOnType->getObjectClassNames() === [] + ? new MixedType() + : new ErrorType(); } if ($expr->class instanceof Expr) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-9844.php b/tests/PHPStan/Analyser/nsrt/bug-9844.php new file mode 100644 index 00000000000..a6ec9c098d4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9844.php @@ -0,0 +1,17 @@ + Date: Fri, 27 Mar 2026 09:21:43 +0100 Subject: [PATCH 2/8] Update bug-9844.php --- tests/PHPStan/Analyser/nsrt/bug-9844.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-9844.php b/tests/PHPStan/Analyser/nsrt/bug-9844.php index a6ec9c098d4..291f4233c87 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9844.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9844.php @@ -13,5 +13,7 @@ class HelloWorld public function sayHello(string $class): void { assertType('mixed', $class::foo()); + assertType('mixed', $class->foo()); + assertType('mixed', $class?->foo()); } } From f978a5af802878deb9e90ebf34df31123f2bdcbc Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 27 Mar 2026 09:23:19 +0100 Subject: [PATCH 3/8] Update bug-9844.php --- tests/PHPStan/Analyser/nsrt/bug-9844.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-9844.php b/tests/PHPStan/Analyser/nsrt/bug-9844.php index 291f4233c87..848f760ee9b 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9844.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9844.php @@ -10,10 +10,11 @@ class HelloWorld /** * @param class-string $class */ - public function sayHello(string $class): void + public function sayHello(string $class, string $method): void { assertType('mixed', $class::foo()); assertType('mixed', $class->foo()); assertType('mixed', $class?->foo()); + assertType('mixed', $class::$method()); } } From e17007557893cc832d1a30ec7451a2d609ae59ee Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 27 Mar 2026 09:25:41 +0100 Subject: [PATCH 4/8] Update bug-9844.php --- tests/PHPStan/Analyser/nsrt/bug-9844.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-9844.php b/tests/PHPStan/Analyser/nsrt/bug-9844.php index 848f760ee9b..845a6ac4b2e 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9844.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9844.php @@ -10,11 +10,12 @@ class HelloWorld /** * @param class-string $class */ - public function sayHello(string $class, string $method): void + public function sayHello(string $class, string $method, string $property): void { assertType('mixed', $class::foo()); assertType('mixed', $class->foo()); assertType('mixed', $class?->foo()); assertType('mixed', $class::$method()); + assertType('mixed', $class->$property); } } From f0eba676211bb8c132073a14ff619f1f8c56b458 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 27 Mar 2026 09:25:50 +0100 Subject: [PATCH 5/8] Update bug-9844.php --- tests/PHPStan/Analyser/nsrt/bug-9844.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-9844.php b/tests/PHPStan/Analyser/nsrt/bug-9844.php index 845a6ac4b2e..df488a9ad43 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9844.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9844.php @@ -17,5 +17,6 @@ public function sayHello(string $class, string $method, string $property): void assertType('mixed', $class?->foo()); assertType('mixed', $class::$method()); assertType('mixed', $class->$property); + assertType('mixed', $class?->$property); } } From d178be3d048857d77e986955bc0358d652a056e8 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 28 Mar 2026 11:35:31 +0000 Subject: [PATCH 6/8] Fix test expectations for instance method calls on class-string Instance method calls ($class->foo()) on a class-string variable correctly return ErrorType since strings don't have instance methods. Only static calls ($class::foo()) should return mixed. Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Analyser/nsrt/bug-9844.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-9844.php b/tests/PHPStan/Analyser/nsrt/bug-9844.php index df488a9ad43..a8a48e925bd 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9844.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9844.php @@ -13,8 +13,8 @@ class HelloWorld public function sayHello(string $class, string $method, string $property): void { assertType('mixed', $class::foo()); - assertType('mixed', $class->foo()); - assertType('mixed', $class?->foo()); + assertType('*ERROR*', $class->foo()); + assertType('*ERROR*', $class?->foo()); assertType('mixed', $class::$method()); assertType('mixed', $class->$property); assertType('mixed', $class?->$property); From 1f4476f7961f3e2f3f651b350fba5554d2858007 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 28 Mar 2026 12:43:49 +0100 Subject: [PATCH 7/8] Revert "Fix test expectations for instance method calls on class-string" This reverts commit 20558037987268a5d4726299c7cd2f196bcd61e6. --- tests/PHPStan/Analyser/nsrt/bug-9844.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-9844.php b/tests/PHPStan/Analyser/nsrt/bug-9844.php index a8a48e925bd..df488a9ad43 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9844.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9844.php @@ -13,8 +13,8 @@ class HelloWorld public function sayHello(string $class, string $method, string $property): void { assertType('mixed', $class::foo()); - assertType('*ERROR*', $class->foo()); - assertType('*ERROR*', $class?->foo()); + assertType('mixed', $class->foo()); + assertType('mixed', $class?->foo()); assertType('mixed', $class::$method()); assertType('mixed', $class->$property); assertType('mixed', $class?->$property); From 65815bdc305c76e3f9712de8a2fc3c6bf1e4e9e3 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 28 Mar 2026 11:51:13 +0000 Subject: [PATCH 8/8] Fix instance method calls on non-generic class-string returning ErrorType Apply the same fix from StaticCallHandler to MethodCallHandler: when methodCallReturnType returns null and the resolved type is a classless object (from plain class-string), return MixedType instead of ErrorType. Co-Authored-By: Claude Opus 4.6 --- src/Analyser/ExprHandler/MethodCallHandler.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Analyser/ExprHandler/MethodCallHandler.php b/src/Analyser/ExprHandler/MethodCallHandler.php index 826d757a20a..edc0676c94c 100644 --- a/src/Analyser/ExprHandler/MethodCallHandler.php +++ b/src/Analyser/ExprHandler/MethodCallHandler.php @@ -288,12 +288,16 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type { if ($expr->name instanceof Identifier) { if ($scope->nativeTypesPromoted) { + $nativeType = $scope->getNativeType($expr->var); $methodReflection = $scope->getMethodReflection( - $scope->getNativeType($expr->var), + $nativeType, $expr->name->name, ); if ($methodReflection === null) { - $returnType = new ErrorType(); + $nativeObjectType = $nativeType->getObjectTypeOrClassStringObjectType(); + $returnType = $nativeObjectType->isObject()->yes() && $nativeObjectType->getObjectClassNames() === [] + ? new MixedType() + : new ErrorType(); } else { $returnType = ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants())->getNativeReturnType(); } @@ -301,14 +305,18 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type return NullsafeShortCircuitingHelper::getType($scope, $expr->var, $returnType); } + $calledOnType = $scope->getType($expr->var); $returnType = $this->methodCallReturnTypeHelper->methodCallReturnType( $scope, - $scope->getType($expr->var), + $calledOnType, $expr->name->name, $expr, ); if ($returnType === null) { - $returnType = new ErrorType(); + $objectType = $calledOnType->getObjectTypeOrClassStringObjectType(); + $returnType = $objectType->isObject()->yes() && $objectType->getObjectClassNames() === [] + ? new MixedType() + : new ErrorType(); } return NullsafeShortCircuitingHelper::getType($scope, $expr->var, $returnType); }