Repository: joernio/joern (scala)

Issue:
[Bug] jssrc+js2cpg missing REF between identifier and parameter for destructuring pattern
e.g. at https://github.com/valentin-panov/next-js-test-env/blob/03175c2cad7c4a9f369243ff527d41e5684523d8/components/windowOpen/windowOpen.tsx#L8


```
cpg.identifier.name("param1_0").refsTo.size
res170: Int = 0
```

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

Patch:
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 14535b2978d3..1aa408d8fd91 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
@@ -72,14 +72,38 @@ trait AstForDeclarationsCreator { this: AstCreator =>
     exportCallAst
   }
 
-  private def createExportAssignmentCallAst(name: String, exportCallAst: Ast, declaration: BabelNodeInfo): Ast =
-    createAssignmentCallAst(
-      exportCallAst,
-      Ast(createIdentifierNode(name, declaration)),
-      s"${codeOf(exportCallAst.nodes.head)} = $name",
-      declaration.lineNumber,
-      declaration.columnNumber
-    )
+  private def createExportAssignmentCallAst(
+    name: String,
+    exportCallAst: Ast,
+    declaration: BabelNodeInfo,
+    from: Option[String]
+  ): Ast = {
+    from match {
+      case Some(value) =>
+        val call = createFieldAccessCallAst(
+          createIdentifierNode(value, None, declaration.lineNumber, declaration.columnNumber),
+          createFieldIdentifierNode(name, declaration.lineNumber, declaration.columnNumber),
+          declaration.lineNumber,
+          declaration.columnNumber
+        )
+        createAssignmentCallAst(
+          exportCallAst,
+          call,
+          s"${codeOf(exportCallAst.nodes.head)} = ${codeOf(call.nodes.head)}",
+          declaration.lineNumber,
+          declaration.columnNumber
+        )
+      case None =>
+        createAssignmentCallAst(
+          exportCallAst,
+          Ast(createIdentifierNode(name, declaration)),
+          s"${codeOf(exportCallAst.nodes.head)} = $name",
+          declaration.lineNumber,
+          declaration.columnNumber
+        )
+    }
+
+  }
 
   private def extractDeclarationsFromExportDecl(declaration: BabelNodeInfo, key: String): Option[(Ast, Seq[String])] =
     safeObj(declaration.json, key)
@@ -161,20 +185,24 @@ trait AstForDeclarationsCreator { this: AstCreator =>
         if (exportName != ExportKeyword)
           diffGraph.addNode(createDependencyNode(name, exportName.stripPrefix("_"), RequireKeyword))
         val exportCallAst = createExportCallAst(name, exportName, declaration)
-        createExportAssignmentCallAst(name, exportCallAst, declaration)
+        createExportAssignmentCallAst(name, exportCallAst, declaration, None)
       }
     }
 
     val specifierAsts = specifiers.map {
       case (Some(name), Some(alias)) =>
-        if (exportName != ExportKeyword)
+        val strippedCode  = cleanImportName(exportName).stripPrefix("_")
+        val exportCallAst = createExportCallAst(alias.code, ExportKeyword, declaration)
+        if (exportName != ExportKeyword) {
           diffGraph.addNode(createDependencyNode(alias.code, exportName.stripPrefix("_"), RequireKeyword))
-        val exportCallAst = createExportCallAst(alias.code, exportName, declaration)
-        createExportAssignmentCallAst(name.code, exportCallAst, declaration)
+          createExportAssignmentCallAst(name.code, exportCallAst, declaration, Option(s"_$strippedCode"))
+        } else {
+          createExportAssignmentCallAst(name.code, exportCallAst, declaration, None)
+        }
       case (None, Some(alias)) =>
         diffGraph.addNode(createDependencyNode(alias.code, exportName.stripPrefix("_"), RequireKeyword))
         val exportCallAst = createExportCallAst(alias.code, ExportKeyword, declaration)
-        createExportAssignmentCallAst(exportName, exportCallAst, declaration)
+        createExportAssignmentCallAst(exportName, exportCallAst, declaration, None)
       case _ => Ast()
     }
 
@@ -188,7 +216,7 @@ trait AstForDeclarationsCreator { this: AstCreator =>
     val declAsts = expressionAstWithNames.map { case (ast, names) =>
       ast +: names.map { name =>
         val exportCallAst = createExportCallAst(name, ExportKeyword, assignment)
-        createExportAssignmentCallAst(name, exportCallAst, assignment)
+        createExportAssignmentCallAst(name, exportCallAst, assignment, None)
       }
     }
 
@@ -204,7 +232,7 @@ trait AstForDeclarationsCreator { this: AstCreator =>
     val declAsts = declAstAndNames.map { case (ast, names) =>
       ast +: names.map { name =>
         val exportCallAst = createExportCallAst(DefaultsKey, exportName, declaration)
-        createExportAssignmentCallAst(name, exportCallAst, declaration)
+        createExportAssignmentCallAst(name, exportCallAst, declaration, None)
       }
     }
 
@@ -223,7 +251,7 @@ trait AstForDeclarationsCreator { this: AstCreator =>
 
     val fromCallAst       = createAstForFrom(exportName, declaration)
     val exportCallAst     = createExportCallAst(name, ExportKeyword, declaration)
-    val assignmentCallAst = createExportAssignmentCallAst(s"_$name", exportCallAst, declaration)
+    val assignmentCallAst = createExportAssignmentCallAst(s"_$name", exportCallAst, declaration, None)
 
     val asts = List(fromCallAst, assignmentCallAst)
     setArgIndices(asts)
diff --git a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstForFunctionsCreator.scala b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstForFunctionsCreator.scala
index 951ff6b5eb70..c0f2eea81240 100644
--- a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstForFunctionsCreator.scala
+++ b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstForFunctionsCreator.scala
@@ -4,6 +4,7 @@ import io.joern.jssrc2cpg.datastructures.BlockScope
 import io.joern.jssrc2cpg.datastructures.MethodScope
 import io.joern.jssrc2cpg.parser.BabelAst._
 import io.joern.jssrc2cpg.parser.BabelNodeInfo
+import io.joern.jssrc2cpg.passes.Defines
 import io.joern.x2cpg.Ast
 import io.joern.x2cpg.datastructures.Stack._
 import io.shiftleft.codepropertygraph.generated.nodes.{Identifier => _, _}
@@ -135,6 +136,8 @@ trait AstForFunctionsCreator { this: AstCreator =>
                 scope.addVariable(elemName, localNode, MethodScope)
 
                 val paramNode = createIdentifierNode(paramName, elementNodeInfo)
+                scope.addVariableReference(paramName, paramNode)
+
                 val keyNode =
                   createFieldIdentifierNode(elemName, elementNodeInfo.lineNumber, elementNodeInfo.columnNumber)
                 val accessAst =
@@ -178,6 +181,8 @@ trait AstForFunctionsCreator { this: AstCreator =>
               scope.addVariable(elemName, localNode, MethodScope)
 
               val paramNode = createIdentifierNode(paramName, elementNodeInfo)
+              scope.addVariableReference(paramName, paramNode)
+
               val keyNode =
                 createFieldIdentifierNode(elemName, elementNodeInfo.lineNumber, elementNodeInfo.columnNumber)
               val accessAst =


Test patch:
diff --git a/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/ast/DependencyAstCreationPassTest.scala b/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/ast/DependencyAstCreationPassTest.scala
index 9b7b9bd350bd..0d96a8a92073 100644
--- a/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/ast/DependencyAstCreationPassTest.scala
+++ b/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/ast/DependencyAstCreationPassTest.scala
@@ -96,8 +96,8 @@ class DependencyAstCreationPassTest extends AbstractPassTest {
       c.importedEntity shouldBe Option(":c")
       c.importedAs shouldBe Option("c")
       d.code shouldBe "import * as d from \"depD\""
-      d.importedEntity shouldBe Some("depD:d")
-      d.importedAs shouldBe Some("d")
+      d.importedEntity shouldBe Option("depD:d")
+      d.importedAs shouldBe Option("d")
       val List(n) = a.namespaceBlock.l
       n.fullName shouldBe "code.js:<global>"
     }
@@ -356,6 +356,17 @@ class DependencyAstCreationPassTest extends AbstractPassTest {
       cpg.method("foo").code.l shouldBe List("function foo(param) {}")
     }
 
+    "have correct structure for export with from clause with path" in AstFixture("""
+        |export { def as Header } from "./path/to/header";
+        |""".stripMargin) { cpg =>
+      val List(header) = cpg.dependency.l
+      header.name shouldBe "Header"
+      header.dependencyGroupId shouldBe Option("./path/to/header")
+      header.version shouldBe "require"
+      cpg.assignment.code.l shouldBe
+        List("var _header = require(\"./path/to/header\")", "exports.Header = _header.def")
+    }
+
     "have correct structure for export with from clause" in AstFixture("""
        |export { import1 as name1, import2 as name2, name3 } from "Foo";
        |export bar from "Bar";
@@ -380,11 +391,11 @@ class DependencyAstCreationPassTest extends AbstractPassTest {
 
       cpg.assignment.code.l shouldBe List(
         "var _Foo = require(\"Foo\")",
-        "_Foo.name1 = import1",
-        "_Foo.name2 = import2",
-        "_Foo.name3 = name3",
+        "exports.name1 = _Foo.import1",
+        "exports.name2 = _Foo.import2",
+        "exports.name3 = _Foo.name3",
         "var _Bar = require(\"Bar\")",
-        "_Bar.bar = bar"
+        "exports.bar = _Bar.bar"
       )
     }
 
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 d7fe58ce1112..eaffd9221d2f 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,7 @@ 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.MethodParameterIn
 import io.shiftleft.semanticcpg.language._
 
 class MixedAstCreationPassTest extends AbstractPassTest {
@@ -788,6 +789,23 @@ class MixedAstCreationPassTest extends AbstractPassTest {
       tmpReturnIdentifier.name shouldBe "_tmp_0"
     }
 
+    "have correct ref edge (destructing parameter)" in AstFixture("""
+        |const WindowOpen = ({ value }) => {
+        |  return (
+        |    <div>
+        |      <Button variant="outlined" onClick={() => windowOpenButton(value)}>
+        |        TRY ME!
+        |      </Button>
+        |    </div>
+        |  );
+        |};
+        """.stripMargin) { cpg =>
+      val List(param) = cpg.identifier.name("param1_0").refsTo.collectAll[MethodParameterIn].l
+      param.name shouldBe "param1_0"
+      param.code shouldBe "{ value }"
+      param.method.fullName shouldBe "code.js::program:anonymous"
+    }
+
     "have correct structure for object deconstruction in function parameter" in AstFixture(
       "function foo({ a }, b) {};"
     ) { cpg =>