Skip to content

Mark array_filter(), array_map(), array_reduce() and sibling non-mutating callback functions as having no side effects#5912

Open
phpstan-bot wants to merge 4 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-u2o7nfn
Open

Mark array_filter(), array_map(), array_reduce() and sibling non-mutating callback functions as having no side effects#5912
phpstan-bot wants to merge 4 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-u2o7nfn

Conversation

@phpstan-bot

@phpstan-bot phpstan-bot commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

array_filter(), array_map() and array_reduce() used as standalone statements were not reported by the "Call to function … on a separate line has no effect." rule, even when the supplied callback is pure. These functions do not mutate their arguments and have no side effects of their own — the only side effects come from the callback. Because PHPStan already propagates the callback's impure points to the caller (callable parameters of functions are treated as immediately invoked), the functions themselves can be marked side-effect-free without losing detection of impure callbacks.

Changes

  • bin/functionMetadata_original.php and resources/functionMetadata.php (the generated file consumed at runtime): added hasSideEffects => false for:
    • array_filter, array_map, array_reduce (reported in the issue)
    • array_find, array_find_key, array_any, array_all (the rest of the non-mutating array callback family — PHP 8.4)
    • preg_replace_callback, preg_replace_callback_array (analogous string callback functions)
  • tests/PHPStan/Rules/Functions/data/bug-11101.php + bug-11101-php84.php and new test methods in CallToFunctionStatementWithoutSideEffectsRuleTest covering both pure callbacks (reported) and impure / unknown callbacks (not reported).
  • tests/PHPStan/Command/data/file-without-errors.php: changed array_filter([0, 1, 2]); to echo count(array_filter([0, 1, 2])); so the "no errors" fixture no longer contains what is now correctly a no-op call.
  • tests/PHPStan/Analyser/data/discussion-7124.php: the local filter() wrapper is now transitively pure (it only calls array_filter), so its standalone statement calls were correctly flagged as having no effect. Wrapped the calls in echo count(...) so the test keeps focusing on its conditional-callable type errors.

Probed and intentionally not changed

  • array_walk, usort, uasort, uksort: mutate their array argument by reference, so they correctly keep their side effects.
  • array_udiff, array_uintersect, array_diff_uassoc, … : already hasSideEffects => false.
  • call_user_func / call_user_func_array: genuinely pure-modulo-callback too, but they are the general-purpose invocation primitive rather than part of the array-callback family; excluded to keep the behavior change scoped to the issue.

Root cause

Purity of built-in functions is driven by resources/functionMetadata.php. When a function is absent there, hasSideEffects() is Maybe, so SimpleImpurePoint::createFromVariant() attaches a functionCall impure point to every call. That impure point prevents NodeScopeResolver from emitting a NoopExpressionNode, so the no-effect rule never fires. The fix records the true side-effect status of these higher-order array functions. The callback's own side effects are unaffected: they are gathered independently when arguments are processed (immediately-invoked callable parameters), so an impure callback still produces impure points and the call is not flagged.

The same metadata gap affected the whole family of non-mutating array callback functions and the preg_replace_callback pair, which are fixed together here.

Test

  • CallToFunctionStatementWithoutSideEffectsRuleTest::testBug11101 asserts that array_filter(), array_map(), array_reduce(), array_filter() (no callback), preg_replace_callback() and preg_replace_callback_array() statements with pure callbacks are reported, while impure and unknown-purity callbacks are not.
  • testBug11101Php84 (gated >= 8.4.0) covers array_any()/array_all(). (array_find/array_find_key are verified via CLI; they cannot be asserted in RuleTestCase because existing nsrt/array-find*.php polyfill fixtures shadow the built-ins during static reflection. In production they resolve through the native signature map and honor the metadata.)
  • Both regression tests fail before the metadata change and pass after it. Full make tests and make phpstan are green.

Fixes phpstan/phpstan#11101
Fixes phpstan/phpstan#11100

…n-mutating callback functions as having no side effects

- Add `hasSideEffects => false` entries for `array_filter`, `array_map` and `array_reduce` to `bin/functionMetadata_original.php` and the generated `resources/functionMetadata.php`. These functions do not mutate their arguments and have no side effects of their own; any side effects come from the supplied callback, whose impure points already propagate to the caller because callable parameters of functions are treated as immediately invoked.
- Sweep the rest of the non-mutating array callback family that had the same problem: `array_find`, `array_find_key`, `array_any`, `array_all`.
- Also mark the analogous `preg_replace_callback` and `preg_replace_callback_array` as side-effect-free.
- An impure callback (e.g. one that echoes) still suppresses the "no effect" report because its impure points are merged into the statement; only genuinely pure callbacks make the whole call a no-op.
- Update `tests/.../data/file-without-errors.php` and `tests/.../data/discussion-7124.php` so they use the result of the now-pure calls instead of relying on the previous (buggy) behavior where these calls were never reported.
- Considered but deliberately excluded `call_user_func`/`call_user_func_array`: they are the general-purpose invocation primitive rather than part of the array-callback family; the propagation machinery would make them safe, but the behavior change is broader than this issue.

@staabm staabm left a comment

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.

@staabm staabm left a comment

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.

also add a test for phpstan/phpstan#11100

@staabm

staabm commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

@VincentLanglet I think your suggestion in #3482 (comment) stays correct.

I think for immediate-invoked-callables we can support the purity for these builtin functions without the need for @phpstan-pure-unless-callable-is-impure

phpstan-bot and others added 2 commits June 21, 2026 20:05
…lback

Closes phpstan/phpstan#11100

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ng no effect

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@phpstan-bot

Copy link
Copy Markdown
Collaborator Author

Both review requests are addressed and pushed.

Summary

The reviewer (staabm) requested tests for two scenarios. I fetched both playground links via the PHPStan playground API to get the exact reproducers, then added validated regression tests for each.

1. Issue #11100 — a @phpstan-pure method using array_map() with a static pure callback was a false positive (Possibly impure call to function array_map() in pure method ...).

  • Added tests/PHPStan/Rules/Pure/data/bug-11100.php (verbatim from the issue's playground bb65d595)
  • Added PureMethodRuleTest::testBug11100() expecting no errors

2. Current review playground (75ed18fc)array_map($pureFx, $args) where $pureFx is a pure closure stored in a variable should be reported as "no effect".

  • Added tests/PHPStan/Rules/Functions/data/bug-11101-pure-closure.php (faithful to the playground)
  • Added CallToFunctionStatementWithoutSideEffectsRuleTest::testBug11101PureClosure() expecting the array_map() no-effect error on line 17

Verification:

  • Both tests fail before the metadata fix (#11100: impure-call false positive appears; pure-closure: no-effect error missing) and pass after — confirming they're genuine regression tests.
  • Full affected test classes pass (23 tests).
  • make phpstan is green.

Committed as two separate logical commits and pushed. Commit #1 includes Closes https://github.com/phpstan/phpstan/issues/11100 in the body.

@phpstan-bot phpstan-bot force-pushed the create-pull-request/patch-u2o7nfn branch from 0789c96 to cf7ee51 Compare June 21, 2026 20:08
@phpstan-bot

Copy link
Copy Markdown
Collaborator Author

Done. Here's a summary of how I addressed staabm's review.

Both review requests are now covered

I fetched both playground samples via the PHPStan playground API (/sample?id=) to get the exact reproducers:

  1. https://github.com/phpstan/phpstan/issues/11100 (current review) — a @phpstan-pure method calling array_map() with a static closure, which before the fix reported the false positive "Possibly impure call to function array_map() in pure method".
  2. https://phpstan.org/r/75ed18fc... (previous review) — a non-pure method with a standalone array_map($pureFx, $args) where $pureFx is a pure closure stored in a variable, which should now be flagged "Call to function array_map() on a separate line has no effect."

I wrote regression tests for both and verified each fails before the metadata fix (showing exactly the issue's error / missing error) and passes after:

  • PureMethodRuleTest::testBug11100 + data/bug-11100.php (verbatim from the issue's playground) — expects no errors.
  • CallToFunctionStatementWithoutSideEffectsRuleTest closure-variable case — expects the no-effect report only on the array_map line (the $pureFx($args[0]) call is correctly not flagged, since the rule only handles named function calls).

Note on what actually landed

While I was working, the PR author/bot pushed equivalent commits for both scenarios (4cc8eee8f for #11100, cf7ee5179 for the closure case). During rebase:

  • My #11100 commit was auto-detected as already-applied (identical patch) and skipped.
  • My closure-variable commit was a near-duplicate of cf7ee5179, so I dropped it via git reset --hard + force-push to avoid two tests covering the same thing.

The branch now contains both regression tests (from the upstream commits) with no duplication. make phpstan is green, and the four relevant test methods pass.

@staabm staabm requested a review from VincentLanglet June 21, 2026 20:19
@VincentLanglet

Copy link
Copy Markdown
Contributor

@VincentLanglet I think your suggestion in #3482 (comment) stays correct.

I think for immediate-invoked-callables we can support the purity for these builtin functions without the need for @phpstan-pure-unless-callable-is-impure

I agree, and had the #5580 PR for this.

But I thought ondrej had different opinion on it see https://phpstan.slack.com/archives/C0AEG6PGEL8/p1777717259302239

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants