Repository: joernio/joern (scala)

Issue:
jssrc: invalid desugaring for spread syntax in object literals
example of actual behavior from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#spread_in_object_literals

```javascript
const array = [1, 2, 3];
const obj = { ...array }; // { 0: 1, 1: 2, 2: 3 }
```

i.e. it does a shallow copy of `array` into the resulting object. The only real way to model that in the CPG is as a call to an operator (or possibly `Object.assign` which has the same semantics, but I'd prefer something like `<operator>.shallowCopy` or `<operator>.spreadObject`).

------


Currently, we try to do weird things based on the name of the variable that's being spread:

```
joern> importCode.jssrc.fromString("""({...bar})""") 
res80: Cpg = Cpg (Graph [55 nodes])

joern> cpg.call.code.p 
res81: List[String] = List("_tmp_0.bar = bar", "_tmp_0.bar")
```

and even more wrong:

```
joern> importCode.jssrc.fromString("""({...foo.bar})""") 
res78: Cpg = Cpg (Graph [58 nodes])

joern> cpg.call.code.p 
res79: List[String] = List("_tmp_0._tmp_1 = foo.bar", "_tmp_0._tmp_1", "foo.bar")
```

Pull request:
Fixes: https://github.com/joernio/joern/issues/2086

Patch:
diff --git a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstCreator.scala b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstCreator.scala
index 036eb3cb4a58..7aed52d6b954 100644
--- a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstCreator.scala
+++ b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstCreator.scala
@@ -201,7 +201,7 @@ class AstCreator(
       case BigIntLiteral             => astForBigIntLiteral(nodeInfo)
       case TemplateLiteral           => astForTemplateLiteral(nodeInfo)
       case TemplateElement           => astForTemplateElement(nodeInfo)
-      case SpreadElement             => astForSpreadElement(nodeInfo)
+      case SpreadElement             => astForSpreadOrRestElement(nodeInfo)
       case JSXElement                => astForJsxElement(nodeInfo)
       case JSXOpeningElement         => astForJsxOpeningElement(nodeInfo)
       case JSXClosingElement         => astForJsxClosingElement(nodeInfo)
diff --git a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstForDeclarationsCreator.scala b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstForDeclarationsCreator.scala
index ac9728ef5624..5eae93c109b6 100644
--- a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstForDeclarationsCreator.scala
+++ b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstForDeclarationsCreator.scala
@@ -520,7 +520,7 @@ trait AstForDeclarationsCreator { this: AstCreator =>
     )
   }
 
-  protected def convertDestructingObjectElementWithDefault(
+  private def convertDestructingObjectElementWithDefault(
     element: BabelNodeInfo,
     key: BabelNodeInfo,
     localTmpName: String
@@ -617,8 +617,8 @@ trait AstForDeclarationsCreator { this: AstCreator =>
           val nodeInfo = createBabelNodeInfo(element)
           nodeInfo.node match {
             case RestElement =>
-              val restElementNodeInfo = createBabelNodeInfo(nodeInfo.json("argument"))
-              convertDestructingObjectElement(restElementNodeInfo, restElementNodeInfo, localTmpName)
+              val arg1Ast = Ast(createIdentifierNode(localTmpName, nodeInfo))
+              astForSpreadOrRestElement(nodeInfo, Some(arg1Ast))
             case _ =>
               val nodeInfo = createBabelNodeInfo(element("value"))
               nodeInfo.node match {
@@ -640,8 +640,12 @@ trait AstForDeclarationsCreator { this: AstCreator =>
             val nodeInfo = createBabelNodeInfo(element)
             nodeInfo.node match {
               case RestElement =>
-                val restElementNodeInfo = createBabelNodeInfo(nodeInfo.json("argument"))
-                convertDestructingArrayElement(restElementNodeInfo, index, localTmpName)
+                val fieldAccessTmpNode = createIdentifierNode(localTmpName, nodeInfo)
+                val keyNode =
+                  createLiteralNode(index.toString, Some(Defines.NUMBER), nodeInfo.lineNumber, nodeInfo.columnNumber)
+                val accessAst =
+                  createIndexAccessCallAst(fieldAccessTmpNode, keyNode, nodeInfo.lineNumber, nodeInfo.columnNumber)
+                astForSpreadOrRestElement(nodeInfo, Some(accessAst))
               case Identifier =>
                 convertDestructingArrayElement(nodeInfo, index, localTmpName)
               case AssignmentPattern =>
diff --git a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstForExpressionsCreator.scala b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstForExpressionsCreator.scala
index 1b0ca842bb19..a7cae13d451b 100644
--- a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstForExpressionsCreator.scala
+++ b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstForExpressionsCreator.scala
@@ -1,6 +1,5 @@
 package io.joern.jssrc2cpg.astcreation
 
-import io.joern.jssrc2cpg.datastructures.BlockScope
 import io.joern.jssrc2cpg.parser.BabelAst._
 import io.joern.jssrc2cpg.parser.BabelNodeInfo
 import io.joern.jssrc2cpg.passes.{Defines, EcmaBuiltins, GlobalBuiltins}
@@ -437,46 +436,46 @@ trait AstForExpressionsCreator { this: AstCreator =>
 
     val propertiesAsts = objExpr.json("properties").arr.toList.map { property =>
       val nodeInfo = createBabelNodeInfo(property)
-      val (lhsNode, rhsAst) = nodeInfo.node match {
-        case ObjectMethod =>
-          val keyName = nodeInfo.json("key")("name").str
-          val keyNode = createFieldIdentifierNode(keyName, nodeInfo.lineNumber, nodeInfo.columnNumber)
-          (keyNode, astForFunctionDeclaration(nodeInfo, shouldCreateFunctionReference = true))
-        case ObjectProperty =>
-          val key = createBabelNodeInfo(nodeInfo.json("key"))
-          val keyName = key.node match {
-            case Identifier if nodeInfo.json("computed").bool =>
-              key.code
-            case _ if nodeInfo.json("computed").bool =>
-              generateUnusedVariableName(usedVariableNames, "_computed_object_property")
-            case _ => key.code
-          }
-          val keyNode = createFieldIdentifierNode(keyName, nodeInfo.lineNumber, nodeInfo.columnNumber)
-          val ast     = astForNodeWithFunctionReference(nodeInfo.json("value"))
-          (keyNode, ast)
+      nodeInfo.node match {
         case SpreadElement =>
-          val ast         = astForNodeWithFunctionReferenceAndCall(nodeInfo.json("argument"))
-          val defaultName = codeForNodes(ast.root.toSeq)
-          val keyName     = nameForBabelNodeInfo(nodeInfo, defaultName)
-          val keyNode     = createFieldIdentifierNode(keyName, nodeInfo.lineNumber, nodeInfo.columnNumber)
-          (keyNode, ast)
+          val arg1Ast = Ast(createIdentifierNode(tmpName, nodeInfo))
+          astForSpreadOrRestElement(nodeInfo, Some(arg1Ast))
         case _ =>
-          // can't happen as per https://github.com/babel/babel/blob/main/packages/babel-types/src/ast-types/generated/index.ts#L573
-          // just to make the compiler happy here.
-          ???
-      }
+          val (lhsNode, rhsAst) = nodeInfo.node match {
+            case ObjectMethod =>
+              val keyName = nodeInfo.json("key")("name").str
+              val keyNode = createFieldIdentifierNode(keyName, nodeInfo.lineNumber, nodeInfo.columnNumber)
+              (keyNode, astForFunctionDeclaration(nodeInfo, shouldCreateFunctionReference = true))
+            case ObjectProperty =>
+              val key = createBabelNodeInfo(nodeInfo.json("key"))
+              val keyName = key.node match {
+                case Identifier if nodeInfo.json("computed").bool =>
+                  key.code
+                case _ if nodeInfo.json("computed").bool =>
+                  generateUnusedVariableName(usedVariableNames, "_computed_object_property")
+                case _ => key.code
+              }
+              val keyNode = createFieldIdentifierNode(keyName, nodeInfo.lineNumber, nodeInfo.columnNumber)
+              val ast     = astForNodeWithFunctionReference(nodeInfo.json("value"))
+              (keyNode, ast)
+            case _ =>
+              // can't happen as per https://github.com/babel/babel/blob/main/packages/babel-types/src/ast-types/generated/index.ts#L573
+              // just to make the compiler happy here.
+              ???
+          }
 
-      val leftHandSideTmpNode = createIdentifierNode(tmpName, nodeInfo)
-      val leftHandSideFieldAccessAst =
-        createFieldAccessCallAst(leftHandSideTmpNode, lhsNode, nodeInfo.lineNumber, nodeInfo.columnNumber)
+          val leftHandSideTmpNode = createIdentifierNode(tmpName, nodeInfo)
+          val leftHandSideFieldAccessAst =
+            createFieldAccessCallAst(leftHandSideTmpNode, lhsNode, nodeInfo.lineNumber, nodeInfo.columnNumber)
 
-      createAssignmentCallAst(
-        leftHandSideFieldAccessAst,
-        rhsAst,
-        s"$tmpName.${lhsNode.canonicalName} = ${codeOf(rhsAst.nodes.head)}",
-        nodeInfo.lineNumber,
-        nodeInfo.columnNumber
-      )
+          createAssignmentCallAst(
+            leftHandSideFieldAccessAst,
+            rhsAst,
+            s"$tmpName.${lhsNode.canonicalName} = ${codeOf(rhsAst.nodes.head)}",
+            nodeInfo.lineNumber,
+            nodeInfo.columnNumber
+          )
+      }
     }
 
     val tmpNode = createIdentifierNode(tmpName, objExpr)
diff --git a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstForPrimitivesCreator.scala b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstForPrimitivesCreator.scala
index 38c2b94e9d17..ef099cdf5c81 100644
--- a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstForPrimitivesCreator.scala
+++ b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstForPrimitivesCreator.scala
@@ -4,7 +4,6 @@ import io.joern.jssrc2cpg.parser.BabelNodeInfo
 import io.joern.jssrc2cpg.passes.Defines
 import io.joern.x2cpg.Ast
 import io.shiftleft.codepropertygraph.generated.DispatchTypes
-import io.shiftleft.codepropertygraph.generated.nodes.NewIdentifier
 
 trait AstForPrimitivesCreator { this: AstCreator =>
 
@@ -35,10 +34,16 @@ trait AstForPrimitivesCreator { this: AstCreator =>
     Ast(createLiteralNode(code, Some(Defines.STRING), stringLiteral.lineNumber, stringLiteral.columnNumber))
   }
 
-  protected def astForSpreadElement(spreadElement: BabelNodeInfo): Ast = {
-    val ast = astForNode(spreadElement.json("argument"))
-    ast.nodes.collectFirst { case i: NewIdentifier => i }.foreach(_.code = spreadElement.code)
-    ast
+  protected def astForSpreadOrRestElement(spreadElement: BabelNodeInfo, arg1Ast: Option[Ast] = None): Ast = {
+    val ast = astForNodeWithFunctionReference(spreadElement.json("argument"))
+    val callNode = createCallNode(
+      spreadElement.code,
+      "<operator>.spread",
+      DispatchTypes.STATIC_DISPATCH,
+      spreadElement.lineNumber,
+      spreadElement.columnNumber
+    )
+    callAst(callNode, arg1Ast.toList :+ ast)
   }
 
   protected def astForTemplateElement(templateElement: BabelNodeInfo): Ast =


Test patch:
diff --git a/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/ast/MixedAstCreationPassTest.scala b/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/ast/MixedAstCreationPassTest.scala
index 7acc8a3bda31..ffe9226c4b08 100644
--- a/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/ast/MixedAstCreationPassTest.scala
+++ b/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/ast/MixedAstCreationPassTest.scala
@@ -4,6 +4,8 @@ import io.joern.jssrc2cpg.passes.AbstractPassTest
 import io.shiftleft.codepropertygraph.generated.DispatchTypes
 import io.shiftleft.codepropertygraph.generated.EvaluationStrategies
 import io.shiftleft.codepropertygraph.generated.Operators
+import io.shiftleft.codepropertygraph.generated.nodes.Call
+import io.shiftleft.codepropertygraph.generated.nodes.Identifier
 import io.shiftleft.semanticcpg.language._
 
 class MixedAstCreationPassTest extends AbstractPassTest {
@@ -841,13 +843,13 @@ class MixedAstCreationPassTest extends AbstractPassTest {
       fieldAccessA.astChildren.isIdentifier.nameExact("_tmp_0").size shouldBe 1
       fieldAccessA.astChildren.isFieldIdentifier.canonicalNameExact("a").size shouldBe 1
 
-      val List(assignmentToRest) = destructionBlock.astChildren.isCall.codeExact("rest = _tmp_0.rest").l
-      assignmentToRest.astChildren.isIdentifier.size shouldBe 1
-
-      val List(fieldAccessRest) = assignmentToRest.astChildren.isCall.codeExact("_tmp_0.rest").l
-      fieldAccessRest.name shouldBe Operators.fieldAccess
-      fieldAccessRest.astChildren.isIdentifier.nameExact("_tmp_0").size shouldBe 1
-      fieldAccessRest.astChildren.isFieldIdentifier.canonicalNameExact("rest").size shouldBe 1
+      val List(restCall) = destructionBlock.astChildren.isCall.nameExact("<operator>.spread").l
+      restCall.code shouldBe "...rest"
+      val List(tmpArg: Identifier) = restCall.argument(1).l
+      tmpArg.code shouldBe "_tmp_0"
+      tmpArg.name shouldBe "_tmp_0"
+      val List(restArg: Identifier) = restCall.argument(2).l
+      restArg.code shouldBe "rest"
 
       val List(tmpReturnIdentifier) = destructionBlock.astChildren.isIdentifier.l
       tmpReturnIdentifier.name shouldBe "_tmp_0"
@@ -1090,13 +1092,15 @@ class MixedAstCreationPassTest extends AbstractPassTest {
       indexAccessA.astChildren.isIdentifier.nameExact("_tmp_0").size shouldBe 1
       indexAccessA.astChildren.codeExact("0").size shouldBe 1
 
-      val List(assignmentToB) = destructionBlock.astChildren.isCall.codeExact("rest = _tmp_0[1]").l
-      assignmentToB.astChildren.isIdentifier.size shouldBe 1
-
-      val List(indexAccessB) = assignmentToB.astChildren.isCall.codeExact("_tmp_0[1]").l
-      indexAccessB.name shouldBe Operators.indexAccess
-      indexAccessB.astChildren.isIdentifier.nameExact("_tmp_0").size shouldBe 1
-      indexAccessB.astChildren.isLiteral.codeExact("1").size shouldBe 1
+      val List(restCall) = destructionBlock.astChildren.isCall.nameExact("<operator>.spread").l
+      restCall.code shouldBe "...rest"
+      val List(tmpArg: Call) = restCall.argument(1).l
+      tmpArg.code shouldBe "_tmp_0[1]"
+      tmpArg.name shouldBe Operators.indexAccess
+      tmpArg.astChildren.isIdentifier.nameExact("_tmp_0").size shouldBe 1
+      tmpArg.astChildren.isLiteral.codeExact("1").size shouldBe 1
+      val List(restArg: Identifier) = restCall.argument(2).l
+      restArg.code shouldBe "rest"
 
       val List(tmpReturnIdentifier) = destructionBlock.astChildren.isIdentifier.l
       tmpReturnIdentifier.name shouldBe "_tmp_0"
@@ -1127,8 +1131,10 @@ class MixedAstCreationPassTest extends AbstractPassTest {
       val List(argumentThis) = fooCall.astChildren.isIdentifier.nameExact("this").l
       argumentThis.argumentIndex shouldBe 0
 
-      val List(argument1) = fooCall.astChildren.isIdentifier.nameExact("args").l
+      val List(argument1) = fooCall.astChildren.isCall.nameExact("<operator>.spread").l
       argument1.argumentIndex shouldBe 1
+      argument1.code shouldBe "...args"
+      argument1.argument(1).code shouldBe "args"
     }
 
     "have correct structure for complex method spread argument" in AstFixture("foo(...x.bar())") { cpg =>
@@ -1143,8 +1149,11 @@ class MixedAstCreationPassTest extends AbstractPassTest {
       val List(argumentThis) = fooCall.astChildren.isIdentifier.nameExact("this").l
       argumentThis.argumentIndex shouldBe 0
 
-      val List(argument1) = fooCall.astChildren.isCall.codeExact("x.bar()").l
+      val List(argument1) = fooCall.astChildren.isCall.nameExact("<operator>.spread").l
       argument1.argumentIndex shouldBe 1
+      argument1.code shouldBe "...x.bar()"
+      val List(arg: Call) = argument1.argument(1).l
+      arg.code shouldBe "x.bar()"
     }
   }
 
diff --git a/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/ast/SimpleAstCreationPassTest.scala b/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/ast/SimpleAstCreationPassTest.scala
index 64b3ddd0543d..c0f15d4a81f9 100644
--- a/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/ast/SimpleAstCreationPassTest.scala
+++ b/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/ast/SimpleAstCreationPassTest.scala
@@ -272,7 +272,15 @@ class SimpleAstCreationPassTest extends AbstractPassTest {
       val List(block) = assignment.astChildren.isBlock.l
       checkObjectInitialization(block, ("key1", "\"value\""))
       checkObjectInitialization(block, ("key2", "2"))
-      checkObjectInitialization(block, ("rest", "rest"))
+
+      val List(spreadObjectCall) = block.astChildren.isCall.nameExact("<operator>.spread").l
+      spreadObjectCall.code shouldBe "...rest"
+      val List(tmpArg: Identifier) = spreadObjectCall.argument(1).l
+      tmpArg.code shouldBe "_tmp_0"
+      tmpArg.name shouldBe "_tmp_0"
+      val List(restArg: Identifier) = spreadObjectCall.argument(2).l
+      restArg.code shouldBe "rest"
+      restArg.name shouldBe "rest"
     }
 
     "have correct structure for 1 object with complex rest" in AstFixture("""
@@ -293,7 +301,14 @@ class SimpleAstCreationPassTest extends AbstractPassTest {
       val List(block) = assignment.astChildren.isBlock.l
       checkObjectInitialization(block, ("key1", "\"value\""))
       checkObjectInitialization(block, ("key2", "2"))
-      checkObjectInitialization(block, ("_tmp_1", "x.foo()"))
+
+      val List(spreadObjectCall) = block.astChildren.isCall.nameExact("<operator>.spread").l
+      spreadObjectCall.code shouldBe "...x.foo()"
+      val List(tmpArg: Identifier) = spreadObjectCall.argument(1).l
+      tmpArg.code shouldBe "_tmp_0"
+      tmpArg.name shouldBe "_tmp_0"
+      val List(restArg: Call) = spreadObjectCall.argument(2).l
+      restArg.code shouldBe "x.foo()"
     }
 
     "have correct structure for 1 object with computed values" in AstFixture("""
diff --git a/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/cfg/MixedCfgCreationPassTest.scala b/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/cfg/MixedCfgCreationPassTest.scala
index 8951ba8fc33e..6d86702bc07a 100644
--- a/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/cfg/MixedCfgCreationPassTest.scala
+++ b/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/cfg/MixedCfgCreationPassTest.scala
@@ -140,12 +140,10 @@ class MixedCfgCreationPassTest extends CfgTestFixture(() => new JsCfgTestCpg())
       succOf("a", 1) shouldBe expected(("_tmp_0.a", AlwaysEdge))
       succOf("_tmp_0.a") shouldBe expected(("a = _tmp_0.a", AlwaysEdge))
 
-      succOf("a = _tmp_0.a") shouldBe expected(("rest", AlwaysEdge))
-      succOf("rest") shouldBe expected(("_tmp_0", 2, AlwaysEdge))
-      succOf("_tmp_0", 2) shouldBe expected(("rest", 1, AlwaysEdge))
-      succOf("rest", 1) shouldBe expected(("_tmp_0.rest", AlwaysEdge))
-      succOf("_tmp_0.rest") shouldBe expected(("rest = _tmp_0.rest", AlwaysEdge))
-      succOf("rest = _tmp_0.rest") shouldBe expected(("_tmp_0", 3, AlwaysEdge))
+      succOf("a = _tmp_0.a") shouldBe expected(("_tmp_0", 2, AlwaysEdge))
+      succOf("_tmp_0", 2) shouldBe expected(("rest", AlwaysEdge))
+      succOf("rest") shouldBe expected(("...rest", AlwaysEdge))
+      succOf("...rest") shouldBe expected(("_tmp_0", 3, AlwaysEdge))
 
       succOf("_tmp_0", 3) shouldBe expected(("var {a, ...rest} = x", AlwaysEdge))
       succOf("var {a, ...rest} = x") shouldBe expected(("RET", AlwaysEdge))
@@ -360,22 +358,22 @@ class MixedCfgCreationPassTest extends CfgTestFixture(() => new JsCfgTestCpg())
       succOf("0") shouldBe expected(("_tmp_0[0]", AlwaysEdge))
       succOf("_tmp_0[0]") shouldBe expected(("a = _tmp_0[0]", AlwaysEdge))
 
-      succOf("a = _tmp_0[0]") shouldBe expected(("rest", AlwaysEdge))
-      succOf("rest") shouldBe expected(("_tmp_0", 2, AlwaysEdge))
+      succOf("a = _tmp_0[0]") shouldBe expected(("_tmp_0", 2, AlwaysEdge))
       succOf("_tmp_0", 2) shouldBe expected(("1", AlwaysEdge))
       succOf("1") shouldBe expected(("_tmp_0[1]", AlwaysEdge))
-      succOf("_tmp_0[1]") shouldBe expected(("rest = _tmp_0[1]", AlwaysEdge))
-      succOf("rest = _tmp_0[1]") shouldBe expected(("_tmp_0", 3, AlwaysEdge))
+      succOf("_tmp_0[1]") shouldBe expected(("rest", AlwaysEdge))
+      succOf("rest") shouldBe expected(("...rest", AlwaysEdge))
+      succOf("...rest") shouldBe expected(("_tmp_0", 3, AlwaysEdge))
       succOf("_tmp_0", 3) shouldBe expected(("var [a, ...rest] = x", AlwaysEdge))
       succOf("var [a, ...rest] = x") shouldBe expected(("RET", AlwaysEdge))
     }
 
     "be correct for array destruction assignment as parameter" in {
       implicit val cpg: Cpg = code("""
-                                     |function userId([id]) {
-                                     |  return id
-                                     |}
-                                     |""".stripMargin)
+       |function userId([id]) {
+       |  return id
+       |}
+       |""".stripMargin)
       succOf("userId", NodeTypes.METHOD) shouldBe expected(("id", AlwaysEdge))
       succOf("id") shouldBe expected(("param1_0", AlwaysEdge))
       succOf("param1_0") shouldBe expected(("id", 1, AlwaysEdge))
@@ -391,7 +389,8 @@ class MixedCfgCreationPassTest extends CfgTestFixture(() => new JsCfgTestCpg())
         implicit val cpg: Cpg = code("foo(...args)")
         succOf(":program") shouldBe expected(("foo", AlwaysEdge))
         succOf("foo") shouldBe expected(("this", AlwaysEdge))
-        succOf("this", NodeTypes.IDENTIFIER) shouldBe expected(("...args", AlwaysEdge))
+        succOf("this", NodeTypes.IDENTIFIER) shouldBe expected(("args", AlwaysEdge))
+        succOf("args") shouldBe expected(("...args", AlwaysEdge))
         succOf("...args") shouldBe expected(("foo(...args)", AlwaysEdge))
         succOf("foo(...args)") shouldBe expected(("RET", AlwaysEdge))
       }