From a936524161acdf4589cbc086004163759928a64e Mon Sep 17 00:00:00 2001 From: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Date: Mon, 6 Apr 2026 17:30:59 +0000 Subject: [PATCH 1/5] Fix phpstan/phpstan#12063: is_callable() false positive with union method names - When ConstantArrayType::findTypeAndMethodNames() skips non-existent methods, isCallable() now accounts for those skipped entries - Added doFindTypeAndMethodNames() private helper with out parameter tracking whether any method names were skipped due to not existing - New regression test in tests/PHPStan/Rules/Comparison/data/bug-12063.php --- src/Type/Constant/ConstantArrayType.php | 19 +++++++++- ...mpossibleCheckTypeFunctionCallRuleTest.php | 6 +++ .../Rules/Comparison/data/bug-12063.php | 38 +++++++++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-12063.php diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 538e8575216..99fa6585f5d 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -494,7 +494,8 @@ public function equals(Type $type): bool public function isCallable(): TrinaryLogic { - $typeAndMethods = $this->findTypeAndMethodNames(); + $hasNonExistentMethod = false; + $typeAndMethods = $this->doFindTypeAndMethodNames($hasNonExistentMethod); if ($typeAndMethods === []) { return TrinaryLogic::createNo(); } @@ -504,7 +505,13 @@ public function isCallable(): TrinaryLogic $typeAndMethods, ); - return TrinaryLogic::createYes()->and(...$results); + $result = TrinaryLogic::createYes()->and(...$results); + + if ($hasNonExistentMethod) { + $result = $result->and(TrinaryLogic::createMaybe()); + } + + return $result; } public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array @@ -537,6 +544,13 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) /** @return ConstantArrayTypeAndMethod[] */ public function findTypeAndMethodNames(): array + { + $hasNonExistentMethod = false; + return $this->doFindTypeAndMethodNames($hasNonExistentMethod); + } + + /** @return ConstantArrayTypeAndMethod[] */ + private function doFindTypeAndMethodNames(bool &$hasNonExistentMethod): array { if (count($this->keyTypes) !== 2) { return []; @@ -578,6 +592,7 @@ public function findTypeAndMethodNames(): array foreach ($methods->getConstantStrings() as $methodName) { $has = $type->hasMethod($methodName->getValue()); if ($has->no()) { + $hasNonExistentMethod = true; continue; } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 5e61f488d05..7442c01084c 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -1214,4 +1214,10 @@ public function testBug13799(): void ]); } + public function testBug12063(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-12063.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-12063.php b/tests/PHPStan/Rules/Comparison/data/bug-12063.php new file mode 100644 index 00000000000..3cfcc42eca1 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-12063.php @@ -0,0 +1,38 @@ +viewFunctions = $viewFunctions; + } + + public function iterateFunctions(): void + { + $functionMappings = [ + 'i_exist' => 'existingMethod', + 'i_dont_exist' => 'nonExistingMethod' + ]; + + $functions = []; + foreach ($functionMappings as $nameFrom => $nameTo) { + $callable = [$this->viewFunctions, $nameTo]; + if (!is_callable($callable)) { + throw new BadFunctionCallException("Function $nameTo does not exist in view functions"); + } + } + } +} From 33171cda588a5c573e2e5a1750f071b1a898d770 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 6 Apr 2026 17:49:50 +0000 Subject: [PATCH 2/5] Remove unused $hasNonExistentMethod variable in findTypeAndMethodNames() Make the by-reference parameter optional with a default value so findTypeAndMethodNames() doesn't need to instantiate a variable it never reads. Co-Authored-By: Claude Opus 4.6 --- src/Type/Constant/ConstantArrayType.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 99fa6585f5d..aa711029a8d 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -545,12 +545,11 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope) /** @return ConstantArrayTypeAndMethod[] */ public function findTypeAndMethodNames(): array { - $hasNonExistentMethod = false; - return $this->doFindTypeAndMethodNames($hasNonExistentMethod); + return $this->doFindTypeAndMethodNames(); } /** @return ConstantArrayTypeAndMethod[] */ - private function doFindTypeAndMethodNames(bool &$hasNonExistentMethod): array + private function doFindTypeAndMethodNames(bool &$hasNonExistentMethod = false): array { if (count($this->keyTypes) !== 2) { return []; From 3e442f2f21ae130a3e2cc1be37da839c528b7938 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 6 Apr 2026 18:12:42 +0000 Subject: [PATCH 3/5] Add non-regression tests for phpstan/phpstan#4608 Ensure that union method names ('abc'|'not_abc') on anonymous classes correctly report errors for dynamic method calls, call_user_func, and array callable invocations. Co-Authored-By: Claude Opus 4.6 --- .../Rules/Functions/CallCallablesRuleTest.php | 10 ++++++++++ .../Functions/CallToFunctionParametersRuleTest.php | 10 ++++++++++ .../Functions/data/bug-4608-call-user-func.php | 11 +++++++++++ .../Rules/Functions/data/bug-4608-callables.php | 11 +++++++++++ tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php | 13 +++++++++++++ tests/PHPStan/Rules/Methods/data/bug-4608.php | 11 +++++++++++ 6 files changed, 66 insertions(+) create mode 100644 tests/PHPStan/Rules/Functions/data/bug-4608-call-user-func.php create mode 100644 tests/PHPStan/Rules/Functions/data/bug-4608-callables.php create mode 100644 tests/PHPStan/Rules/Methods/data/bug-4608.php diff --git a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php index 2ed3dbf6f54..72f0f27e21e 100644 --- a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php @@ -364,6 +364,16 @@ public function testPipeOperator(): void ]); } + public function testBug4608(): void + { + $this->analyse([__DIR__ . '/data/bug-4608-callables.php'], [ + [ + "Trying to invoke array{class@anonymous/tests/PHPStan/Rules/Functions/data/bug-4608-callables.php:5, 'abc'|'not_abc'} but it might not be a callable.", + 11, + ], + ]); + } + public function testMaybeNotCallable(): void { $errors = []; diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index ee754a40d90..0abd2b554f2 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2817,4 +2817,14 @@ public function testBug14312b(): void $this->analyse([__DIR__ . '/data/bug-14312b.php'], []); } + public function testBug4608(): void + { + $this->analyse([__DIR__ . '/data/bug-4608-call-user-func.php'], [ + [ + "Parameter #1 \$callback of function call_user_func expects callable(): mixed, array{class@anonymous/tests/PHPStan/Rules/Functions/data/bug-4608-call-user-func.php:5, 'abc'|'not_abc'} given.", + 11, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-4608-call-user-func.php b/tests/PHPStan/Rules/Functions/data/bug-4608-call-user-func.php new file mode 100644 index 00000000000..29ca4e5cfd7 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-4608-call-user-func.php @@ -0,0 +1,11 @@ +checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-4608.php'], [ + [ + 'Call to an undefined method class@anonymous/tests/PHPStan/Rules/Methods/data/bug-4608.php:5::not_abc().', + 11, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-4608.php b/tests/PHPStan/Rules/Methods/data/bug-4608.php new file mode 100644 index 00000000000..118ff889cef --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-4608.php @@ -0,0 +1,11 @@ +{$s}(); From 784b42bdebda96c269ca3a21709e1b181831032d Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 6 Apr 2026 18:18:53 +0000 Subject: [PATCH 4/5] Fix call_user_func parameter name for PHP 7.4 compatibility The first parameter of call_user_func is named $function in PHP 7.x but was renamed to $callback in PHP 8.0+. Use PHP_VERSION_ID to select the correct name in the test assertion. Co-Authored-By: Claude Opus 4.6 --- .../Rules/Functions/CallToFunctionParametersRuleTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 0abd2b554f2..f826a8d865a 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2819,9 +2819,10 @@ public function testBug14312b(): void public function testBug4608(): void { + $paramName = PHP_VERSION_ID >= 80000 ? 'callback' : 'function'; $this->analyse([__DIR__ . '/data/bug-4608-call-user-func.php'], [ [ - "Parameter #1 \$callback of function call_user_func expects callable(): mixed, array{class@anonymous/tests/PHPStan/Rules/Functions/data/bug-4608-call-user-func.php:5, 'abc'|'not_abc'} given.", + "Parameter #1 \${$paramName} of function call_user_func expects callable(): mixed, array{class@anonymous/tests/PHPStan/Rules/Functions/data/bug-4608-call-user-func.php:5, 'abc'|'not_abc'} given.", 11, ], ]); From 01b21443d7c32fad09f4463ec2a281b0fe132b0a Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 6 Apr 2026 18:24:35 +0000 Subject: [PATCH 5/5] Fix coding standard: use sprintf() instead of variable in double-quoted string Co-Authored-By: Claude Opus 4.6 --- .../Rules/Functions/CallToFunctionParametersRuleTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index f826a8d865a..60b40422305 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2822,7 +2822,7 @@ public function testBug4608(): void $paramName = PHP_VERSION_ID >= 80000 ? 'callback' : 'function'; $this->analyse([__DIR__ . '/data/bug-4608-call-user-func.php'], [ [ - "Parameter #1 \${$paramName} of function call_user_func expects callable(): mixed, array{class@anonymous/tests/PHPStan/Rules/Functions/data/bug-4608-call-user-func.php:5, 'abc'|'not_abc'} given.", + sprintf("Parameter #1 \$%s of function call_user_func expects callable(): mixed, array{class@anonymous/tests/PHPStan/Rules/Functions/data/bug-4608-call-user-func.php:5, 'abc'|'not_abc'} given.", $paramName), 11, ], ]);