Repository: joernio/joern (scala)

Issue:
jssrc2cpg: dealing with `const foo = (x,y) => { ... }`
The following failing test case illustrates a problem with currently have when analyzing type script code:
https://github.com/joernio/joern/compare/master...fabs/flowconst#diff-afb5c19d03ad15d4ad7e4d95280e9b70f5e434466d8eb2f414ae4444fd35741bR600

For a function defined anonymously and assigned to a constant, we swallow the assignment to the constant and the name of the function remains unknown in the CPG, for example, for

```
const foo = (x,y) => { ... }
```

we end up with a `METHOD` node where the `NAME` and `FULL_NAME` fields simply tell us that this is an anonymous method. The assignment to `foo` is not visible in the CPG. I would propose that we either make the assignment visible, or we simply handle this as an alternative way of defining a named method. The latter is probably easier and would mean that this works without additional changes in the data flow engine.

Pull request description:
Fixes #1870

Patch:
diff --git a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/JsSrc2Cpg.scala b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/JsSrc2Cpg.scala
index e73557f8590d..a320b0e0be57 100644
--- a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/JsSrc2Cpg.scala
+++ b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/JsSrc2Cpg.scala
@@ -7,6 +7,7 @@ import io.joern.jssrc2cpg.passes.{
   AstCreationPass,
   BuiltinTypesPass,
   ConfigPass,
+  ConstClosurePass,
   DependenciesPass,
   JsMetaDataPass,
   PrivateKeyFilePass,
@@ -20,6 +21,7 @@ import io.joern.x2cpg.X2Cpg.withNewEmptyCpg
 import io.joern.x2cpg.X2CpgFrontend
 import io.joern.x2cpg.utils.HashUtil
 import io.joern.x2cpg.SourceFiles
+import io.joern.x2cpg.passes.frontend.JavascriptCallLinker
 import io.shiftleft.passes.CpgPassBase
 import io.shiftleft.semanticcpg.layers.LayerCreatorContext
 
@@ -73,6 +75,6 @@ class JsSrc2Cpg extends X2CpgFrontend[Config] {
 object JsSrc2Cpg {
 
   def postProcessingPasses(cpg: Cpg): List[CpgPassBase] =
-    List(new RequirePass(cpg))
+    List(new RequirePass(cpg), new ConstClosurePass(cpg), new JavascriptCallLinker(cpg))
 
 }
diff --git a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/passes/ConstClosurePass.scala b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/passes/ConstClosurePass.scala
new file mode 100644
index 000000000000..04b9d0f57a18
--- /dev/null
+++ b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/passes/ConstClosurePass.scala
@@ -0,0 +1,24 @@
+package io.joern.jssrc2cpg.passes
+
+import io.shiftleft.codepropertygraph.Cpg
+import io.shiftleft.passes.SimpleCpgPass
+import overflowdb.BatchedUpdate
+import io.shiftleft.semanticcpg.language._
+import overflowdb.traversal._
+
+/** A pass that identifies assignments of closures to constants and updates `METHOD` nodes accordingly.
+  */
+class ConstClosurePass(cpg: Cpg) extends SimpleCpgPass(cpg) {
+  override def run(diffGraph: BatchedUpdate.DiffGraphBuilder): Unit = {
+    for {
+      assignment      <- cpg.assignment
+      name            <- assignment.filter(_.code.startsWith("const ")).target.isIdentifier.name
+      method          <- assignment.start.source.isMethodRef.referencedMethod
+      enclosingMethod <- assignment.start.method.fullName
+    } {
+      diffGraph.setNodeProperty(method, "NAME", name)
+      diffGraph.setNodeProperty(method, "FULL_NAME", s"$enclosingMethod:$name")
+    }
+
+  }
+}
diff --git a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/passes/RequirePass.scala b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/passes/RequirePass.scala
index 355d85141ea6..91159bfd3ec7 100644
--- a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/passes/RequirePass.scala
+++ b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/passes/RequirePass.scala
@@ -10,6 +10,8 @@ import overflowdb.traversal.{NodeOps, Traversal}
 
 import java.nio.file.Paths
 
+/** This pass enhances the call graph and call nodes by interpreting assignments of the form `foo = require("module")`.
+  */
 class RequirePass(cpg: Cpg) extends SimpleCpgPass(cpg) {
 
   private val codeRoot = cpg.metaData.root.headOption.getOrElse("")
diff --git a/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/layers/CallGraph.scala b/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/layers/CallGraph.scala
index 15bceef98b39..955b8c9b9bbe 100644
--- a/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/layers/CallGraph.scala
+++ b/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/layers/CallGraph.scala
@@ -14,13 +14,7 @@ object CallGraph {
   def defaultOpts         = new LayerCreatorOptions()
 
   def passes(cpg: Cpg): Iterator[CpgPassBase] = {
-    val languageSpecificPasses = cpg.metaData.language.lastOption match {
-      case Some(Languages.JSSRC)      => Iterator[CpgPassBase](new JavascriptCallLinker(cpg))
-      case Some(Languages.JAVASCRIPT) => Iterator[CpgPassBase](new JavascriptCallLinker(cpg))
-      case _                          => Iterator[CpgPassBase]()
-    }
-    languageSpecificPasses ++
-      Iterator(new MethodRefLinker(cpg), new StaticCallLinker(cpg), new DynamicCallLinker(cpg))
+    Iterator(new MethodRefLinker(cpg), new StaticCallLinker(cpg), new DynamicCallLinker(cpg))
   }
 
 }


Test patch:
diff --git a/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/dataflow/DataflowTest.scala b/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/dataflow/DataflowTest.scala
index ccab6b716383..84f04c8c0ab6 100644
--- a/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/dataflow/DataflowTest.scala
+++ b/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/dataflow/DataflowTest.scala
@@ -597,4 +597,20 @@ class DataflowTest extends DataFlowCodeToCpgSuite {
     sink.reachableBy(src).size shouldBe 1
   }
 
+  "Flow into method defined as lambda and assigned to constant" in {
+    val cpg: Cpg = code("""
+        | const foo = (x, y) => {
+        |   sink(x);
+        | };
+        |
+        | foo(1, 2);
+        |""".stripMargin)
+
+    val sink = cpg.call("sink").l
+    val src  = cpg.literal.code("1").l
+    sink.size shouldBe 1
+    src.size shouldBe 1
+    sink.reachableBy(src).size shouldBe 1
+  }
+
 }
diff --git a/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/ConstClosurePassTests.scala b/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/ConstClosurePassTests.scala
new file mode 100644
index 000000000000..e6f5a0bfb29f
--- /dev/null
+++ b/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/ConstClosurePassTests.scala
@@ -0,0 +1,20 @@
+package io.joern.jssrc2cpg.passes
+
+import io.joern.jssrc2cpg.testfixtures.DataFlowCodeToCpgSuite
+import io.shiftleft.semanticcpg.language._
+
+class ConstClosurePassTests extends DataFlowCodeToCpgSuite {
+
+  "should return method `foo` via `cpg.method`" in {
+    val cpg = code("""
+         const foo = (x,y) => {
+           return x + y;
+         }
+      """.stripMargin)
+
+    val List(m) = cpg.method.name("foo").l
+    m.name shouldBe "foo"
+    m.fullName.endsWith("program:foo") shouldBe true
+  }
+
+}