From 3e0dc8ab1f9ddf5fd70236146f06b3f91a1ec1c7 Mon Sep 17 00:00:00 2001 From: Gabrieljr42 Date: Fri, 16 Oct 2026 22:51:52 -0300 Subject: [PATCH 1/2] Preserve RHS types during destructuring assignments --- src/Analyser/ExprHandler/AssignHandler.php | 6 ++- tests/PHPStan/Analyser/nsrt/bug-15091.php | 63 ++++++++++++++++++++++ turbo-ext/src/AssignHandler.cpp | 10 ++-- 3 files changed, 72 insertions(+), 7 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-15091.php diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 0a1f5f83e84..4290aba72f5 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -1721,6 +1721,8 @@ public function applyWrite( $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); $isAlwaysTerminating = $result->isAlwaysTerminating(); $scope = $result->getScope(); + $assignedExprType = $this->readAssignedValueType($nodeScopeResolver, $assignedValueResult, $assignedExpr, $scope); + $assignedExprNativeType = $this->readAssignedValueType($nodeScopeResolver, $assignedValueResult, $assignedExpr, $scope->doNotTreatPhpDocTypesAsCertain()); foreach ($var->items as $i => $arrayItem) { if ($arrayItem === null) { continue; @@ -1753,8 +1755,8 @@ public function applyWrite( $nativeDimType = $dimType; } $getOffsetValueTypeExpr = new NativeTypeExpr( - $this->readAssignedValueType($nodeScopeResolver, $assignedValueResult, $assignedExpr, $scope)->getOffsetValueType($dimType), - $this->readAssignedValueType($nodeScopeResolver, $assignedValueResult, $assignedExpr, $scope->doNotTreatPhpDocTypesAsCertain())->getOffsetValueType($nativeDimType), + $assignedExprType->getOffsetValueType($dimType), + $assignedExprNativeType->getOffsetValueType($nativeDimType), ); // store the fabricated result so narrowing walks over the item value // compose from it instead of falling back to on-demand pricing diff --git a/tests/PHPStan/Analyser/nsrt/bug-15091.php b/tests/PHPStan/Analyser/nsrt/bug-15091.php new file mode 100644 index 00000000000..d63d02a44e6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-15091.php @@ -0,0 +1,63 @@ + 1, 'second' => 'value']; + ['first' => $values, 'second' => $second] = $values; + assertType("'value'", $second); +} + +function reassignNestedItem(): void +{ + $values = [['inner', 42], 'last']; + [[$values, $number], $last] = $values; + assertType('42', $number); + assertType("'last'", $last); +} + +/** @param array{string, callable(): int} $value */ +function callableSecond(array $value): void +{ + [$value, $callable] = $value; + assertType('callable(): int', $callable); + assertNativeType('mixed', $callable); + assertType('string', $value); + assertNativeType('mixed', $value); +} + +function sourceInLaterTarget(): void +{ + $values = ['first', 'second']; + [$first, $values] = $values; + assertType("'first'", $first); + assertType("'second'", $values); +} + +function skipSlotAfterReassigningSource(): void +{ + $values = ['first', 'skipped', 'third']; + [$values, , $third] = $values; + assertType("'third'", $third); +} + +function withoutReassigningSource(): void +{ + $values = ['first', 'second']; + [$first, $second] = $values; + assertType("'first'", $first); + assertNativeType("'first'", $first); + assertType("'second'", $second); +} diff --git a/turbo-ext/src/AssignHandler.cpp b/turbo-ext/src/AssignHandler.cpp index f5a81e3b4bc..125c61a9efa 100644 --- a/turbo-ext/src/AssignHandler.cpp +++ b/turbo-ext/src/AssignHandler.cpp @@ -4034,6 +4034,9 @@ class AssignHandler } AH_OKB(pt_expression_result_is_always_terminating(result, isAlwaysTerminating)); AH_SETB(scope, ahResultScope(result)); + AH_VALB(assignedExprType, readAssignedValueType(nsr, assignedValueResult, assignedExpr, scope.raw())); + AH_VALB(nativeScope, pt_mutating_scope_do_not_treat_phpdoc_types_as_certain(Z_OBJ_P(scope.raw()))); + AH_VALB(assignedExprNativeType, readAssignedValueType(nsr, assignedValueResult, assignedExpr, nativeScope.raw())); zval *items = AH_PROP(var, items); if (Z_TYPE_P(items) != IS_ARRAY) return true; zv::Val itemsHolder = zv::Val::copyOf(zv::Ref(items)); @@ -4081,11 +4084,8 @@ class AssignHandler AH_SETB(dimType, newConstantIntegerType((zend_long) entry.indexKey())); nativeDimType = zv::Val::copyOf(dimType.ref()); } - AH_VALB(valueType, readAssignedValueType(nsr, assignedValueResult, assignedExpr, scope.raw())); - AH_VALB(offsetValueType, ahTypeOp(valueType.raw(), PT_OP_GET_OFFSET_VALUE_TYPE, 1, dimType.raw())); - AH_VALB(nativeItemScope, pt_mutating_scope_do_not_treat_phpdoc_types_as_certain(Z_OBJ_P(scope.raw()))); - AH_VALB(nativeValueType, readAssignedValueType(nsr, assignedValueResult, assignedExpr, nativeItemScope.raw())); - AH_VALB(nativeOffsetValueType, ahTypeOp(nativeValueType.raw(), PT_OP_GET_OFFSET_VALUE_TYPE, 1, nativeDimType.raw())); + AH_VALB(offsetValueType, ahTypeOp(assignedExprType.raw(), PT_OP_GET_OFFSET_VALUE_TYPE, 1, dimType.raw())); + AH_VALB(nativeOffsetValueType, ahTypeOp(assignedExprNativeType.raw(), PT_OP_GET_OFFSET_VALUE_TYPE, 1, nativeDimType.raw())); AH_VALB(getOffsetValueTypeExpr, newNativeTypeExpr(offsetValueType.raw(), nativeOffsetValueType.raw())); // store the fabricated result so narrowing walks over the item value // compose from it instead of falling back to on-demand pricing From 45bd1bb574c44e12f09c929faf71dcdef29a4a53 Mon Sep 17 00:00:00 2001 From: Gabrieljr42 Date: Fri, 16 Oct 2026 22:52:01 -0300 Subject: [PATCH 2/2] Bump expected turbo version --- src/Turbo/TurboExtensionEnabler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Turbo/TurboExtensionEnabler.php b/src/Turbo/TurboExtensionEnabler.php index 22991812fa2..1c0dc55d1b8 100644 --- a/src/Turbo/TurboExtensionEnabler.php +++ b/src/Turbo/TurboExtensionEnabler.php @@ -33,7 +33,7 @@ final class TurboExtensionEnabler { - public const EXPECTED_EXTENSION_VERSION = '1cb5faf'; + public const EXPECTED_EXTENSION_VERSION = '3e0dc8a'; private static bool $active = false;