Skip to content

Consult ExpressionTypeResolverExtensions in MutatingScope::resolveScopeStateType() - #6816

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

phpstan-bot wants to merge 1 commit into
phpstan:2.3.xfrom
phpstan-bot:create-pull-request/patch-lt8538i

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

Since 2.3.0, $crate->holder->day?->toDateString() was reported as method.nonObject ("Cannot call method toDateString() on Day|null") when the type of $crate->holder came from an ExpressionTypeResolverExtension (e.g. Pest's TestCaseDynamicPropertyTypeExtension). This change makes the scope's state-type read ask the extensions, the same way the regular type resolution does.

Changes

  • src/Analyser/MutatingScope.php: resolveScopeStateType() now asks ExpressionTypeResolverExtensions first for non-variable expressions, mirroring resolveType(). For the native flavour it asks on the native-promoted scope.
  • turbo-ext/src/MutatingScope.cpp: the same change in the native mirror.
  • Tests: the reproducer from the issue, plus the analogous constructs, run with an extension that types Crate::$holder as Holder.

Root cause

The nullsafe handlers, ??, isset()/empty() and condition narrowing make the receiver non-null with NonNullabilityHelper and MutatingScope::getStateType(). That read works out the type from tracked state and reflection, and it skipped the extension hook on purpose. resolveType() asks the extensions before anything else. So for $crate->holder->day, the state read saw $crate->holder as mixed (from the universal object crate) instead of the extension's Holder. The non-null ensure then had no nullable type to narrow, and the plain twin of the ?-> call was checked against the real Day|null.

The same mismatch affected every user of the state read. Before the fix these also failed:

  • chained $crate->holder->day?->next()?->next(): method.nonObject reported twice
  • $crate->holder->days['day']?->date: "Cannot access property $date on Day|null"
  • $crate->holder->day->date ?? null was inferred as mixed instead of string|null
  • if ($crate->holder->day !== null) narrowed to mixed~null instead of Day

I also checked ?-> on a method-call receiver ($crate->holder->getDay()?->…), a nullsafe property fetch directly on the extension-typed chain, and isset()/empty(). Those already worked; they stay in the test file as coverage.

Test

  • tests/PHPStan/Analyser/data/bug-15421.php, with an extension in tests/PHPStan/Analyser/data/Bug15421ExpressionTypeResolverExtension.php registered in expression-type-resolver-extension.neon:
    • type assertions in ExpressionTypeResolverExtensionTest
    • CallMethodsRuleExpressionTypeResolverExtensionTest: no errors expected; four method.nonObject errors before the fix
    • AccessPropertiesRuleExpressionTypeResolverExtensionTest: no errors expected; one error before the fix
  • Full test suite passes with and without the turbo extension. turbo-ext/tests/smoke.php and walk-trace.php report identical behaviour.

Fixes phpstan/phpstan#15421

…ScopeStateType()`

- The state-type read (getStateType()) used by the non-nullability ensure
  of `?->`, `??`, isset()/empty() and by narrowing skipped expression type
  resolver extensions, unlike resolveType(). A receiver typed only by an
  extension (e.g. a universal object crate property) was read as mixed, so
  `$crate->holder->day` was never ensured non-null and the plain twin of
  `$crate->holder->day?->toDateString()` was checked against `Day|null`.
- Non-variable expressions now ask the extensions first (on the native-
  promoted scope for the native flavour), mirroring resolveType().
- Ported the same change to the turbo-ext mirror (MutatingScope.cpp).
- The same root cause also broke, and this fixes: chained `?->` over an
  extension-typed receiver, `?->` property fetch on an offset of it,
  `$crate->holder->day->date ?? null` (was mixed), and `!== null` narrowing
  of `$crate->holder->day` (was mixed~null).
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 method.nonObject on nullsafe call when the receiver type comes from an ExpressionTypeResolverExtension (2.3.0 regression)

1 participant