Repository: joernio/joern (scala)

Issue:
jssrc2cpg: member initialization missing
Similar to https://github.com/joernio/joern/issues/1757 (javasrc), jssrc2cpg also does not represent member initializations in the following code:

```
class Foo {
   x = "foo"
}
```

My suggestion would be to model it in the exact same way as it is done by the Java frontend. That way, the ossdataflowengine will pick this up correctly.

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

Patch:
diff --git a/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/AstCreatorHelper.scala b/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/AstCreatorHelper.scala
index 607ffa71d48f..c9cfbfd85731 100644
--- a/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/AstCreatorHelper.scala
+++ b/joern-cli/frontends/c2cpg/src/main/scala/io/joern/c2cpg/astcreation/AstCreatorHelper.scala
@@ -411,8 +411,8 @@ trait AstCreatorHelper { this: AstCreator =>
     val code = childrenAsts.flatMap(_.nodes.headOption.map(_.asInstanceOf[NewCall].code)).mkString(",")
     val fakeStaticInitMethod =
       NewMethod()
-        .name("<sinit>")
-        .fullName(s"$name:<sinit>")
+        .name("<clinit>")
+        .fullName(s"$name:<clinit>")
         .code(code)
         .filename(filename)
         .lineNumber(lineNumber)
diff --git a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstForTypesCreator.scala b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstForTypesCreator.scala
index bbf450222186..362fc2cd2436 100644
--- a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstForTypesCreator.scala
+++ b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstForTypesCreator.scala
@@ -14,6 +14,7 @@ import io.shiftleft.codepropertygraph.generated.nodes.NewNamespaceBlock
 import io.shiftleft.codepropertygraph.generated.nodes.NewCall
 import io.shiftleft.codepropertygraph.generated.DispatchTypes
 import io.shiftleft.codepropertygraph.generated.Operators
+import io.shiftleft.codepropertygraph.generated.nodes.NewTypeDecl
 import ujson.Value
 
 import scala.util.Try
@@ -142,7 +143,48 @@ trait AstForTypesCreator { this: AstCreator =>
     } else {
       Seq(Ast(memberNode))
     }
+  }
 
+  private def astForClassMember(classElement: Value, typeDeclNode: NewTypeDecl, metaTypeDeclNode: NewTypeDecl): Ast = {
+    val nodeInfo = createBabelNodeInfo(classElement)
+    val memberNode = nodeInfo.node match {
+      case ClassMethod | ClassPrivateMethod =>
+        val function = createMethodAstAndNode(nodeInfo).methodNode
+        addModifier(function, nodeInfo.json)
+        val dynamicTypeHintFullName = Some(function.fullName)
+        createMemberNode(function.name, nodeInfo.code, dynamicTypeHintFullName)
+      case _ =>
+        val name = nodeInfo.node match {
+          case ClassProperty        => code(nodeInfo.json("key"))
+          case ClassPrivateProperty => code(nodeInfo.json("key")("id"))
+          // TODO: name field most likely needs adjustment for other Babel AST types
+          case _ => nodeInfo.code
+        }
+        createMemberNode(name, nodeInfo.code, dynamicTypeOption = None)
+    }
+    addModifier(memberNode, classElement)
+    if (classElement("static").bool) {
+      diffGraph.addEdge(metaTypeDeclNode, memberNode, EdgeTypes.AST)
+    } else {
+      diffGraph.addEdge(typeDeclNode, memberNode, EdgeTypes.AST)
+    }
+
+    if (hasKey(nodeInfo.json, "value") && !nodeInfo.json("value").isNull) {
+      val lhsAst = astForNode(nodeInfo.json("key"))
+      val rhsAst = astForNodeWithFunctionReference(nodeInfo.json("value"))
+      val callNode =
+        createCallNode(
+          nodeInfo.code,
+          Operators.assignment,
+          DispatchTypes.STATIC_DISPATCH,
+          nodeInfo.lineNumber,
+          nodeInfo.columnNumber
+        )
+      val argAsts = List(lhsAst, rhsAst)
+      createCallAst(callNode, argAsts)
+    } else {
+      Ast()
+    }
   }
 
   protected def astForEnum(tsEnum: BabelNodeInfo): Ast = {
@@ -238,32 +280,8 @@ trait AstForTypesCreator { this: AstCreator =>
     diffGraph.addEdge(metaTypeDeclNode, constructorBindingNode, EdgeTypes.BINDS)
     diffGraph.addEdge(constructorBindingNode, constructorNode, EdgeTypes.REF)
 
-    val classBodyElements = classMembers(clazz, withConstructor = false)
-
-    classBodyElements.foreach { classElement =>
-      val nodeInfo = createBabelNodeInfo(classElement)
-      val memberNode = nodeInfo.node match {
-        case ClassMethod | ClassPrivateMethod =>
-          val function = createMethodAstAndNode(nodeInfo).methodNode
-          addModifier(function, nodeInfo.json)
-          val dynamicTypeHintFullName = Some(function.fullName)
-          createMemberNode(function.name, nodeInfo.code, dynamicTypeHintFullName)
-        case _ =>
-          val name = nodeInfo.node match {
-            case ClassProperty        => code(nodeInfo.json("key"))
-            case ClassPrivateProperty => code(nodeInfo.json("key")("id"))
-            // TODO: name field most likely needs adjustment for other Babel AST types
-            case _ => nodeInfo.code
-          }
-          createMemberNode(name, nodeInfo.code, dynamicTypeOption = None)
-      }
-      addModifier(memberNode, classElement)
-      if (classElement("static").bool) {
-        diffGraph.addEdge(metaTypeDeclNode, memberNode, EdgeTypes.AST)
-      } else {
-        diffGraph.addEdge(typeDeclNode, memberNode, EdgeTypes.AST)
-      }
-    }
+    val memberInitCalls =
+      classMembers(clazz, withConstructor = false).map(m => astForClassMember(m, typeDeclNode, metaTypeDeclNode))
 
     methodAstParentStack.pop()
     dynamicInstanceTypeStack.pop()
@@ -271,6 +289,12 @@ trait AstForTypesCreator { this: AstCreator =>
     scope.popScope()
     scope.popScope()
 
+    if (memberInitCalls.nonEmpty) {
+      val init =
+        createAstForFakeStaticInitMethod(typeFullName, parserResult.filename, clazz.lineNumber, memberInitCalls)
+      Ast.storeInDiffGraph(init, diffGraph)
+      diffGraph.addEdge(typeDeclNode, init.nodes.head, EdgeTypes.AST)
+    }
     Ast(metaTypeRefNode)
   }
 
diff --git a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstNodeBuilder.scala b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstNodeBuilder.scala
index b8a5ba2f54c6..64c77f680857 100644
--- a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstNodeBuilder.scala
+++ b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstNodeBuilder.scala
@@ -341,8 +341,8 @@ trait AstNodeBuilder { this: AstCreator =>
     val code = childrenAsts.flatMap(_.nodes.headOption.map(_.asInstanceOf[NewCall].code)).mkString(",")
     val fakeStaticInitMethod =
       NewMethod()
-        .name("<sinit>")
-        .fullName(s"$name:<sinit>")
+        .name("<clinit>")
+        .fullName(s"$name:<clinit>")
         .code(code)
         .filename(filename)
         .lineNumber(lineNumber)


Test patch:
diff --git a/joern-cli/frontends/c2cpg/src/test/scala/io/joern/c2cpg/passes/types/EnumTypeTests.scala b/joern-cli/frontends/c2cpg/src/test/scala/io/joern/c2cpg/passes/types/EnumTypeTests.scala
index c5467250efc7..6174f4ec2a02 100644
--- a/joern-cli/frontends/c2cpg/src/test/scala/io/joern/c2cpg/passes/types/EnumTypeTests.scala
+++ b/joern-cli/frontends/c2cpg/src/test/scala/io/joern/c2cpg/passes/types/EnumTypeTests.scala
@@ -31,11 +31,11 @@ class EnumTypeTests extends CCodeToCpgSuite(fileSuffix = FileDefaults.CPP_EXT) {
           green.name shouldBe "green"
           blue.name shouldBe "blue"
         }
-        inside(color.astChildren.isMethod.l) { case List(sinit) =>
-          sinit.name shouldBe "<sinit>"
-          sinit.fullName shouldBe "color:<sinit>"
-          sinit.code shouldBe "green = 20"
-          inside(sinit.ast.isCall.l) { case List(greenInit) =>
+        inside(color.astChildren.isMethod.l) { case List(clinit) =>
+          clinit.name shouldBe "<clinit>"
+          clinit.fullName shouldBe "color:<clinit>"
+          clinit.code shouldBe "green = 20"
+          inside(clinit.ast.isCall.l) { case List(greenInit) =>
             greenInit.code shouldBe "green = 20"
           }
         }
@@ -64,11 +64,11 @@ class EnumTypeTests extends CCodeToCpgSuite(fileSuffix = FileDefaults.CPP_EXT) {
             green.name shouldBe "green"
             blue.name shouldBe "blue"
           }
-          inside(color.astChildren.isMethod.l) { case List(sinit) =>
-            sinit.name shouldBe "<sinit>"
-            sinit.fullName shouldBe "color:<sinit>"
-            sinit.code shouldBe "green = 20"
-            inside(sinit.ast.isCall.l) { case List(greenInit) =>
+          inside(color.astChildren.isMethod.l) { case List(clinit) =>
+            clinit.name shouldBe "<clinit>"
+            clinit.fullName shouldBe "color:<clinit>"
+            clinit.code shouldBe "green = 20"
+            inside(clinit.ast.isCall.l) { case List(greenInit) =>
               greenInit.code shouldBe "green = 20"
             }
           }
@@ -91,11 +91,11 @@ class EnumTypeTests extends CCodeToCpgSuite(fileSuffix = FileDefaults.CPP_EXT) {
           low.name shouldBe "low"
           low.typeFullName shouldBe "char"
         }
-        inside(altitude.astChildren.isMethod.l) { case List(sinit) =>
-          sinit.name shouldBe "<sinit>"
-          sinit.fullName shouldBe "altitude:<sinit>"
-          sinit.code shouldBe "high='h',low='l'"
-          inside(sinit.ast.isCall.code.l) { case List(highInit, lowInit) =>
+        inside(altitude.astChildren.isMethod.l) { case List(clinit) =>
+          clinit.name shouldBe "<clinit>"
+          clinit.fullName shouldBe "altitude:<clinit>"
+          clinit.code shouldBe "high='h',low='l'"
+          inside(clinit.ast.isCall.code.l) { case List(highInit, lowInit) =>
             highInit shouldBe "high='h'"
             lowInit shouldBe "low='l'"
           }
diff --git a/joern-cli/frontends/c2cpg/src/test/scala/io/joern/c2cpg/passes/types/StructTypeTests.scala b/joern-cli/frontends/c2cpg/src/test/scala/io/joern/c2cpg/passes/types/StructTypeTests.scala
index dd898b61fd82..1688636d1b43 100644
--- a/joern-cli/frontends/c2cpg/src/test/scala/io/joern/c2cpg/passes/types/StructTypeTests.scala
+++ b/joern-cli/frontends/c2cpg/src/test/scala/io/joern/c2cpg/passes/types/StructTypeTests.scala
@@ -23,8 +23,8 @@ class StructTypeTests extends CCodeToCpgSuite {
     }
 
     "initialize array members correctly" in {
-      val List(fakeInitMethod)                  = cpg.method.nameExact("<sinit>").l
-      val List(aInitCall, bInitCall, cInitCall) = fakeInitMethod.call.nameExact(Operators.arrayInitializer).l
+      val List(clInitMethod)                    = cpg.method.nameExact("<clinit>").l
+      val List(aInitCall, bInitCall, cInitCall) = clInitMethod.call.nameExact(Operators.arrayInitializer).l
 
       aInitCall.code shouldBe "a[SIZE]"
       val List(argAInit) = aInitCall.argument.l
diff --git a/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/ast/JsClassesAstCreationPassTest.scala b/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/ast/JsClassesAstCreationPassTest.scala
index 9eea72366391..4d15399dbb39 100644
--- a/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/ast/JsClassesAstCreationPassTest.scala
+++ b/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/ast/JsClassesAstCreationPassTest.scala
@@ -59,6 +59,28 @@ class JsClassesAstCreationPassTest extends AbstractPassTest {
       memberFoo.code shouldBe "foo() {}"
     }
 
+    "have member with initialization in TYPE_DECL for ClassA" in AstFixture("""
+        |class ClassA {
+        |  a = 1
+        |  b = "foo"
+        |  c = true
+        |}""".stripMargin) { cpg =>
+      val List(classATypeDecl) = cpg.typeDecl.nameExact("ClassA").fullNameExact("code.js::program:ClassA").l
+      val List(a, b, c)        = classATypeDecl.member.l
+      a.name shouldBe "a"
+      a.code shouldBe "a = 1"
+      b.name shouldBe "b"
+      b.code shouldBe """b = "foo""""
+      c.name shouldBe "c"
+      c.code shouldBe "c = true"
+
+      val List(clInitMethod)                    = cpg.typeDecl.nameExact("ClassA").method.nameExact("<clinit>").l
+      val List(aInitCall, bInitCall, cInitCall) = clInitMethod.ast.isCall.l
+      aInitCall.code shouldBe "a = 1"
+      bInitCall.code shouldBe """b = "foo""""
+      cInitCall.code shouldBe "c = true"
+    }
+
     "have method for non-static method in ClassA AST" in AstFixture("""
         |var x = class ClassA {
         |  foo() {}
diff --git a/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/ast/TsAstCreationPassTest.scala b/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/ast/TsAstCreationPassTest.scala
index 2104bdf6b7c5..2e929edb1d99 100644
--- a/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/ast/TsAstCreationPassTest.scala
+++ b/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/passes/ast/TsAstCreationPassTest.scala
@@ -94,7 +94,7 @@ class TsAstCreationPassTest extends AbstractPassTest {
         direction.fullName shouldBe "code.ts::program:Direction"
         direction.filename shouldBe "code.ts"
         direction.file.name.head shouldBe "code.ts"
-        inside(direction.method.name("<sinit>").l) { case List(init) =>
+        inside(direction.method.name("<clinit>").l) { case List(init) =>
           init.block.astChildren.isCall.code.head shouldBe "Up = 1"
         }
         inside(cpg.typeDecl("Direction").member.l) { case List(up, down, left, right) =>