@@ -760,7 +760,7 @@ private function doPrepareTarget(
760760 // 1b. build the write chain (Set*OffsetValueTypeExpr nesting) after
761761 // the root walk, so a property base's holder and current types are
762762 // read from its stored result instead of pricing the unwalked fetch
763- $ assignedPropertyExpr = $ assignedExpr ;
763+ $ assignedPropertyExpr = $ this -> getWrittenExpr ( $ originalVar , $ assignedExpr) ;
764764 $ chainVar = $ originalVar ;
765765 while ($ chainVar instanceof ArrayDimFetch) {
766766 $ varForSetOffsetValue = $ chainVar ->var ;
@@ -1374,7 +1374,7 @@ public function applyWrite(
13741374 $ conditionalExpressions = $ this ->processSureNotTypesForConditionalExpressionsAfterAssign ($ nodeScopeResolver , $ scope , $ storage , $ var ->name , $ conditionalExpressions , $ identicalSpecifiedTypes , $ falseyType , $ impurePoints , $ assignedExpr , $ storedAssignedExprResult );
13751375 }
13761376
1377- $ nodeScopeResolver ->callNodeCallback ($ nodeCallback , new VariableAssignNode ($ var , $ assignedExpr ), $ scopeBeforeAssignEval , $ storage );
1377+ $ nodeScopeResolver ->callNodeCallback ($ nodeCallback , new VariableAssignNode ($ var , $ this -> getWrittenExpr ( $ var , $ assignedExpr) ), $ scopeBeforeAssignEval , $ storage );
13781378
13791379 $ nativeType = $ this ->readAssignedValueType ($ nodeScopeResolver , $ storedAssignedExprResult , $ assignedExpr , $ scope ->doNotTreatPhpDocTypesAsCertain ());
13801380 $ scope = $ scope ->assignVariable (
@@ -1596,7 +1596,7 @@ public function applyWrite(
15961596 if ($ templateArgumentFrame !== null ) {
15971597 $ scope = $ scope ->addTemplateArgumentConstraints ($ this ->templateArgumentObserver ->collectSend ($ propertyReflection ->getWritableType (), $ assignedExprType ));
15981598 }
1599- $ nodeScopeResolver ->callNodeCallback ($ nodeCallback , new PropertyAssignNode ($ var , $ assignedExpr , $ isAssignOp ), $ scopeBeforeAssignEval , $ storage );
1599+ $ nodeScopeResolver ->callNodeCallback ($ nodeCallback , new PropertyAssignNode ($ var , $ this -> getWrittenExpr ( $ var , $ assignedExpr) , $ isAssignOp ), $ scopeBeforeAssignEval , $ storage );
16001600 if ($ propertyReflection ->canChangeTypeAfterAssignment ()) {
16011601 if ($ propertyReflection ->hasNativeType ()) {
16021602 $ propertyNativeType = $ propertyReflection ->getNativeType ();
@@ -1657,7 +1657,7 @@ public function applyWrite(
16571657 } else {
16581658 // fallback
16591659 $ assignedExprType = $ this ->readAssignedValueType ($ nodeScopeResolver , $ assignedValueResult , $ assignedExpr , $ scope );
1660- $ nodeScopeResolver ->callNodeCallback ($ nodeCallback , new PropertyAssignNode ($ var , $ assignedExpr , $ isAssignOp ), $ scopeBeforeAssignEval , $ storage );
1660+ $ nodeScopeResolver ->callNodeCallback ($ nodeCallback , new PropertyAssignNode ($ var , $ this -> getWrittenExpr ( $ var , $ assignedExpr) , $ isAssignOp ), $ scopeBeforeAssignEval , $ storage );
16611661 $ scope = $ scope ->assignExpression ($ var , $ assignedExprType , $ this ->readAssignedValueType ($ nodeScopeResolver , $ assignedValueResult , $ assignedExpr , $ scope ->doNotTreatPhpDocTypesAsCertain ()));
16621662 // simulate dynamic property assign by __set to get throw points
16631663 if (!$ propertyHolderType ->hasMethod ('__set ' )->no ()) {
@@ -1693,7 +1693,7 @@ public function applyWrite(
16931693 $ scope = $ scope ->addTemplateArgumentConstraints ($ this ->templateArgumentObserver ->collectSend ($ propertyReflection ->getWritableType (), $ assignedExprType ));
16941694 }
16951695 }
1696- $ nodeScopeResolver ->callNodeCallback ($ nodeCallback , new PropertyAssignNode ($ var , $ assignedExpr , $ isAssignOp ), $ scopeBeforeAssignEval , $ storage );
1696+ $ nodeScopeResolver ->callNodeCallback ($ nodeCallback , new PropertyAssignNode ($ var , $ this -> getWrittenExpr ( $ var , $ assignedExpr) , $ isAssignOp ), $ scopeBeforeAssignEval , $ storage );
16971697 if ($ propertyReflection !== null && $ propertyReflection ->canChangeTypeAfterAssignment ()) {
16981698 if ($ propertyReflection ->hasNativeType ()) {
16991699 $ propertyNativeType = $ propertyReflection ->getNativeType ();
@@ -1724,7 +1724,7 @@ public function applyWrite(
17241724 } else {
17251725 // fallback
17261726 $ assignedExprType = $ this ->readAssignedValueType ($ nodeScopeResolver , $ assignedValueResult , $ assignedExpr , $ scope );
1727- $ nodeScopeResolver ->callNodeCallback ($ nodeCallback , new PropertyAssignNode ($ var , $ assignedExpr , $ isAssignOp ), $ scopeBeforeAssignEval , $ storage );
1727+ $ nodeScopeResolver ->callNodeCallback ($ nodeCallback , new PropertyAssignNode ($ var , $ this -> getWrittenExpr ( $ var , $ assignedExpr) , $ isAssignOp ), $ scopeBeforeAssignEval , $ storage );
17281728 $ scope = $ scope ->assignExpression ($ var , $ assignedExprType , $ this ->readAssignedValueType ($ nodeScopeResolver , $ assignedValueResult , $ assignedExpr , $ scope ->doNotTreatPhpDocTypesAsCertain ()));
17291729 }
17301730 } elseif ($ kind === PreparedAssignTarget::KIND_LIST ) {
@@ -2049,6 +2049,18 @@ private function assignDynamicVariable(MutatingScope $scope, ExpressionResult $n
20492049 return $ scope ;
20502050 }
20512051
2052+ /**
2053+ * `$var ??= $value` evaluates to `$var ?? $value`, but writes only `$value` to `$var`.
2054+ */
2055+ private function getWrittenExpr (Expr $ var , Expr $ assignedExpr ): Expr
2056+ {
2057+ if ($ assignedExpr instanceof Expr \AssignOp \Coalesce && $ assignedExpr ->var === $ var ) {
2058+ return $ assignedExpr ->expr ;
2059+ }
2060+
2061+ return $ assignedExpr ;
2062+ }
2063+
20522064 private function unwrapAssign (Expr $ expr ): Expr
20532065 {
20542066 if ($ expr instanceof Assign) {
0 commit comments