Skip to content

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

Merged
staabm merged 1 commit into
phpstan:2.3.xfrom
phpstan-bot:create-pull-request/patch-52tts3g
Sep 29, 2026
Merged

staabm merged 1 commit into
phpstan:2.3.xfrom
phpstan-bot:create-pull-request/patch-52tts3g

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

Since 2.2.15, calling a void assertion annotated with @phpstan-assert =ExpectedType $actual (e.g. PHPUnit's assertSame()) twice with an impure call as an argument reported the second call as alreadyNarrowedType:

assertSame($key, $counter->next()); // @phpstan-impure
assertSame($key, $counter->next()); // "will always evaluate to true"

The statement handler remembered the whole assertion call as true, even though the impure argument gives a new value on every call. Now it only does that when the call's arguments contain no impure call.

Changes

  • src/Analyser/StmtHandler/ExpressionHandler.php: store the statement's true result for an equality assertion only when isCallRememberedDespiteItsOwnImpurity() holds. This means no impure point other than the call's own (a void call is always impure) comes from a function, method, static or nullsafe method call. Possibly-impure calls are skipped when rememberPossiblyImpureFunctionValues is enabled, which matches DefaultNarrowingHelper::isSubjectValueRemembered(). The parameter is autowired into the constructor.
  • turbo-ext/src/ExpressionHandler.cpp and turbo-ext/src/generated/ExpressionHandler.h: the same gate and the new constructor parameter in the native mirror.

Root cause

ExpressionHandler assigned true to the assertion call expression when the statement's narrowing was an equality narrowing. It never checked whether the call could be repeated with the same outcome. The first assertSame($key, random_int(1, 10)) stored assertSame($key, random_int(1, 10)) as true. Nothing invalidated that entry, because random_int() touches no variable. So ImpossibleCheckTypeHelper read the stored true for the second call. With $counter->next() an intervening $counter->next(); statement invalidated the entry, which matches the observations in the issue.

Test

  • tests/PHPStan/Rules/Comparison/data/bug-15328.php (ImpossibleCheckTypeFunctionCallRuleTest::testBug15328) contains the reproducer from the issue: an impure method and an impure function as $actual. It adds an impure call as $expected (the narrowed $actual must not make the repeat look redundant) and a pure duplicate, which is still reported.
  • tests/PHPStan/Rules/Comparison/data/bug-15328-method.php (ImpossibleCheckTypeMethodCallRuleTest and ImpossibleCheckTypeStaticMethodCallRuleTest::testBug15328) covers the PHPUnit-style analogues $this->assertSame() and self::assertSame(), with impure and pure arguments.

All the new expectations failed before the fix.

I also probed the if counterpart, @phpstan-assert-if-true =Type inside nested ifs with random_int(). The inner duplicate is still reported. That comes from the default narrowing of any call used as a condition: if (isFive(random_int(1, 10))) also remembers the call as true. That narrowing checks only the called function's own purity, not its arguments. The behaviour predates this regression, so this PR leaves it unchanged.

Fixes phpstan/phpstan#15328

🤖 Generated with Claude Code

…arguments contain an impure call

- ExpressionHandler stores the result of a void `@phpstan-assert =Type`
  call statement as `true` so a duplicate assertion is reported as
  always-true. It did so even when an argument contained an impure call
  (`assertSame($key, $counter->next())`, `assertSame($key, random_int(1, 10))`),
  so the repeated assertion was wrongly reported as always true.
- The statement is now remembered only when none of its impure points other
  than the call's own (a void call is always impure) come from a call - the
  same gate DefaultNarrowingHelper::isSubjectValueRemembered() applies,
  honouring rememberPossiblyImpureFunctionValues for possibly-impure calls.
- Ported the gate to the native ExpressionHandler mirror in turbo-ext.
- Covered function, instance method (`$this->assertSame()`) and static
  method (`self::assertSame()`) assertions, impure calls in both the
  asserted and the expected argument, and kept the duplicate report for
  pure arguments.
- Probed the `if (isSame($key, random_int(1, 10)))` counterpart
  (`@phpstan-assert-if-true =Type`): the nested duplicate is still reported,
  but that comes from the long-standing default narrowing of any call in a
  condition, which gates only on the called function's own purity, not on
  its arguments - a separate, pre-existing behaviour left unchanged here.
@staabm

staabm commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

//cc @SanderMuller

@VincentLanglet

Copy link
Copy Markdown
Contributor

This seems ok, but should target 2.2.x to fix the 2.2.15 release IMHO @staabm.

@staabm

staabm commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

@VincentLanglet in https://github.com/phpstan/phpstan-src/pull/6628/files there is a 2.2.x variant of this.

I am not sure we should merge it though, as its a fix in classes which largely changed in 2.3.x which means this will get hard to merge (and 2.3.0 will ship likely this week)

@VincentLanglet

Copy link
Copy Markdown
Contributor

I am not sure we should merge it though, as its a fix in classes which largely changed in 2.3.x which means this will get hard to merge (and 2.3.0 will ship likely this week)

Sure I'm ok waiting for 2.3.0 if we/ondrej doesn't think it's a big enough bug.

The fix in https://github.com/phpstan/phpstan-src/pull/6628/changes seems simpler than this one, but maybe it's different because classes changed (?)

@staabm

staabm commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

merging per PM with ondrej.

@staabm
staabm merged commit 005b513 into phpstan:2.3.x Sep 29, 2026
503 of 537 checks passed
@staabm
staabm deleted the create-pull-request/patch-52tts3g branch September 29, 2026 13:40
@staabm

staabm commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

The fix in https://github.com/phpstan/phpstan-src/pull/6628/changes seems simpler than this one, but maybe it's different because classes changed (?)

yes, its using a method which was removed in 2.3.x

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.

False positive alreadyNarrowedType for repeated @phpstan-assert =ExpectedType assertion on impure call

3 participants