Skip to content

Fix phpstan/phpstan#12163: non-negative-int evaluated to int#5176

Open
phpstan-bot wants to merge 3 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-sxca8e1
Open

Fix phpstan/phpstan#12163: non-negative-int evaluated to int#5176
phpstan-bot wants to merge 3 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-sxca8e1

Conversation

@phpstan-bot
Copy link
Collaborator

Summary

When a variable was assigned 0 and then modified inside a for loop (incremented or reset to 0), PHPStan incorrectly generalized its type to int instead of preserving int<0, max> (non-negative-int). This caused false positive errors like "expects int<0, max>, int given".

Changes

  • Modified MutatingScope::generalizeType() in src/Analyser/MutatingScope.php
  • Changed the constant integers generalization logic for the case when values expand in both directions (gotGreater && gotSmaller)
  • Instead of immediately widening to IntegerType (plain int), the fix computes the actual observed min/max bounds from both iterations using IntegerRangeType::fromInterval($newMin, $newMax)
  • Added regression test in tests/PHPStan/Analyser/nsrt/bug-12163.php

Root cause

In generalizeType(), constant integer values from consecutive loop iterations are compared. If values in the current iteration are both greater AND smaller than the previous iteration's range, the code assumed both bounds were unstable and widened to int.

In the reported case, $columnIndex started at 0, was incremented ($columnIndex++) in one branch and reset to 0 in another. After the first loop iteration, the type was 1 (only the increment path was reachable). After the second iteration (merged with init scope), it became 0|1|2. The generalization saw 10|1|2 — values grew in both directions — and concluded int.

The fix computes the actual combined bounds (int<0, 2>) instead. On the next generalization iteration, this becomes an IntegerRangeType, which the integer range generalization logic handles correctly — it sees only upward growth and produces int<0, max>. For truly unbounded cases (both directions growing indefinitely), the integer range generalization in the subsequent iteration still correctly produces int.

Test

Added tests/PHPStan/Analyser/nsrt/bug-12163.php which reproduces the original issue: a for loop with two index variables where one is incremented or reset to 0. Both $rowIndex and $columnIndex are asserted to be int<0, max>.

Fixes phpstan/phpstan#12163

github-actions bot and others added 3 commits March 9, 2026 17:23
- Fixed overly aggressive type generalization in MutatingScope::generalizeType()
  for constant integers when values expand in both directions across loop iterations
- Instead of widening to plain `int`, now computes actual observed bounds, allowing
  the next iteration to correctly determine stable vs growing bounds
- New regression test in tests/PHPStan/Analyser/nsrt/bug-12163.php

Closes phpstan/phpstan#12163
@staabm
Copy link
Contributor

staabm commented Mar 10, 2026

@phpstan-bot the fix and test applies to lines 4115-4127 in MutatingScope. for symmetry we need the same fix, covered by a test, in line 4207 of MutatingScope

@staabm staabm self-assigned this Mar 10, 2026
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.

2 participants