Ask isCallable() of callable-like arrays and strings only where the answer is consumed - #6559
Merged
ondrejmirtes merged 2 commits intoSep 23, 2026
Conversation
… answer is consumed - ArrayHandler: check for a narrowed `is_callable()` expression in scope before asking the array literal type `isCallable()`, instead of asking it for every two-item array literal - ArgumentsHandler: ask the argument type `isCallable()` only when the result is used - when the callback is called immediately, or when invalidation applies and the argument can be an object (only closures carry expressions to invalidate) - DependencyResolver: skip the callable-array dependency for class-level initializers (class constants, property defaults, enum case values), which are never called where declared - GenericParametersAcceptorResolver: ask the argument type `isCallable()` only once a callable parameter with asserts is found (same eager reflection for generic function arguments) - Mirror the ArrayHandler and ArgumentsHandler changes in turbo-ext - Probed and left as is: UnusedPrivateMethodRule, SimpleImpurePoint, FuncCallHandler and other callers ask `isCallable()` only of values that are actually used as callables Answering `isCallable()` of `['SomeClass', 'someString']` or `'SomeClass::someString'` locates and reflects `SomeClass`, which for a huge generated class costs hundreds of MB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ondrejmirtes
force-pushed
the
create-pull-request/patch-zu1gstu
branch
from
September 23, 2026 10:28
a32405e to
1f8dd0a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A two-item array such as
['BigContainer', 'arbitrary_ident']looks like a callable. Asking its typeisCallable()makes PHPStan locate and reflectBigContainerto see whether it has anarbitrary_identmethod. PHPStan asked this in several places even when nothing used the array as a callable, for example a private class constant used only inin_array(). With a large generated class (a compiled DI container), this made single-file analysis use hundreds of MB.This change asks
isCallable()only where the answer is used. On the issue's reproducer (a class with 25,000 methods), analysing the file drops from 288 MB to 34.5 MB (30.5 MB with the turbo extension). The file with the reversed item order, which never matched a class, uses 52.5 MB.Changes
src/Analyser/ExprHandler/ArrayHandler.php: the array literal's type callback askedisCallable()of every two-item array and only then checked whether a narrowedis_callable($array)expression was in scope. The cheap scope lookup now runs first, andisCallable()is asked only when that narrowing exists.src/Analyser/ArgumentsHandler.php:isCallable()was asked of every argument, including arrays and strings passed to plainarray/stringparameters likein_array()'s haystack. It is now asked only when the result is used:callCallbackImmediately()), or[]for both.src/Dependency/DependencyResolver.php: theArray_branch (dependencies on the return types of callable arrays) is skipped for class-level initializers: class constants, property defaults and enum case values. Nothing calls these where they are declared.src/Reflection/GenericParametersAcceptorResolver.php(analogous case):inferPredicateTemplateTypes()asked every argument of every generic functionisCallable()before checking whether the parameter is a callable with asserts. The order is now reversed. This also covered'Class::method'string arguments.turbo-ext/src/ArrayHandler.cpp,turbo-ext/src/ArgumentsHandler.cpp: the same changes ported to the native mirrors. A follow-upmake bump-turbocommit is needed once this commit has its final SHA.isCallable()only of values that are actually used as callables:UnusedPrivateMethodRule,SimpleImpurePoint,FuncCallHandler,ArgumentsNormalizer,TypeSpecifier,CallCallablesRule,FunctionCallableRule.Root cause
ConstantArrayType::isCallable()andConstantStringType::isCallable()have to reflect the named class to answer. Several analyser paths asked this unconditionally, "just in case", before checking whether the answer would matter. The pattern: an expensive type query ordered before a cheap check that usually makes it unnecessary. Affected locations:ArrayHandler's type callbackArgumentsHandler::processArgs()DependencyResolver::collectNodeDependencies()GenericParametersAcceptorResolver::inferPredicateTemplateTypes()Test
tests/PHPStan/Analyser/Bug15292Test.phpregisters a methods class reflection extension that records everyhasMethod()question. It asserts:testCallableLikeValuesAreNotReflected(data/bug-15292.php): the class named in a callable-like class constant, a property default, an enum constant, and a'Class::method'string argument toin_array()is never asked about. Without the fix, four method lookups are recorded. Each of the three changed analyser/dependency files is needed on its own; reverting any one brings back some of them.testCallableLikeGenericArgumentsAreNotReflected(data/bug-15292-generic.php): the same for arguments to a generic function. It fails without theGenericParametersAcceptorResolverchange.testCallableIsReflected(data/bug-15292-callable.php): a positive control. An array passed tousort()is still reflected as a callable.Also verified:
make testspasses with and without the turbo extension loaded.turbo-ext/tests/walk-trace.phpproduces identical traces.smoke.php,side-by-side.phpandsignature-parity.phppass.make phpstanpasses.make name-collisionfails, but on an existing file it cannot parse (tests/PHPStan/Build/data/final-class-rule-pipe.php); it fails the same way on the base branch.Fixes phpstan/phpstan#15292
🤖 Generated with Claude Code