Fix phpstan/phpstan#14452: Performance degradation introduced in 2.1.41#5441
Merged
staabm merged 3 commits intophpstan:2.1.xfrom Apr 11, 2026
Merged
Conversation
…eCallExpr - Excluded PossiblyImpureCallExpr from participating in conditional expression creation during scope merges in MutatingScope::createConditionalExpressions() - PossiblyImpureCallExpr entries no longer become type guards (preventing O(N^2) conditional expression growth with N possibly-impure method calls) - PossiblyImpureCallExpr entries no longer have conditional expressions attached to them (they store declared return types, not control-flow-dependent types) - New regression test in tests/PHPStan/Analyser/nsrt/bug-14452.php
Contributor
VincentLanglet
approved these changes
Apr 11, 2026
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
Fixes a severe performance regression introduced in 2.1.41 where analysis of code with many possibly-impure method calls inside conditional branches could take exponentially longer (from 0.3s to 14+ minutes in the reported case). The regression was caused by
PossiblyImpureCallExprentries participating in the conditional expression system during scope merges, creating O(N^2) conditional expressions that were then repeatedly processed during type narrowing.Changes
MutatingScope::createConditionalExpressions()insrc/Analyser/MutatingScope.phpto skipPossiblyImpureCallExprentries in three places:$typeGuardsset (prevents O(N^2) conditional expression creation)$newVariableTypesloop that creates conditional expressions for variables$mergedExpressionTypesloop that creates ErrorType conditional expressionstests/PHPStan/Analyser/nsrt/bug-14452.phpRoot cause
The commit
cf070ae("Tip about adding@phpstan-impurewhere applicable") introducedPossiblyImpureCallExprnodes that are stored in the scope's expression types for every possibly-impure method/function call. These nodes carry the declared return type for use in tip generation.When an
ifblock contains N possibly-impure method calls, all NPossiblyImpureCallExprentries become type guards during scope merging increateConditionalExpressions. This creates O(N * M) conditional expressions (where M is the total number of differing expression types, also O(N)), resulting in O(N^2) conditional expressions. These conditional expressions are then processed during every subsequent type narrowing operation in a while-loop fixed-point iteration.PossiblyImpureCallExprentries should not participate in the conditional expression system because they store fixed declared return types (metadata for tip generation), not control-flow-dependent types that need narrowing across branches.Test
The regression test (
tests/PHPStan/Analyser/nsrt/bug-14452.php) reproduces the issue pattern from the bug report: a class with many possibly-impure method calls (MyBag::getInt(),MyBag::has(),MyBag::get()) inside conditional branches with nested ifs. Without the fix, the test times out (>60s). With the fix, it completes in ~0.2s.Fixes phpstan/phpstan#14452