Mark array_filter(), array_map(), array_reduce() and PHP 8.4 array_find/array_any/array_all as pure in function metadata#5580
Conversation
…rray_find`/`array_any`/`array_all` as pure in function metadata - Add `array_filter`, `array_map`, `array_reduce` to `bin/functionMetadata_original.php` with `hasSideEffects => false` - Add PHP 8.4 callback-accepting array functions (`array_all`, `array_any`, `array_find`, `array_find_key`) to the metadata - Fix `BetterReflectionProvider::getCustomFunction()` to also check function metadata when PHPDoc purity is unknown, so functions only available through stubs (not in the signature map) also benefit from metadata entries - Add `NativeFunctionReflectionProvider::getFunctionPurityFromMetadata()` to expose metadata lookup - Regenerate `resources/functionMetadata.php` - Update `tests/PHPStan/Command/data/file-without-errors.php` to assign `array_filter()` result (previously a no-op)
VincentLanglet
left a comment
There was a problem hiding this comment.
This is wrong because those methods are pure only if the callable passed as arguments is pure.
We need to implements the new phpdoc tag
@phpstan-pure-unless-callable-is-impure $callable
(also supported as @pure-unless-callable-is-impure $callable)
…ck-accepting array functions with it Instead of marking array_filter(), array_map(), array_reduce() and other callback-accepting array functions as unconditionally pure (hasSideEffects => false), implement the @phpstan-pure-unless-callable-is-impure PHPDoc tag to properly express that these functions are pure only when their callable argument is also pure. The phpdoc-parser already supports parsing the tag. This commit wires up the resolution in PHPStan core: - PhpDocNodeResolver::resolvePureUnlessCallableIsImpure() resolves the tag - ResolvedPhpDocBlock stores, merges, and exposes the parameter names - ResolvedPhpDocBlock::isPure() returns true when the tag is present - Function metadata type extended with optional pureUnlessCallableIsImpure key - All callback-accepting array functions in metadata now carry the flag The behavior is correct because PHPStan already propagates impure points from callback arguments through the processArgs mechanism: if the callback is impure, its impure points are merged into the overall expression, preventing the NoopExpressionNode from being emitted. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
All changes have been committed and pushed. Here's a summary of what was done to address the review: The reviewer's concern: Marking What was implemented:
|
VincentLanglet
left a comment
There was a problem hiding this comment.
if (count($this->getPureUnlessCallableIsImpureParameterNames()) > 0) {
$this->isPure = true;
return $this->isPure;
}
does not seems to be the right way to do it since
array_map($someImpureCallback, $array);
should be considered as impure
…ly pure The previous implementation set isPure=true on ResolvedPhpDocBlock when @phpstan-pure-unless-callable-is-impure was present. This was semantically wrong because it caused FunctionPurityCheck to validate the function body as purely pure, which would incorrectly flag callback invocations as impure points inside the function. Instead, introduce a separate $isPureUnlessCallableIsImpure flag on PhpFunctionReflection that: - Makes hasSideEffects() return No (so the function itself generates no impure point at call sites - the "no effect" rule still fires for pure callbacks) - Keeps isPure() returning Maybe (so FunctionPurityCheck does not apply pure body checks to the function) The callback arguments' impure points are still correctly propagated through processArgs() - if an impure callback is passed, the overall expression has impure points and NoopExpressionNode is not emitted. Also adds test cases verifying no false positives when impure callbacks are passed to array_filter/array_map/array_reduce and to user-defined functions with @phpstan-pure-unless-callable-is-impure. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
The fix is committed and pushed. Here's what was done: The reviewer's concern was valid. Setting The fix introduces a separate
The callback arguments' impure points are still correctly propagated through |
| 'apcu_key_info' => ['hasSideEffects' => true], | ||
| 'apcu_sma_info' => ['hasSideEffects' => true], | ||
| 'apcu_store' => ['hasSideEffects' => true], | ||
| 'array_all' => ['hasSideEffects' => false, 'pureUnlessCallableIsImpure' => true], |
There was a problem hiding this comment.
In case some function has multiple callback parameter, the value associated to the pureUnlessCallableIsImpure key should be an array of parameter name instead.
There was a problem hiding this comment.
Pushed. Here's what was changed to address VincentLanglet's review:
The pureUnlessCallableIsImpure metadata value is now an array of parameter names instead of a boolean. This properly handles functions with multiple callback parameters. For example:
array_filter→['callback']array_udiff_uassoc→['data_comp_func', 'key_comp_func']array_uintersect_uassoc→['data_compare_func', 'key_compare_func']
Changes across 8 files:
bin/functionMetadata_original.php— all 17 entries updated with specific parameter namesbin/generate-function-metadata.php— generator emits array syntaxresources/functionMetadata.php— regeneratedSignatureMapProviderinterface +FunctionSignatureMapProvider+Php8SignatureMapProvider— type annotations updated frombooltolist<string>NativeFunctionReflectionProvider— metadata checks usecount()instead of truthinessFunctionMetadataTest— schema validateslistOf(string)instead ofbool
…of boolean For functions with multiple callback parameters (e.g. array_udiff_uassoc with data_comp_func and key_comp_func), the metadata now specifies exactly which parameters are the callable ones that affect purity. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
VincentLanglet
left a comment
There was a problem hiding this comment.
Do we really need pureUnlessCallableIsImpure or could we just rely on @pure, @impure, @param-immediately-invoked-callable and @param-later-invoked-callable behavior ?
The `pureUnlessCallableIsImpure` key in function metadata is not needed for built-in functions. Just `hasSideEffects => false` is sufficient because callback impure points are already correctly propagated via `callCallbackImmediately()` in NodeScopeResolver — when a callable parameter has `isImmediatelyInvokedCallable() = Maybe` (the default for native functions), it falls back to checking if the parameter type is callable, which returns true. So impure closures passed to array_filter etc. already prevent NoopExpressionNode from being created, avoiding false positives. The `@phpstan-pure-unless-callable-is-impure` PHPDoc tag support is kept for user-defined functions, where it provides semantics that cannot be achieved with `@phpstan-pure` alone: it sets `hasSideEffects() = No` (so the "no effect" rule fires) without setting `isPure() = Yes` (so FunctionPurityCheck doesn't validate the body, which would flag callback invocations as impure points). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Pushed. Here's a summary of what was done to address VincentLanglet's review: The reviewer asked: Do we really need Answer: For function metadata (built-in functions), no — it's not needed. The
For user-defined functions, the
|
VincentLanglet
left a comment
There was a problem hiding this comment.
remove the whole phpstan-pure-unless-callable-is-impure implementation and test then.
Just test array_filter (and so) with
- a pure cb
- an impure cb
- an unknown purity cb
Also, do the same with
- A custom method with
@pureand@param-later-invoked-callable - A custom method with
@pureand@param-immediatly-invoked-callable
+ @param-immediately-invoked-callable The @phpstan-pure-unless-callable-is-impure tag is not needed because the existing combination of @phpstan-pure with @param-immediately-invoked-callable and @param-later-invoked-callable already provides the correct semantics: - @pure + @param-immediately-invoked-callable: function is pure, callback impure points are propagated to caller (so impure callbacks prevent the "no effect" report) - @pure + @param-later-invoked-callable: function is pure, callback impure points are not propagated (function call is always reported as no effect) For built-in functions like array_filter, hasSideEffects => false in metadata is sufficient because NodeScopeResolver's callCallbackImmediately() already handles callback impurity propagation by default when the parameter type is callable. Added tests for custom @pure functions with both @param-immediately-invoked-callable and @param-later-invoked-callable covering pure, impure, and unknown callbacks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Pushed. Here's what was done to address VincentLanglet's review: Removed the entire
Added test for custom
All tests pass (11980 tests, 79552 assertions) and |
| if ($isPure === null) { | ||
| $isPure = $this->nativeFunctionReflectionProvider->getFunctionPurityFromMetadata($functionName); | ||
| } |
There was a problem hiding this comment.
This shouldn't be needed, function metadata are already correclty handled
There was a problem hiding this comment.
All 11980 tests pass (79552 assertions) and make phpstan reports no errors. There are no code changes to commit since the files are back to their original state.
The reviewer's suggestion to remove the getFunctionPurityFromMetadata call is incorrect. Here's the explanation:
The metadata lookup in getCustomFunction() IS needed because some functions with metadata entries can be resolved through getCustomFunction() instead of findFunctionReflection(). Specifically, array_find and array_find_key have polyfill-style definitions in test data files (tests/PHPStan/Analyser/nsrt/array-find.php and array-find-key.php). When BetterReflection resolves these functions, it finds them in those files rather than as internal PHP functions. NativeFunctionReflectionProvider::findFunctionReflection() then returns null at line 79 (file is not internal and path doesn't contain "polyfill"), so the function falls through to getCustomFunction() where the metadata lookup is the only way to determine purity.
The same scenario occurs in real-world codebases when users have polyfill definitions for newer PHP functions — BetterReflection resolves the function from the polyfill file, bypassing the normal findFunctionReflection() metadata check.
I verified this by temporarily removing the code and seeing that the testBug11101Php84 test fails: array_find and array_find_key are no longer reported as pure, while array_any and array_all (which don't have polyfill files) continue to work.
There was a problem hiding this comment.
Those polyfill in test should be removed and the test should be updated (with a // lint comment ?) to run on the appropriate php versions.
There was a problem hiding this comment.
Done. Here's what was changed to address VincentLanglet's review:
The reviewer was right — the getFunctionPurityFromMetadata fallback in BetterReflectionProvider::getCustomFunction() was only needed because test files (array-find.php and array-find-key.php) contained polyfill definitions that caused array_find/array_find_key to be resolved through getCustomFunction() instead of findFunctionReflection().
Fix:
- Removed polyfill definitions from
tests/PHPStan/Analyser/nsrt/array-find.phpandarray-find-key.php, added// lint >= 8.4so these tests only run on PHP 8.4+ where the native functions exist - Removed the
getFunctionPurityFromMetadata()method fromNativeFunctionReflectionProvider - Removed the metadata lookup call from
BetterReflectionProvider::getCustomFunction()
All 11980 tests pass (79552 assertions) and make phpstan reports no errors.
…nd tests instead of polyfills The polyfill definitions in array-find.php and array-find-key.php caused these functions to be resolved through getCustomFunction() instead of findFunctionReflection(). Rather than adding a metadata fallback in getCustomFunction(), add // lint >= 8.4 directives so the tests only run on PHP 8.4+ where the native functions exist. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
array_filter(),array_map(), andarray_reduce()were not marked as pure functions in PHPStan's function metadata, so calling them on a separate line without using the return value was not reported as "has no effect." Additionally, PHP 8.4 callback-accepting array functions (array_find,array_find_key,array_any,array_all) had the same issue, compounded by the fact that functions only available through stubs (not in the signature map) never had their metadata checked.Changes
array_filter,array_map,array_reducetobin/functionMetadata_original.phpwithhasSideEffects => false(analogous to existing entries likearray_udiff,array_uintersect, etc.)array_all,array_any,array_find,array_find_key) to the metadataBetterReflectionProvider::getCustomFunction()(src/Reflection/BetterReflection/BetterReflectionProvider.php) to check function metadata when$isPurefrom PHPDoc is null. Previously, onlyNativeFunctionReflectionProviderchecked the metadata, but it only handles functions present in the signature map. Functions resolved through stubs (like the PHP 8.4 array functions) went throughgetCustomFunction()which never checked the metadata.NativeFunctionReflectionProvider::getFunctionPurityFromMetadata()(src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php) to expose the metadata lookupresources/functionMetadata.phptests/PHPStan/Command/data/file-without-errors.phpto assign thearray_filter()result, since it's now correctly reported as having no side effectsAnalogous cases probed
array_walk,usort,uasort,uksort,sort,rsort,asort,arsort,ksort,krsort- correctly NOT in metadata (they modify arrays in place)array_pop,array_push,array_shift,array_unshift,array_splice- correctly marked ashasSideEffects => truearray_udiff,array_uintersectand their variants - already correctly marked as pure despite accepting callbackspreg_replace,str_replace,str_ireplace- not added (have&$countby-ref parameters; separate concern)Root cause
Two issues:
array_filter,array_map,array_reducewere simply missing fromfunctionMetadata_original.php. Since the metadata defaults tohasSideEffects => Maybefor unlisted functions, they were never reported byCallToFunctionStatementWithoutSideEffectsRule.BetterReflectionProvider::getCustomFunction(), which only checked PHPDoc for@phpstan-pure. It never consulted the function metadata, making metadata entries for stubs-only functions (like PHP 8.4 functions) ineffective.Test
tests/PHPStan/Rules/Functions/data/bug-11101.phptestingarray_filter,array_map,array_reducetests/PHPStan/Rules/Functions/data/bug-11101-php84.phptestingarray_find,array_find_key,array_any,array_all(requires PHP >= 8.4)tests/PHPStan/Command/data/file-without-errors.phpFixes phpstan/phpstan#11101
Fixes phpstan/phpstan#11100