Repository: vercel/next.js
Pull request: Turbopack: Fix compound assignment expression evaluation (#85478)

Migrated from https://github.com/vercel/next.js/pull/85478 into this non-fork because we had some issues with our CI infra that should be fixed going forward: https://vercel.slack.com/archives/C04KC8A53T7/p1761862171772939 

---

fixes #85474 

Previously, the analyzer treated all assignment expressions the same way, evaluating them as their right-hand side value. This was incorrect for compound assignments (+=, -=, *=, |=, >>=, etc.), which compute a new value based on both the left and right operands.

This commit distinguishes between simple assignments (=) and compound assignments. Simple assignments continue to evaluate to the right-hand side, while compound assignments are now properly marked as unknown values with side effects.

Also adds tests for compound assignments in conditionals, covering bitshift operations (>>=, <<=, >>>=) and bitwise operations (|=, &=, ^=) with both numbers and bigints.

Changed files:
filename: turbopack/crates/turbopack-ecmascript/src/analyzer/graph.rs
patch: @@ -802,7 +802,10 @@ impl EvalContext {
                 ..
             }) => JsValue::WellKnownObject(WellKnownObjectKind::ImportMeta),
 
-            Expr::Assign(AssignExpr { right, .. }) => self.eval(right),
+            Expr::Assign(AssignExpr { op, right, .. }) => match op {
+                AssignOp::Assign => self.eval(right),
+                _ => JsValue::unknown_empty(true, "compound assignment expression"),
+            },
 
             _ => JsValue::unknown_empty(true, "unsupported expression"),
         }

filename: turbopack/crates/turbopack-ecmascript/tests/analyzer/graph/react-dom-production/graph-explained.snapshot
patch: @@ -3330,8 +3330,10 @@ c#169 = (???*0* | ???*1*)
 - *1* unsupported assign operation
   ⚠️  This value might have side effects
 
-c#171 = (b | ???*0*)
-- *0* unsupported assign operation
+c#171 = (???*0* | ???*1*)
+- *0* compound assignment expression
+  ⚠️  This value might have side effects
+- *1* unsupported assign operation
   ⚠️  This value might have side effects
 
 c#175 = arguments[2]
@@ -4829,8 +4831,14 @@ f#423 = (b["contextType"] | (Zf(b) ? Xf : H["current"]) | b["getDerivedStateFrom
 
 f#424 = (`${a}` | undefined)
 
-f#431 = (...) => (c | (???*0* ? c : d))
-- *0* unsupported expression
+f#431 = (...) => (???*0* | (???*1* ? ???*2* : d) | c)
+- *0* c
+  ⚠️  sequence with side effects
+  ⚠️  This value might have side effects
+- *1* unsupported expression
+  ⚠️  This value might have side effects
+- *2* c
+  ⚠️  sequence with side effects
   ⚠️  This value might have side effects
 
 f#440 = c["type"]
@@ -6198,7 +6206,7 @@ sh (const after eval) = (...) => (b["ref"] | b | a)
 
 si (const after eval) = (...) => di()["memoizedState"]
 
-sj (const after eval) = (...) => (???*0* | null | f | tj(a, b, g, null) | tj(a, b, g, d) | b)
+sj (const after eval) = (...) => (???*0* | f | tj(a, b, g, null) | tj(a, b, g, d) | b)
 - *0* tj(a, b, g, d)
   ⚠️  sequence with side effects
   ⚠️  This value might have side effects

filename: turbopack/crates/turbopack-ecmascript/tests/analyzer/graph/react-dom-production/resolved-effects.snapshot
patch: @@ -8242,8 +8242,8 @@ ${La}${a}`("SuspenseList")
   ⚠️  nested operation
 - *5* unsupported expression
   ⚠️  This value might have side effects
-- *6* arguments[1]
-  ⚠️  function calls are not analyzed yet
+- *6* compound assignment expression
+  ⚠️  This value might have side effects
 - *7* unsupported assign operation
   ⚠️  This value might have side effects
 
@@ -24836,14 +24836,20 @@ ${La}${a}`("SuspenseList")
 - *1* max number of linking steps reached
   ⚠️  This value might have side effects
 
-0 -> 3401 call = (...) => (c | (???*0* ? c : d))(???*1*, ???*2*, (0 | ???*3*))
-- *0* unsupported expression
+0 -> 3401 call = (...) => (???*0* | (???*1* ? ???*2* : d) | c)(???*3*, ???*4*, (0 | ???*5*))
+- *0* c
+  ⚠️  sequence with side effects
   ⚠️  This value might have side effects
-- *1* max number of linking steps reached
+- *1* unsupported expression
   ⚠️  This value might have side effects
-- *2* max number of linking steps reached
+- *2* c
+  ⚠️  sequence with side effects
   ⚠️  This value might have side effects
-- *3* updated with update expression
+- *3* max number of linking steps reached
+  ⚠️  This value might have side effects
+- *4* max number of linking steps reached
+  ⚠️  This value might have side effects
+- *5* updated with update expression
   ⚠️  This value might have side effects
 
 0 -> 3402 conditional = (null === ???*0*)
@@ -24891,14 +24897,20 @@ ${La}${a}`("SuspenseList")
 - *4* arguments[3]
   ⚠️  function calls are not analyzed yet
 
-3409 -> 3413 call = (...) => (c | (???*0* ? c : d))(???*1*, ???*2*, (0 | ???*3*))
-- *0* unsupported expression
+3409 -> 3413 call = (...) => (???*0* | (???*1* ? ???*2* : d) | c)(???*3*, ???*4*, (0 | ???*5*))
+- *0* c
+  ⚠️  sequence with side effects
   ⚠️  This value might have side effects
-- *1* max number of linking steps reached
+- *1* unsupported expression
   ⚠️  This value might have side effects
-- *2* max number of linking steps reached
+- *2* c
+  ⚠️  sequence with side effects
   ⚠️  This value might have side effects
-- *3* updated with update expression
+- *3* max number of linking steps reached
+  ⚠️  This value might have side effects
+- *4* max number of linking steps reached
+  ⚠️  This value might have side effects
+- *5* updated with update expression
   ⚠️  This value might have side effects
 
 3409 -> 3414 conditional = (null === ???*0*)
@@ -24963,14 +24975,20 @@ ${La}${a}`("SuspenseList")
 - *6* max number of linking steps reached
   ⚠️  This value might have side effects
 
-3409 -> 3428 call = (...) => (c | (???*0* ? c : d))(???*1*, ???*2*, (0 | ???*3*))
-- *0* unsupported expression
+3409 -> 3428 call = (...) => (???*0* | (???*1* ? ???*2* : d) | c)(???*3*, ???*4*, (0 | ???*5*))
+- *0* c
+  ⚠️  sequence with side effects
   ⚠️  This value might have side effects
-- *1* max number of linking steps reached
+- *1* unsupported expression
   ⚠️  This value might have side effects
-- *2* max number of linking steps reached
+- *2* c
+  ⚠️  sequence with side effects
   ⚠️  This value might have side effects
-- *3* updated with update expression
+- *3* max number of linking steps reached
+  ⚠️  This value might have side effects
+- *4* max number of linking steps reached
+  ⚠️  This value might have side effects
+- *5* updated with update expression
   ⚠️  This value might have side effects
 
 3409 -> 3429 conditional = (null === ???*0*)
@@ -25085,14 +25103,20 @@ ${La}${a}`("SuspenseList")
 - *1* max number of linking steps reached
   ⚠️  This value might have side effects
 
-0 -> 3460 call = (...) => (c | (???*0* ? c : d))(???*1*, ???*2*, (0 | ???*3*))
-- *0* unsupported expression
+0 -> 3460 call = (...) => (???*0* | (???*1* ? ???*2* : d) | c)(???*3*, ???*4*, (0 | ???*5*))
+- *0* c
+  ⚠️  sequence with side effects
   ⚠️  This value might have side effects
-- *1* max number of linking steps reached
+- *1* unsupported expression
   ⚠️  This value might have side effects
-- *2* max number of linking steps reached
+- *2* c
+  ⚠️  sequence with side effects
   ⚠️  This value might have side effects
-- *3* updated with update expression
+- *3* max number of linking steps reached
+  ⚠️  This value might have side effects
+- *4* max number of linking steps reached
+  ⚠️  This value might have side effects
+- *5* updated with update expression
   ⚠️  This value might have side effects
 
 0 -> 3461 conditional = (null === ???*0*)
@@ -25144,14 +25168,20 @@ ${La}${a}`("SuspenseList")
 - *4* arguments[3]
   ⚠️  function calls are not analyzed yet
 
-3468 -> 3474 call = (...) => (c | (???*0* ? c : d))(???*1*, ???*2*, (0 | ???*3*))
-- *0* unsupported expression
+3468 -> 3474 call = (...) => (???*0* | (???*1* ? ???*2* : d) | c)(???*3*, ???*4*, (0 | ???*5*))
+- *0* c
+  ⚠️  sequence with side effects
   ⚠️  This value might have side effects
-- *1* max number of linking steps reached
+- *1* unsupported expression
   ⚠️  This value might have side effects
-- *2* max number of linking steps reached
+- *2* c
+  ⚠️  sequence with side effects
   ⚠️  This value might have side effects
-- *3* updated with update expression
+- *3* max number of linking steps reached
+  ⚠️  This value might have side effects
+- *4* max number of linking steps reached
+  ⚠️  This value might have side effects
+- *5* updated with update expression
   ⚠️  This value might have side effects
 
 3468 -> 3475 conditional = (null === ???*0*)
@@ -25221,14 +25251,20 @@ ${La}${a}`("SuspenseList")
 - *6* max number of linking steps reached
   ⚠️  This value might have side effects
 
-3468 -> 3491 call = (...) => (c | (???*0* ? c : d))(???*1*, ???*2*, (0 | ???*3*))
-- *0* unsupported expression
+3468 -> 3491 call = (...) => (???*0* | (???*1* ? ???*2* : d) | c)(???*3*, ???*4*, (0 | ???*5*))
+- *0* c
+  ⚠️  sequence with side effects
   ⚠️  This value might have side effects
-- *1* max number of linking steps reached
+- *1* unsupported expression
   ⚠️  This value might have side effects
-- *2* max number of linking steps reached
+- *2* c
+  ⚠️  sequence with side effects
   ⚠️  This value might have side effects
-- *3* updated with update expression
+- *3* max number of linking steps reached
+  ⚠️  This value might have side effects
+- *4* max number of linking steps reached
+  ⚠️  This value might have side effects
+- *5* updated with update expression
   ⚠️  This value might have side effects
 
 3468 -> 3492 conditional = (null === ???*0*)
@@ -35905,7 +35941,7 @@ ${La}${a}`("SuspenseList")
 - *0* max number of linking steps reached
   ⚠️  This value might have side effects
 
-4626 -> 4627 call = (...) => (???*0* | null | f | tj(a, b, g, null) | tj(a, b, g, d) | b)(???*1*, ???*2*, ???*3*, ???*4*, ???*5*, ???*6*, (???*7* | ???*8*))
+4626 -> 4627 call = (...) => (???*0* | f | tj(a, b, g, null) | tj(a, b, g, d) | b)(???*1*, ???*2*, ???*3*, ???*4*, ???*5*, ???*6*, (???*7* | ???*8*))
 - *0* tj(a, b, g, d)
   ⚠️  sequence with side effects
   ⚠️  This value might have side effects

filename: turbopack/crates/turbopack-ecmascript/tests/analyzer/graph/react-dom-production/resolved-explained.snapshot
patch: @@ -9730,8 +9730,8 @@ c#169 = (???*0* | ???*1*)
   ⚠️  This value might have side effects
 
 c#171 = (???*0* | ???*1*)
-- *0* arguments[1]
-  ⚠️  function calls are not analyzed yet
+- *0* compound assignment expression
+  ⚠️  This value might have side effects
 - *1* unsupported assign operation
   ⚠️  This value might have side effects
 
@@ -17480,8 +17480,14 @@ f#424 = (???*0* | ???*1* | undefined)
 - *2* arguments[2]
   ⚠️  function calls are not analyzed yet
 
-f#431 = (...) => (c | (???*0* ? c : d))
-- *0* unsupported expression
+f#431 = (...) => (???*0* | (???*1* ? ???*2* : d) | c)
+- *0* c
+  ⚠️  sequence with side effects
+  ⚠️  This value might have side effects
+- *1* unsupported expression
+  ⚠️  This value might have side effects
+- *2* c
+  ⚠️  sequence with side effects
   ⚠️  This value might have side effects
 
 f#440 = ???*0*
@@ -21224,7 +21230,7 @@ sh = (...) => (b["ref"] | b | a)
 
 si = (...) => di()["memoizedState"]
 
-sj = (...) => (???*0* | null | f | tj(a, b, g, null) | tj(a, b, g, d) | b)
+sj = (...) => (???*0* | f | tj(a, b, g, null) | tj(a, b, g, d) | b)
 - *0* tj(a, b, g, d)
   ⚠️  sequence with side effects
   ⚠️  This value might have side effects

filename: turbopack/crates/turbopack-tests/tests/execution/turbopack/basic/compound-assign-in-conditional/input/index.js
patch: @@ -0,0 +1,69 @@
+it('should handle right shift assignment in conditional for number', () => {
+  let value = 16
+  let entered = false
+  if ((value >>= 2)) {
+    entered = true
+  }
+  expect(value).toBe(4)
+  expect(entered).toBe(true)
+})
+
+it('should handle right shift assignment in conditional for bigint', () => {
+  let value = 16n
+  let entered = false
+  if ((value >>= 2n)) {
+    entered = true
+  }
+  expect(value).toBe(4n)
+  expect(entered).toBe(true)
+})
+
+it('should handle left shift assignment in conditional for number', () => {
+  let value = 3
+  let entered = false
+  if ((value <<= 2)) {
+    entered = true
+  }
+  expect(value).toBe(12)
+  expect(entered).toBe(true)
+})
+
+it('should handle unsigned right shift assignment in conditional for number', () => {
+  let value = 16
+  let entered = false
+  if ((value >>>= 2)) {
+    entered = true
+  }
+  expect(value).toBe(4)
+  expect(entered).toBe(true)
+})
+
+it('should handle bitwise OR assignment in conditional', () => {
+  let value = 0
+  let entered = false
+  if ((value |= 5)) {
+    entered = true
+  }
+  expect(value).toBe(5)
+  expect(entered).toBe(true)
+})
+
+it('should handle bitwise AND assignment in conditional', () => {
+  let value = 7
+  let entered = false
+  if ((value &= 3)) {
+    entered = true
+  }
+  expect(value).toBe(3)
+  expect(entered).toBe(true)
+})
+
+it('should handle bitwise XOR assignment in conditional', () => {
+  let value = 5
+  let entered = false
+  if ((value ^= 3)) {
+    entered = true
+  }
+  expect(value).toBe(6)
+  expect(entered).toBe(true)
+})