From f61d90f7656bab46af47c555ee216a08af7a0c8b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 17:13:17 +0000 Subject: [PATCH] Fix method_exists() with string literal not narrowing static method calls - When method_exists() receives a string literal class name (not ClassName::class), the type narrowing was only applied to the string expression, not to the equivalent ClassConstFetch expression that StaticMethodCallCheck uses - Added logic in MethodExistsTypeSpecifyingExtension to also narrow the ClassConstFetch expression when the first argument is a string literal that is a class-string - New regression tests in tests/PHPStan/Rules/Methods/data/bug-9592.php and tests/PHPStan/Analyser/nsrt/bug-9592.php Closes https://github.com/phpstan/phpstan/issues/9592 --- .../MethodExistsTypeSpecifyingExtension.php | 30 ++++++++++++++++++- tests/PHPStan/Analyser/nsrt/bug-9592.php | 19 ++++++++++++ .../Methods/CallStaticMethodsRuleTest.php | 7 +++++ tests/PHPStan/Rules/Methods/data/bug-9592.php | 30 +++++++++++++++++++ 4 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-9592.php create mode 100644 tests/PHPStan/Rules/Methods/data/bug-9592.php diff --git a/src/Type/Php/MethodExistsTypeSpecifyingExtension.php b/src/Type/Php/MethodExistsTypeSpecifyingExtension.php index 3e42c9c42e..03b6e5fc8c 100644 --- a/src/Type/Php/MethodExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/MethodExistsTypeSpecifyingExtension.php @@ -2,6 +2,8 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Expr; +use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Name\FullyQualified; use PHPStan\Analyser\Scope; @@ -20,6 +22,7 @@ use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\UnionType; use function count; +use function ltrim; #[AutowiredService] final class MethodExistsTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension @@ -64,7 +67,7 @@ public function specifyTypes( $objectType = $scope->getType($args[0]->value); if ($objectType->isString()->yes()) { if ($objectType->isClassString()->yes()) { - return $this->typeSpecifier->create( + $result = $this->typeSpecifier->create( $args[0]->value, new IntersectionType([ $objectType, @@ -73,6 +76,31 @@ public function specifyTypes( $context, $scope, ); + + if (!$args[0]->value instanceof ClassConstFetch) { + foreach ($objectType->getConstantStrings() as $constantString) { + $className = ltrim($constantString->getValue(), '\\'); + if ($className === '') { + continue; + } + $classConstFetch = new Expr\ClassConstFetch( + new FullyQualified($className), + 'class', + ); + $classConstFetchType = $scope->getType($classConstFetch); + $result = $result->unionWith($this->typeSpecifier->create( + $classConstFetch, + new IntersectionType([ + $classConstFetchType, + new HasMethodType($methodNameType->getValue()), + ]), + $context, + $scope, + )); + } + } + + return $result; } return new SpecifiedTypes([], []); diff --git a/tests/PHPStan/Analyser/nsrt/bug-9592.php b/tests/PHPStan/Analyser/nsrt/bug-9592.php new file mode 100644 index 0000000000..f2a1ceb05f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9592.php @@ -0,0 +1,19 @@ +checkThisOnly = false; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/bug-9592.php'], []); + } + public function testBug1267(): void { $this->checkThisOnly = false; diff --git a/tests/PHPStan/Rules/Methods/data/bug-9592.php b/tests/PHPStan/Rules/Methods/data/bug-9592.php new file mode 100644 index 0000000000..165440e95f --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9592.php @@ -0,0 +1,30 @@ +