Skip to content

Do not remember an equality assertion statement as true when its arguments contain an impure call - #6628

Closed
phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-93sz6cq
Closed

phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-93sz6cq

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

Since 2.2.15, calling a templated @phpstan-assert =ExpectedType $actual assertion (like PHPUnit's assertSame()) twice with an impure call as an argument reported the second call as alreadyNarrowedType. An example is assertSame($key, $counter->next()) where next() is @phpstan-impure, or random_int(). This PR stops PHPStan from remembering the assertion's true outcome when the call's arguments contain a call that isn't known to be pure.

Changes

  • src/Analyser/TypeSpecifier.php: add callOperandsContainNonPureCall(). It runs the existing non-pure-call search (findNonPureCall, now split so the sub-node walk can be reused) over the call's arguments and over what it is called on, without the call itself.
  • src/Analyser/StmtHandler/ExpressionHandler.php: only assignExpression() the equality assertion statement as true when its operands contain no non-pure call.
  • Analogous cases:
    • Function, method and static method assertion calls were all affected. All three are fixed and tested.
    • The impure call can be in either argument ($expected or $actual). Both positions are covered.
    • I probed the bool-returning @phpstan-assert-if-true =T variant used in a condition. It was already correct, because MutatingScope::filterByTruthyValue() stores the result through TypeSpecifier::create(), which already refuses expressions containing non-pure calls.

Root cause

Commit 14ad31b added storing a void equality-assertion statement's result as true, so that an exact duplicate assertion is reported as always true. Unlike the truthy/falsey path, it used assignExpression() directly. That bypasses the purity check TypeSpecifier::create() normally applies before remembering an expression's value. Because the expression key of assertSame($key, $counter->next()) is identical for both calls, the second call looked up the remembered true. The whole call can't be checked with the regular purity check, because a void assertion function is itself considered impure. So the fix checks only the call's operands.

Test

tests/PHPStan/Rules/Comparison/data/bug-15328.php contains the reproducer from the issue (impure method and random_int()), with an impure $expected argument, plus the same scenarios for a static-method and an instance-method assertion. It is analysed by ImpossibleCheckTypeFunctionCallRuleTest, ImpossibleCheckTypeMethodCallRuleTest and ImpossibleCheckTypeStaticMethodCallRuleTest. Each file also has a duplicate assertion with pure arguments, which is still reported. All three tests fail without the fix.

Fixes phpstan/phpstan#15328

…guments contain an impure call

- ExpressionHandler stored the result of a void `@phpstan-assert =T` call
  statement as `true`, so that a duplicate assertion is reported as always
  true. It did this with assignExpression(), which skips the purity check that
  TypeSpecifier::create() applies. A repeated `assertSame($key, $counter->next())`
  or `assertSame($key, random_int(1, 10))` was then wrongly reported as
  alreadyNarrowedType.
- Add TypeSpecifier::callOperandsContainNonPureCall(). It reuses the existing
  non-pure-call search on the call's arguments and on what the call is made
  on, without the call itself. The void assertion call is usually impure
  itself.
- Only store `true` for the statement when that check passes. Function,
  method and static method assertions are all covered. Repeated assertions
  with pure arguments are still reported.
- Probed the bool-returning `@phpstan-assert-if-true =T` variant in a
  condition (filterByTruthyValue path). It is already correct because it goes
  through TypeSpecifier::create(), which rejects impure expressions.
@staabm staabm closed this Sep 29, 2026
@staabm
staabm deleted the create-pull-request/patch-93sz6cq branch September 29, 2026 13:41
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