Skip to content

Fix phpstan/phpstan#12063: Call to function is_callable() with array{...} will always evaluate to true#5409

Merged
VincentLanglet merged 5 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-3oomohs
Apr 7, 2026
Merged

Fix phpstan/phpstan#12063: Call to function is_callable() with array{...} will always evaluate to true#5409
VincentLanglet merged 5 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-3oomohs

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

@phpstan-bot phpstan-bot commented Apr 6, 2026

Summary

When is_callable() is called with an array like [$object, $methodName] where $methodName is a union type containing both existing and non-existing method names (e.g., 'existingMethod'|'nonExistingMethod'), PHPStan incorrectly reported that the call will always evaluate to true. The fix makes isCallable() return maybe when some method names in the union don't exist on the target class.

Changes

  • Modified ConstantArrayType::isCallable() in src/Type/Constant/ConstantArrayType.php to detect when findTypeAndMethodNames() skipped non-existent methods
  • Extracted the body of findTypeAndMethodNames() into a private doFindTypeAndMethodNames(bool &$hasNonExistentMethod) helper that tracks whether any method names were skipped
  • When non-existent methods are detected, isCallable() ANDs the result with maybe to prevent false yes results

Root cause

ConstantArrayType::findTypeAndMethodNames() skips method names that don't exist on the target class (hasMethod()->no()continue). This means when checking is_callable([$obj, 'existingMethod'|'nonExistingMethod']), only existingMethod was returned, making isCallable() conclude the array is always callable. The fix preserves the skip behavior (needed for getCallableParametersAcceptors) but lets isCallable() know that some methods were filtered out.

Test

Added tests/PHPStan/Rules/Comparison/data/bug-12063.php with the exact reproducing code from the issue — a class with existingMethod and a loop over a mapping that includes both existingMethod and nonExistingMethod. The test expects no errors (the false positive function.alreadyNarrowedType should not be reported).

Fixes phpstan/phpstan#12063
Fixes phpstan/phpstan#4608

…thod 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
/** @return ConstantArrayTypeAndMethod[] */
public function findTypeAndMethodNames(): array
{
$hasNonExistentMethod = false;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have to instantiate this variable since we don't use it ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Made the $hasNonExistentMethod parameter optional with a default value of false in doFindTypeAndMethodNames(), so findTypeAndMethodNames() no longer needs to instantiate a variable it never uses. All tests and static analysis pass.

@VincentLanglet VincentLanglet self-assigned this Apr 6, 2026
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 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add non regression for phpstan/phpstan#4608, for this, ensure the three last line of

$c = new class {
	public function abc(): void {}
};

$s = rand(0, 1) ? 'abc' : 'not_abc';

$c->{$s}();
call_user_func([$c, $s]);
[$c, $s]();

report an error

- Call to an undefined method class@anonymous/tmp.php:3::not_abc().
- Parameter #1 $callback of function call_user_func expects callable(): mixed, array{class@anonymous/tmp.php:3, 'abc'|'not_abc'} given.
- Trying to invoke array{class@anonymous/tmp.php:3, 'abc'|'not_abc'} but it might not be a callable.

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 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

The background make phpstan task also completed successfully (exit code 0). Everything is green and pushed.

Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You get a failure on PHP 7.4

1) PHPStan\Rules\Functions\CallToFunctionParametersRuleTest::testBug4608
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
-'11: 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: Parameter #1 $function 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.
 '

The name of the param is depend on the PHP VERSION ID

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 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Fixed. The call_user_func first parameter is named $function in PHP 7.x but $callback in PHP 8.0+. The test now uses PHP_VERSION_ID to select the correct parameter name, matching the pattern already used elsewhere in that test file.

Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix

--------------------------------------------------------------------------------
FOUND 1 ERROR AFFECTING 1 LINE
--------------------------------------------------------------------------------
 2825 | ERROR | Variable "$paramName" not allowed in double quoted string; use
      |       | sprintf() instead (Squiz.Strings.DoubleQuoteUsage.ContainsVar)
--------------------------------------------------------------------------------

…ed string

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

make tests also confirmed complete (exit code 0). Both checks passed and the fix is already pushed.

@VincentLanglet VincentLanglet requested a review from staabm April 6, 2026 18:48
@VincentLanglet VincentLanglet merged commit a3bf054 into phpstan:2.1.x Apr 7, 2026
656 checks passed
@VincentLanglet VincentLanglet deleted the create-pull-request/patch-3oomohs branch April 7, 2026 06:07
phpstan-bot added a commit to phpstan-bot/phpstan-src that referenced this pull request Apr 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants