Skip to content

Fix phpstan/phpstan#14452: Performance degradation introduced in 2.1.41#5441

Merged
staabm merged 3 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-slldpy5
Apr 11, 2026
Merged

Fix phpstan/phpstan#14452: Performance degradation introduced in 2.1.41#5441
staabm merged 3 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-slldpy5

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

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 PossiblyImpureCallExpr entries participating in the conditional expression system during scope merges, creating O(N^2) conditional expressions that were then repeatedly processed during type narrowing.

Changes

  • Modified MutatingScope::createConditionalExpressions() in src/Analyser/MutatingScope.php to skip PossiblyImpureCallExpr entries in three places:
    1. Excluded from the $typeGuards set (prevents O(N^2) conditional expression creation)
    2. Excluded from the $newVariableTypes loop that creates conditional expressions for variables
    3. Excluded from the $mergedExpressionTypes loop that creates ErrorType conditional expressions
  • Added regression test tests/PHPStan/Analyser/nsrt/bug-14452.php

Root cause

The commit cf070ae ("Tip about adding @phpstan-impure where applicable") introduced PossiblyImpureCallExpr nodes 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 if block contains N possibly-impure method calls, all N PossiblyImpureCallExpr entries become type guards during scope merging in createConditionalExpressions. 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.

PossiblyImpureCallExpr entries 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

staabm and others added 3 commits April 11, 2026 06:46
…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
@staabm staabm requested a review from VincentLanglet April 11, 2026 07:28
@staabm
Copy link
Copy Markdown
Contributor

staabm commented Apr 11, 2026

with this fix applied, the "never ending case" from the reproducer in bug 14452 ends in 0.17 seconds.

reason for the slowness were lots of conditional types based on virtual-nodes (possibly-impure-method-call)

grafik

@staabm staabm merged commit 378f49c into phpstan:2.1.x Apr 11, 2026
656 checks passed
@staabm staabm deleted the create-pull-request/patch-slldpy5 branch April 11, 2026 08:28
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