Skip to content

Fix phpstan/phpstan#13416: Incorrectly narrowed to previously asserted type, even if marked impure#5112

Closed
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-418z048
Closed

Fix phpstan/phpstan#13416: Incorrectly narrowed to previously asserted type, even if marked impure#5112
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-418z048

Conversation

@phpstan-bot
Copy link
Collaborator

Summary

When assert(count(MyRecord::find()) === 1) narrowed the type of count(MyRecord::find()) to 1, subsequent calls to count(MyRecord::find()) incorrectly reused this narrowed type — even though MyRecord::find() is marked @phpstan-impure and could return different values on each call. This caused false positives like "comparison will always evaluate to false" when comparing the count to a different value after modifying state.

Changes

  • Added containsImpureCall() private method to src/Analyser/TypeSpecifier.php that recursively checks whether an expression tree contains any impure function, method, or static method calls
  • In createForExpr(), after verifying the top-level call is pure, now also checks if the call's arguments contain impure sub-expressions before remembering the narrowed type
  • Applied the check to all three call type branches: FuncCall, MethodCall, and StaticCall

Root cause

TypeSpecifier::createForExpr() determines whether an expression's narrowed type should be remembered in the scope. For function calls like count(X), it checked if count() itself was pure (it is), but did not check if X contained impure calls. Since MyRecord::find() is marked @phpstan-impure, the expression count(MyRecord::find()) should not have its type remembered across different evaluation points — each call to MyRecord::find() can return a different array, so the count can differ.

The fix adds a recursive check of all sub-expressions within the arguments of a pure function call. If any sub-expression is an impure function/method/static call, the entire expression's type is not remembered.

Test

Added tests/PHPStan/Analyser/nsrt/bug-13416.php which reproduces the issue: after assert(count(MyRecord::find()) === 1) and an impure insert() call, count(MyRecord::find()) should be int<0, max> (not 1).

Fixes phpstan/phpstan#13416

- TypeSpecifier::createForExpr() checked if the top-level function call was pure before remembering its type, but did not check whether arguments contained impure sub-expressions
- Added containsImpureCall() helper to recursively detect impure function/method/static calls within expression trees
- Applied the check to FuncCall, MethodCall, and StaticCall branches in createForExpr()
- Added regression test in tests/PHPStan/Analyser/nsrt/bug-13416.php

Closes phpstan/phpstan#13416
@staabm staabm closed this Mar 1, 2026
@staabm staabm deleted the create-pull-request/patch-418z048 branch March 1, 2026 08:52
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