Skip to content

Refine remembered method calls and property fetches when their receiver is narrowed#6008

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

Refine remembered method calls and property fetches when their receiver is narrowed#6008
phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-odmxn13

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

After a control-flow narrowing recorded a type for a method call or property
fetch (for example, $x->getValue() narrowed to int|string after an
if ($x->getValue() === null) { return; }), a later instanceof check that
made the receiver ($x) more specific did not update that remembered type.
The call/fetch kept its stale, broader type instead of combining it with the
type derived from the now-narrower receiver.

This meant e.g. assertType('string', $x->getValue()) inside
if ($x instanceof StringValue) reported int|string even though
StringValue::getValue() covariantly returns string.

Changes

  • src/Analyser/MutatingScope.php
    • New private refreshDependentCallsAndFetches(array $narrowedExprStrings):
      scans the remembered expression types for MethodCall,
      NullsafeMethodCall, PropertyFetch and NullsafePropertyFetch whose
      immediate receiver (->var) was just narrowed, re-derives their type from
      the current (narrower) receiver via the expression handlers, and intersects
      it with the remembered type. Both PHPDoc and native expression types are
      refreshed.
    • New private resolveExprHandlerType() helper that resolves an expression's
      type through the ExprHandler registry, bypassing the memoized entry.
    • filterBySpecifiedTypes() now calls the refresh with the set of expressions
      that were narrowed by the condition.
  • tests/PHPStan/Analyser/nsrt/bug-10050.php — regression test with both a
    method-call case (covariant getValue() override across interfaces) and a
    property-fetch case (A|B receiver narrowed to A).

Root cause

Remembered (control-flow narrowed) types for calls/fetches are stored against
the receiver type that was current at the time of narrowing. When the receiver
is later narrowed further, the remembered type is not a subtype of the newly
derivable type, and nothing recombined the two. The reported issue was one
instance (method calls); the same pattern affects property fetches, so both are
handled by keying the refresh on receiver narrowing.

Doing this by receiver narrowing — rather than by unconditionally recomputing
call/fetch types on every read — is important: property fetches can also be
memoized by direct assignment ($this->foo = new Fooo()), where the stored
type may be a covariant subtype that is intentionally not compatible with the
declared property type. Those are not driven by receiver narrowing, so they are
left untouched (verified by tests/PHPStan/Rules/Properties/data/bug-3777.php).

Test

  • tests/PHPStan/Analyser/nsrt/bug-10050.php:
    • testMethod() reproduces the reported bug: $v->get() is string inside
      if ($v instanceof StringValue) after null was removed earlier.
    • testProperty() covers the analogous property-fetch case: $x->p is int
      inside if ($x instanceof A) for an A|B receiver.
    • Both assertions fail before the fix (int|string) and pass after.
  • Full test suite (make tests) and self-analysis (make phpstan) pass.

Fixes phpstan/phpstan#10050

…er is narrowed

- Add `MutatingScope::refreshDependentCallsAndFetches()`, called at the end of
  `filterBySpecifiedTypes()`, which re-derives every remembered `MethodCall`,
  `NullsafeMethodCall`, `PropertyFetch` and `NullsafePropertyFetch` whose
  receiver was just narrowed, and intersects the remembered (control-flow
  narrowed) type with the type derived from the now-narrower receiver.
- This fixes the case where an earlier control-flow narrowing (e.g. removing
  `null` after `=== null`) recorded a call/fetch type against a broader
  receiver, and a later `instanceof` (or any condition narrowing) made the
  receiver more specific without refreshing the dependent call/fetch.
- Refresh both PHPDoc and native expression types.
- The refresh is driven by receiver narrowing (only from `filterBySpecifiedTypes`),
  so it never touches call/fetch types recorded by direct assignment to a
  property (which may legitimately hold a covariant subtype not compatible with
  the declared property type).
- Covers the property-fetch analogue of the reported method-call bug.
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.

missing return type narrowing on instance of

2 participants