From 1bf45995aa3c3c537d3e28734583578c210d1afa Mon Sep 17 00:00:00 2001 From: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Date: Mon, 6 Apr 2026 19:08:34 +0000 Subject: [PATCH 1/3] Fix phpstan/phpstan#9844: Static method call on class-string returns mixed instead of ErrorType - When calling a static method on a non-generic class-string variable, return mixed instead of ErrorType - The fix only applies to class-string types, not to named class or typed object expressions - New regression test in tests/PHPStan/Analyser/nsrt/bug-9844.php - Root cause: class-string converts to ObjectWithoutClassType which has hasMethod() = MAYBE, causing filterTypeWithMethod to bail out and fall back to ErrorType --- src/Analyser/ExprHandler/StaticCallHandler.php | 14 +++++++++++--- tests/PHPStan/Analyser/nsrt/bug-9844.php | 17 +++++++++++++++++ 2 files changed, 28 insertions(+), 3 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 ee0a758d491..182c891ad94 100644 --- a/src/Analyser/ExprHandler/StaticCallHandler.php +++ b/src/Analyser/ExprHandler/StaticCallHandler.php @@ -291,20 +291,28 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type return $callType; } + $calledOnClassString = false; if ($expr->class instanceof Name) { $staticMethodCalledOnType = $this->resolveTypeByNameWithLateStaticBinding($scope, $expr->class, $expr->name); } else { - $staticMethodCalledOnType = TypeCombinator::removeNull($scope->getType($expr->class))->getObjectTypeOrClassStringObjectType(); + $classType = TypeCombinator::removeNull($scope->getType($expr->class)); + $calledOnClassString = $classType->isClassString()->yes(); + $staticMethodCalledOnType = $classType->getObjectTypeOrClassStringObjectType(); } + $methodName = $expr->name->toString(); $callType = $this->methodCallReturnTypeHelper->methodCallReturnType( $scope, $staticMethodCalledOnType, - $expr->name->toString(), + $methodName, $expr, ); if ($callType === null) { - $callType = new ErrorType(); + if ($calledOnClassString && $staticMethodCalledOnType->hasMethod($methodName)->maybe()) { + $callType = new MixedType(); + } else { + $callType = 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: Mon, 6 Apr 2026 19:27:45 +0000 Subject: [PATCH 2/3] Revert class-string static call to ErrorType per review Keep ErrorType for `$class::foo()` on non-generic class-string, consistent with how `object::foo()` reports an error. Co-Authored-By: Claude Opus 4.6 --- src/Analyser/ExprHandler/StaticCallHandler.php | 14 +++----------- tests/PHPStan/Analyser/nsrt/bug-9844.php | 2 +- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/Analyser/ExprHandler/StaticCallHandler.php b/src/Analyser/ExprHandler/StaticCallHandler.php index 182c891ad94..ee0a758d491 100644 --- a/src/Analyser/ExprHandler/StaticCallHandler.php +++ b/src/Analyser/ExprHandler/StaticCallHandler.php @@ -291,28 +291,20 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type return $callType; } - $calledOnClassString = false; if ($expr->class instanceof Name) { $staticMethodCalledOnType = $this->resolveTypeByNameWithLateStaticBinding($scope, $expr->class, $expr->name); } else { - $classType = TypeCombinator::removeNull($scope->getType($expr->class)); - $calledOnClassString = $classType->isClassString()->yes(); - $staticMethodCalledOnType = $classType->getObjectTypeOrClassStringObjectType(); + $staticMethodCalledOnType = TypeCombinator::removeNull($scope->getType($expr->class))->getObjectTypeOrClassStringObjectType(); } - $methodName = $expr->name->toString(); $callType = $this->methodCallReturnTypeHelper->methodCallReturnType( $scope, $staticMethodCalledOnType, - $methodName, + $expr->name->toString(), $expr, ); if ($callType === null) { - if ($calledOnClassString && $staticMethodCalledOnType->hasMethod($methodName)->maybe()) { - $callType = new MixedType(); - } else { - $callType = new ErrorType(); - } + $callType = 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 index a6ec9c098d4..03d1f09139a 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9844.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9844.php @@ -12,6 +12,6 @@ class HelloWorld */ public function sayHello(string $class): void { - assertType('mixed', $class::foo()); + assertType('*ERROR*', $class::foo()); } } From c38813decd1b565048478b00b824cfb65d17fda6 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 6 Apr 2026 19:51:19 +0000 Subject: [PATCH 3/3] Report staticMethod.notFound error for static calls on class-string Static method calls on bare class-string (e.g. $class::foo()) now report "Call to an undefined static method class-string::foo()" instead of silently returning no errors. This is consistent with how object type static calls already report the error. Co-Authored-By: Claude Opus 4.6 --- src/Rules/Methods/StaticMethodCallCheck.php | 7 +++++- .../Methods/CallStaticMethodsRuleTest.php | 21 +++++++++++++++++ tests/PHPStan/Rules/Methods/data/bug-9844.php | 23 +++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Rules/Methods/data/bug-9844.php diff --git a/src/Rules/Methods/StaticMethodCallCheck.php b/src/Rules/Methods/StaticMethodCallCheck.php index aaabe3d1e79..6feb0b1c4a8 100644 --- a/src/Rules/Methods/StaticMethodCallCheck.php +++ b/src/Rules/Methods/StaticMethodCallCheck.php @@ -24,8 +24,10 @@ use PHPStan\Rules\RuleLevelHelper; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\ClassStringType; use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StaticType; use PHPStan\Type\StringType; use PHPStan\Type\Type; @@ -200,11 +202,14 @@ public function check( if (!$classType->isObject()->yes()) { return [[], null]; } + } elseif ($classType instanceof ClassStringType) { // @phpstan-ignore phpstanApi.instanceofType + $typeForDescribe = $classType; + $classType = new ObjectWithoutClassType(); } elseif ($classType->isString()->yes()) { return [[], null]; } - $typeForDescribe = $classType; + $typeForDescribe ??= $classType; if ($classType instanceof StaticType) { $typeForDescribe = $classType->getStaticObjectType(); } diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index d7dc2805345..8e332d6967b 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -243,6 +243,10 @@ public function testCallStaticMethods(): void 'Static call to instance method CallStaticMethods\InterfaceWithStaticMethod::doInstanceFoo().', 213, ], + [ + 'Call to an undefined static method class-string::nonexistentMethod().', + 295, + ], [ 'Static method CallStaticMethods\Foo::test() invoked with 3 parameters, 0 required.', 298, @@ -1009,4 +1013,21 @@ public function testPipeOperator(): void ]); } + public function testBug9844(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = false; + $this->checkImplicitMixed = false; + $this->analyse([__DIR__ . '/data/bug-9844.php'], [ + [ + 'Call to an undefined static method class-string::foo().', + 13, + ], + [ + 'Call to an undefined static method object::foo().', + 21, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-9844.php b/tests/PHPStan/Rules/Methods/data/bug-9844.php new file mode 100644 index 00000000000..cb8dba22489 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9844.php @@ -0,0 +1,23 @@ +