Skip to content

Fix phpstan/phpstan#14319: PHPStan takes extremely long analyzing if-offset checks on array|object#5242

Merged
VincentLanglet merged 10 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-cltr70w
Mar 18, 2026
Merged

Fix phpstan/phpstan#14319: PHPStan takes extremely long analyzing if-offset checks on array|object#5242
VincentLanglet merged 10 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-cltr70w

Conversation

@phpstan-bot
Copy link
Collaborator

Summary

PHPStan hangs or takes extremely long when analyzing code with multiple consecutive if ($rows['key']) blocks where $rows has type array|object. The analysis time grows exponentially with the number of if-blocks (6 blocks → 12+ seconds, effectively hanging for real-world code with more blocks).

Changes

  • Added ARRAY_DIM_FETCH_UNION_TYPE_LIMIT = 8 constant to src/Analyser/MutatingScope.php
  • In specifyExpressionType(), when processing ArrayDimFetch expressions, skip propagating HasOffsetValueType intersections and generalOffsetAccessibleType intersections to the parent variable when the parent type is already a UnionType with more than 8 members
  • Added regression test tests/PHPStan/Analyser/nsrt/bug-14319.php with assertType checks
  • Added benchmark test tests/bench/data/bug-14319.php for performance regression tracking

Root cause

When processing if ($rows['key']) where $rows is array|object, specifyExpressionType narrows the parent variable $rows by intersecting it with generalOffsetAccessibleType (converting object to ArrayAccess) and HasOffsetValueType('key', <truthy/falsy type>). This creates an intersection type for each union member.

After merging the truthy and falsy branches of each if-block, the type of $rows becomes a union of both branch types, doubling the number of union members. With N consecutive if-blocks checking different keys, the type grows to O(2^N) union members. Each subsequent TypeCombinator::intersect call on this growing union becomes exponentially slower.

The fix introduces a complexity limit: when the parent variable's type already has more than 8 union members, the narrowing propagation to the parent is skipped. The dim fetch expression itself ($rows['key']) is still narrowed correctly, so analysis precision is maintained for the accessed value. Performance improves from 12+ seconds to ~1 second for the reproduction case.

Test

The regression test bug-14319.php reproduces the exact scenario from the issue: a function returning array|object, followed by 6 consecutive if ($rows['key']) blocks with .= concatenation. It includes assertType checks to verify that type inference still works correctly (variable types are properly inferred as string after the operations).

Fixes phpstan/phpstan#14319

@staabm
Copy link
Contributor

staabm commented Mar 18, 2026

before this PR

time php bin/phpstan analyze bug-14319.php --debug

8.90s user 0.30s system 99% cpu 9.214 total

with this PR

time php bin/phpstan analyze bug-14319.php --debug

0.99s user 0.23s system 99% cpu 1.816 total

@staabm staabm self-assigned this Mar 18, 2026
@staabm staabm changed the title Fix phpstan/phpstan#14319: PHPStan hangs and times out, possible infinite loop Fix phpstan/phpstan#14319: PHPStan takes extremely long analyzing if-offset checks on array|object Mar 18, 2026
@kamil-tekiela
Copy link
Contributor

I confirm that this fixes the issue. When using the new phar the analyse run completes in reasonable time.

@clxmstaab clxmstaab force-pushed the create-pull-request/patch-cltr70w branch from 204ad22 to 718cca9 Compare March 18, 2026 13:39
phpstan-bot and others added 8 commits March 18, 2026 14:43
…im fetch narrowing

- Added ARRAY_DIM_FETCH_UNION_TYPE_LIMIT (8) to MutatingScope to cap parent type narrowing
- When array dim fetch narrows a parent variable type that is already a UnionType with
  more than 8 members, skip propagating HasOffsetValueType intersections to the parent
- This prevents 2^N exponential growth when N consecutive if-blocks check different array
  offsets on an array|object typed variable
- New regression test in tests/PHPStan/Analyser/nsrt/bug-14319.php
- New benchmark test in tests/bench/data/bug-14319.php
Instead of counting all union type members, only count union members
that contain HasOffsetValueType accessories. This more precisely
targets the source of exponential growth while avoiding false
positives on unions that are large for other reasons.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@staabm staabm force-pushed the create-pull-request/patch-cltr70w branch from 0cda4c2 to 87f29af Compare March 18, 2026 13:43
@staabm staabm requested a review from VincentLanglet March 18, 2026 13:44
$exprVarType = $scope->getType($expr->var);
$isArray = $exprVarType->isArray();
if (!$exprVarType instanceof MixedType && !$isArray->no()) {
$tooComplex = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The var name si too vague.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Renamed $tooComplex to $tooManyHasOffsetValueTypes to better describe what the variable tracks — whether the union type has exceeded the limit of HasOffsetValueType accessory types. All tests pass (11602 tests) and static analysis reports no errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Collaborator Author

The branch is clean and up to date. The PR is approved with no outstanding change requests. There's nothing to address — all previous feedback has been implemented and the reviewer has approved the PR.

@phpstan-bot
Copy link
Collaborator Author

The review from VincentLanglet is an APPROVED review with no new comments or change requests. All previous feedback has already been addressed:

  • $tooComplex was renamed to $tooManyHasOffsetValueTypes (VincentLanglet's earlier comment)
  • The check now counts HasOffsetValueType accessories specifically rather than just union member count (staabm's earlier comment)
  • The constant was renamed to ARRAY_DIM_FETCH_UNION_HAS_OFFSET_VALUE_TYPE_LIMIT

There's nothing new to address — the PR is approved and ready to merge.

@VincentLanglet VincentLanglet merged commit 7e07096 into phpstan:2.1.x Mar 18, 2026
652 of 654 checks passed
@VincentLanglet VincentLanglet deleted the create-pull-request/patch-cltr70w branch March 18, 2026 15:46
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.

4 participants