Repository navigation
Track ExpressionTypeResolverExtension declines per type flavour in ExpressionResult - #6831
Open
phpstan-bot wants to merge 1 commit into
Open
phpstan-bot wants to merge 1 commit into
phpstan-bot wants to merge 1 commit into
Conversation
…`ExpressionResult` - ExpressionResult kept a single `$extensionsDeclined` flag. Once every extension returned null on one flavour (e.g. native, where a PHPDoc-only typed receiver is `mixed`), extensions were never asked again on the other flavour, so a foreach over the expression fell back to the property's native `array` type and the value became `mixed`. - Add `$nativeExtensionsDeclined`; consultExpressionTypeResolverExtensions() picks the flag from the read scope's `nativeTypesPromoted`, so getType(), getNativeType() and getTypeOnScope() each honour only the decline of their own flavour. The flag is carried through finalize(), the from-scope copy and onNonNullabilityDevicedScopes(). - Covers both directions: a native decline no longer hides the PHPDoc answer, and a PHPDoc decline no longer hides the native answer. - Ported to the turbo-ext mirror (turbo-ext/src/ExpressionResult.cpp, regenerated turbo-ext/src/generated/ExpressionResult.h). - Probed the other per-flavour memo slots of ExpressionResult (cachedType / cachedNativeType, resolvedType / resolvedNativeType, projectedType / projectedNativeType): they are already split per flavour. The decline flag was the only state shared between them.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
An
ExpressionTypeResolverExtensionwhose answer depends on the receiver's type could be skipped for one type flavour after it declined the other. In the reported case,$orderisOrderonly by PHPDoc. The extension answered for the PHPDoc scope and declined for the native scope (the receiver ismixedthere). After thatExpressionResultstopped asking it at all, soforeach ($order->lines as $line)typed$lineasmixedand not asLine.This PR records extension declines separately for each flavour.
Changes
src/Analyser/ExpressionResult.php: new$nativeExtensionsDeclinedflag (constructor parameter, defaults tofalse), next to the existing$extensionsDeclined, which now means "declined on the PHPDoc flavour".consultExpressionTypeResolverExtensions()chooses the flag from$readScope->nativeTypesPromoted.getNativeType()checks the native flag.finalize(), the from-scope copy andonNonNullabilityDevicedScopes()carry both flags over.turbo-ext/src/ExpressionResult.cpp+ regeneratedturbo-ext/src/generated/ExpressionResult.h: the same change in the native mirror (new slot, constructor argument, per-flavour slot selection inconsultExpressionTypeResolverExtensions(),getNativeType()guard, copies).ExpressionResult(cachedType/cachedNativeType,resolvedType/resolvedNativeType,projectedType/projectedNativeType) were already split per flavour. The decline flag was the only state the two flavours shared.Root cause
The one-round "every extension declined" cache treated a decline as a fact about the expression alone. But extensions are asked with a PHPDoc-flavoured and a native-flavoured scope, and their answer can depend on that scope (the receiver's type differs between the two). A decline on one flavour blocked the other flavour, in both directions:
getType()) hid a native answer.Keeping one flag per flavour keeps the performance benefit (each flavour still asks only once) without mixing the two.
Test
tests/PHPStan/Analyser/data/bug-15496.phpwithBug15496ExpressionTypeResolverExtension, registered inexpression-type-resolver-extension.neonand run byExpressionTypeResolverExtensionTest:Linein the PHPDoc flavour; a natively typed receiver →Linein both flavours);assertNativeType('Bug15496\Line', ...)).make testspasses with and without the extension loaded, and turbosmoke.php,signature-parity.phpandside-by-side.phppass.Fixes phpstan/phpstan#15496
🤖 Generated with Claude Code