4747use PHPStan \Analyser \SpecifiedTypes ;
4848use PHPStan \Analyser \TypeSpecifierContext ;
4949use PHPStan \Analyser \VarAnnotationProcessor ;
50+ use PHPStan \Analyser \VariableWriteOffset ;
5051use PHPStan \DependencyInjection \AutowiredService ;
5152use PHPStan \Node \Expr \ExistingArrayDimFetch ;
5253use PHPStan \Node \Expr \IntertwinedVariableByReferenceWithExpr ;
@@ -187,8 +188,20 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
187188 $ expr ->expr ,
188189 );
189190 }
191+ $ valueContext = $ valueContext ->enterDeep ();
192+ // the write site is registered before its value is walked, so the
193+ // variable reads in the value are recorded as what the write is
194+ // computed from rather than as sinks
195+ $ valueFlowWrite = $ expr instanceof AssignRef ? null : $ this ->registerWriteSite ($ nodeScopeResolver , $ target , VariableWrite::KIND_ASSIGN );
196+ if ($ valueFlowWrite !== null ) {
197+ $ valueContext = $ valueContext ->enterValueFlow ($ valueFlowWrite , true );
198+ }
190199
191- $ assignedExprResult = $ nodeScopeResolver ->processExprNode ($ stmt , $ expr ->expr , $ valueScope , $ storage , $ nodeCallback , $ valueContext ->enterDeep ());
200+ $ assignedExprResult = $ nodeScopeResolver ->processExprNode ($ stmt , $ expr ->expr , $ valueScope , $ storage , $ nodeCallback , $ valueContext );
201+ if ($ valueFlowWrite !== null && $ context ->getValueFlowTarget () !== null ) {
202+ // `$a = $b = ...`: the value flows into both targets
203+ $ nodeScopeResolver ->copyVariableWriteDependencies ($ context ->getValueFlowTarget (), $ valueFlowWrite );
204+ }
192205 $ valueImpurePoints = array_merge ($ valueImpurePoints , $ assignedExprResult ->getImpurePoints ());
193206 $ valueScope = $ assignedExprResult ->getScope ();
194207
@@ -547,14 +560,15 @@ public function prepareTarget(
547560 callable $ nodeCallback ,
548561 ExpressionContext $ context ,
549562 AssignTargetWalkMode $ mode ,
563+ ?VariableWrite $ valueFlowTarget = null ,
550564 ): PreparedAssignTarget
551565 {
552566 // The raw target's node callback fires after the walk below composed and
553567 // stored the target's read result, with the scope captured at entry -
554568 // a synchronously invoked rule (the plain resolver, PHP < 8.1) then
555569 // answers its asks from the storage instead of re-walking on demand,
556570 // same as NodeScopeResolver::processExprNodeInternal().
557- $ prepared = $ this ->doPrepareTarget ($ nodeScopeResolver , $ scope , $ storage , $ stmt , $ var , $ assignedExpr , $ nodeCallback , $ context , $ mode );
571+ $ prepared = $ this ->doPrepareTarget ($ nodeScopeResolver , $ scope , $ storage , $ stmt , $ var , $ assignedExpr , $ nodeCallback , $ context , $ mode, $ valueFlowTarget );
558572 $ nodeScopeResolver ->callNodeCallback ($ nodeCallback , $ var , $ mode ->enterExpressionAssign () ? $ scope ->enterExpressionAssign ($ var ) : $ scope , $ storage );
559573
560574 return $ prepared ;
@@ -573,6 +587,7 @@ private function doPrepareTarget(
573587 callable $ nodeCallback ,
574588 ExpressionContext $ context ,
575589 AssignTargetWalkMode $ mode ,
590+ ?VariableWrite $ valueFlowTarget = null ,
576591 ): PreparedAssignTarget
577592 {
578593 $ enterExpressionAssign = $ mode ->enterExpressionAssign ();
@@ -595,7 +610,7 @@ private function doPrepareTarget(
595610 if (!is_string ($ var ->name )) {
596611 // `$$name OP= ...` evaluates the name before reading the old
597612 // value: walk it once here, the write flow consumes the result
598- $ variableNameResult = $ nodeScopeResolver ->processExprNode ($ stmt , $ var ->name , $ scope , $ storage , $ nodeCallback , $ context );
613+ $ variableNameResult = $ nodeScopeResolver ->processExprNode ($ stmt , $ var ->name , $ scope , $ storage , $ nodeCallback , $ context-> withoutValueFlow () );
599614 $ hasYield = $ variableNameResult ->hasYield ();
600615 $ throwPoints = $ variableNameResult ->getThrowPoints ();
601616 $ impurePoints = $ variableNameResult ->getImpurePoints ();
@@ -607,7 +622,7 @@ private function doPrepareTarget(
607622 $ nonNullabilityResult = $ this ->nonNullabilityHelper ->ensureNonNullability ($ scope , $ var );
608623 $ readScope = $ nodeScopeResolver ->lookForSetAllowedUndefinedExpressions ($ nonNullabilityResult ->getScope (), $ var );
609624 }
610- $ targetReadResult = $ this ->variableHandler ->composeResult ($ nodeScopeResolver , $ var , $ variableNameResult , $ storage , $ readScope );
625+ $ targetReadResult = $ this ->variableHandler ->composeResult ($ nodeScopeResolver , $ var , $ variableNameResult , $ storage , $ readScope, $ valueFlowTarget );
611626 if ($ mode ->issetSemanticsForRead ()) {
612627 $ targetChainResults [spl_object_id ($ var )] = $ targetReadResult ;
613628 }
@@ -648,7 +663,7 @@ private function doPrepareTarget(
648663 if ($ enterExpressionAssign ) {
649664 $ scope = $ scope ->enterExpressionAssign ($ var , false );
650665 }
651- $ varResult = $ nodeScopeResolver ->processExprNode ($ stmt , $ var , $ scope , $ storage , $ nodeCallback , $ context ->enterDeep ());
666+ $ varResult = $ nodeScopeResolver ->processExprNode ($ stmt , $ var , $ scope , $ storage , $ nodeCallback , $ context ->enterDeep ()-> enterArrayDimFetchRoot () );
652667 $ hasYield = $ varResult ->hasYield ();
653668 $ throwPoints = $ varResult ->getThrowPoints ();
654669 $ impurePoints = $ varResult ->getImpurePoints ();
@@ -834,7 +849,7 @@ private function doPrepareTarget(
834849
835850 if ($ var instanceof PropertyFetch) {
836851 $ scopeBeforeVar = $ scope ;
837- $ objectResult = $ nodeScopeResolver ->processExprNode ($ stmt , $ var ->var , $ scope , $ storage , $ nodeCallback , $ context );
852+ $ objectResult = $ nodeScopeResolver ->processExprNode ($ stmt , $ var ->var , $ scope , $ storage , $ nodeCallback , $ context-> withoutValueFlow () );
838853 $ hasYield = $ objectResult ->hasYield ();
839854 $ throwPoints = $ objectResult ->getThrowPoints ();
840855 $ impurePoints = $ objectResult ->getImpurePoints ();
@@ -846,7 +861,7 @@ private function doPrepareTarget(
846861 if ($ var ->name instanceof Node \Identifier) {
847862 $ propertyName = $ var ->name ->name ;
848863 } else {
849- $ propertyNameResult = $ nodeScopeResolver ->processExprNode ($ stmt , $ var ->name , $ scope , $ storage , $ nodeCallback , $ context );
864+ $ propertyNameResult = $ nodeScopeResolver ->processExprNode ($ stmt , $ var ->name , $ scope , $ storage , $ nodeCallback , $ context-> withoutValueFlow () );
850865 $ hasYield = $ hasYield || $ propertyNameResult ->hasYield ();
851866 $ throwPoints = array_merge ($ throwPoints , $ propertyNameResult ->getThrowPoints ());
852867 $ impurePoints = array_merge ($ impurePoints , $ propertyNameResult ->getImpurePoints ());
@@ -903,7 +918,7 @@ private function doPrepareTarget(
903918 if ($ var ->class instanceof Node \Name) {
904919 $ propertyHolderType = $ scope ->resolveTypeByName ($ var ->class );
905920 } else {
906- $ classResult = $ nodeScopeResolver ->processExprNode ($ stmt , $ var ->class , $ scope , $ storage , $ nodeCallback , $ context );
921+ $ classResult = $ nodeScopeResolver ->processExprNode ($ stmt , $ var ->class , $ scope , $ storage , $ nodeCallback , $ context-> withoutValueFlow () );
907922 $ propertyHolderType = $ classResult ->getType ();
908923 }
909924
@@ -912,7 +927,7 @@ private function doPrepareTarget(
912927 if ($ var ->name instanceof Node \Identifier) {
913928 $ propertyName = $ var ->name ->name ;
914929 } else {
915- $ propertyNameResult = $ nodeScopeResolver ->processExprNode ($ stmt , $ var ->name , $ scope , $ storage , $ nodeCallback , $ context );
930+ $ propertyNameResult = $ nodeScopeResolver ->processExprNode ($ stmt , $ var ->name , $ scope , $ storage , $ nodeCallback , $ context-> withoutValueFlow () );
916931 $ hasYield = $ propertyNameResult ->hasYield ();
917932 $ throwPoints = $ propertyNameResult ->getThrowPoints ();
918933 $ impurePoints = $ propertyNameResult ->getImpurePoints ();
@@ -1027,7 +1042,7 @@ private function doPrepareTarget(
10271042 );
10281043 }
10291044
1030- $ varResult = $ nodeScopeResolver ->processExprNode ($ stmt , $ var , $ scope , $ storage , $ nodeCallback , $ context );
1045+ $ varResult = $ nodeScopeResolver ->processExprNode ($ stmt , $ var , $ scope , $ storage , $ nodeCallback , $ context-> withoutValueFlow () );
10311046 $ hasYield = $ varResult ->hasYield ();
10321047 $ throwPoints = array_merge ($ throwPoints , $ varResult ->getThrowPoints ());
10331048 $ impurePoints = array_merge ($ impurePoints , $ varResult ->getImpurePoints ());
@@ -1245,7 +1260,7 @@ public function applyWrite(
12451260 $ this ->readAssignedValueType ($ nodeScopeResolver , $ storedAssignedExprResult , $ assignedExpr , $ scope ->doNotTreatPhpDocTypesAsCertain ()),
12461261 TrinaryLogic::createYes (),
12471262 [],
1248- $ write ,
1263+ $ write !== null ? $ nodeScopeResolver -> getVariableWriteMarkersToPlant ( $ write ) : [] ,
12491264 $ write !== null ? $ nodeScopeResolver ->getVariableWriteMarkersToKill ($ var ->name ) : [],
12501265 );
12511266 foreach ($ conditionalExpressions as $ exprString => $ holders ) {
@@ -1259,7 +1274,7 @@ public function applyWrite(
12591274 // a plain assignment does not read the target, so the dynamic name
12601275 // is walked here; read-modify-write targets walked it in
12611276 // prepareTarget() and already carry its state
1262- $ nameExprResult = $ nodeScopeResolver ->processExprNode ($ stmt , $ var ->name , $ scope , $ storage , $ nodeCallback , $ context );
1277+ $ nameExprResult = $ nodeScopeResolver ->processExprNode ($ stmt , $ var ->name , $ scope , $ storage , $ nodeCallback , $ context-> withoutValueFlow () );
12631278 $ hasYield = $ hasYield || $ nameExprResult ->hasYield ();
12641279 $ throwPoints = array_merge ($ throwPoints , $ nameExprResult ->getThrowPoints ());
12651280 $ impurePoints = array_merge ($ impurePoints , $ nameExprResult ->getImpurePoints ());
@@ -1351,16 +1366,20 @@ public function applyWrite(
13511366 // an offset write on an object (ArrayAccess, mixed) mutates a shared
13521367 // handle - only a value container (array, string) makes it a write site
13531368 $ write = $ target ->getWriteSiteKind () !== null && ($ varType ->isArray ()->yes () || $ varType ->isString ()->yes ())
1354- ? $ nodeScopeResolver -> recordVariableWrite ( $ var , VariableWrite:: KIND_ARRAY_DIM_WRITE )
1369+ ? $ this -> registerArrayDimWriteSite ( $ nodeScopeResolver , $ target )
13551370 : null ;
1371+ if ($ write !== null && $ target ->getWriteSiteKind () === VariableWrite::KIND_READ_MODIFY_WRITE ) {
1372+ // `$a['k'] OP= ...` and `$a['k'] ??= ...` read the offset's old value
1373+ $ nodeScopeResolver ->markVariableOffsetRead ($ var ->name , $ write ->getOffset (), $ scopeBeforeAssignEval );
1374+ }
13561375 $ scope = $ scope ->assignVariable (
13571376 $ var ->name ,
13581377 $ valueToWrite ,
13591378 $ nativeValueToWrite ,
13601379 TrinaryLogic::createYes (),
13611380 [],
1362- $ write ,
1363- $ write !== null ? $ nodeScopeResolver -> getVariableWriteMarkersToKill ( $ var -> name ) : [],
1381+ $ write !== null ? [ $ write -> getMarkerExpr ()] : [] ,
1382+ $ write !== null ? $ this -> getArrayDimWriteMarkersToKill ( $ nodeScopeResolver , $ target , $ write ) : [],
13641383 );
13651384 } else {
13661385 if ($ var instanceof PropertyFetch || $ var instanceof StaticPropertyFetch) {
@@ -2132,6 +2151,71 @@ private function isExprSafeToProjectThroughVariable(Expr $expr, string $variable
21322151 return count ($ rhsImpurePoints ) === 0 ;
21332152 }
21342153
2154+ /**
2155+ * Registers the write site of a prepared plain-variable or offset target
2156+ * before its value is walked, so the reads in the value are recorded as
2157+ * what the write is computed from; null when the target is not a site (a
2158+ * dynamic name, an offset of an object or of a value of unknown type).
2159+ *
2160+ * @param VariableWrite::KIND_* $kind
2161+ */
2162+ public function registerWriteSite (NodeScopeResolver $ nodeScopeResolver , PreparedAssignTarget $ target , int $ kind ): ?VariableWrite
2163+ {
2164+ $ var = $ target ->getVar ();
2165+ if ($ var instanceof Variable) {
2166+ return is_string ($ var ->name ) ? $ nodeScopeResolver ->recordVariableWrite ($ var , $ kind ) : null ;
2167+ }
2168+ if (!$ var instanceof ArrayDimFetch) {
2169+ return null ;
2170+ }
2171+ $ rootType = $ target ->getVarResult ()->getType ();
2172+ if (
2173+ !$ rootType ->isArray ()->yes ()
2174+ && !$ rootType ->isString ()->yes ()
2175+ && !$ this ->isImplicitArrayCreation ($ target ->getDimFetchStack (), $ target ->getScope ())->yes ()
2176+ ) {
2177+ return null ;
2178+ }
2179+
2180+ return $ this ->registerArrayDimWriteSite ($ nodeScopeResolver , $ target );
2181+ }
2182+
2183+ /**
2184+ * `$a['k'] = ...` writes offset 'k' of $a. `$a['k']['j'] = ...` extends the
2185+ * value of offset 'k' instead: that value is read and the earlier writes of
2186+ * the offset stay alive (the site kills nothing).
2187+ */
2188+ private function registerArrayDimWriteSite (NodeScopeResolver $ nodeScopeResolver , PreparedAssignTarget $ target ): ?VariableWrite
2189+ {
2190+ $ rootVar = $ target ->getRootVar ();
2191+ if (!$ rootVar instanceof Variable || !is_string ($ rootVar ->name )) {
2192+ return null ;
2193+ }
2194+ $ offsetTypes = $ target ->getOffsetTypes ();
2195+ $ offset = $ offsetTypes [0 ][0 ] !== null ? VariableWriteOffset::fromType ($ offsetTypes [0 ][0 ]) : null ;
2196+ if (count ($ target ->getDimFetchStack ()) > 1 ) {
2197+ $ nodeScopeResolver ->markVariableOffsetRead ($ rootVar ->name , $ offset , $ target ->getBeforeScope ());
2198+ }
2199+
2200+ return $ nodeScopeResolver ->recordVariableOffsetWrite ($ target ->getVar (), $ rootVar ->name , VariableWrite::KIND_ARRAY_DIM_WRITE , $ offset );
2201+ }
2202+
2203+ /**
2204+ * The markers an offset write kills: the earlier writes of the same
2205+ * constant offset - see MutatingScope::assignVariable().
2206+ *
2207+ * @return list<Expr>
2208+ */
2209+ private function getArrayDimWriteMarkersToKill (NodeScopeResolver $ nodeScopeResolver , PreparedAssignTarget $ target , VariableWrite $ write ): array
2210+ {
2211+ $ offset = $ write ->getOffset ();
2212+ if ($ offset === null || count ($ target ->getDimFetchStack ()) > 1 ) {
2213+ return [];
2214+ }
2215+
2216+ return $ nodeScopeResolver ->getVariableOffsetWriteMarkersToKill ($ write ->getVariableName (), $ offset );
2217+ }
2218+
21352219 /**
21362220 * @param list<ArrayDimFetch> $dimFetchStack
21372221 */
0 commit comments