Repository: joernio/joern (scala)

Issue:
jssrc2cpg: Support for Dynamic Declared Members
**Is your feature request related to a problem? Please describe.**
In the code below, the fields dynamically declared under the constructor are not populated as members:
```javascript
class ClassA {
  constructor(param1, param2) {
    this.e = param1;
    this.f = param2;
  }
}
```
`cpg.typeDecl.nameExact("ClassA").member.l` will not reflect any members.

During debugging, under `AstForExpressionsCreator::astForMemberExpression` I do see these coming through and these are labelled as field accesses, but the corresponding members aren't created. 

Modifying the `JsClassesAstCreationPassTest::have member with initialization in TYPE_DECL for ClassA` to look like:
```javascript
class ClassA {
  a = 1
  b = "foo"
  static c = true
  static d
  static {
    this.d = false
  }

  constructor(param1, param2) {
    this.e = param1;
    this.f = param2;
  }
}
```
I can observe `List(a, b, c, d)`. I'm just not quite sure where to register `e` and `f` to become members.

cc @max-leuthaeuser 

**Describe the solution you'd like**
I would like `this.foo <operator>.assignment (Literal|Identifier|MethodRef|TypeRef)` calls within a type to be registered as members under the type.

**Additional context**
If you could point me to how to register the members then I can go ahead and do it myself, but I am slightly thrown off atm.

Pull request:
Class members that are dynamically initialized in a class constructor are added as class member now. This init assignment call itself stays in the constructor block.

Fixes: https://github.com/joernio/joern/issues/2612

Patch:
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 a31d613f31c6..214fb4eae4ee 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
@@ -69,10 +69,17 @@ trait AstForTypesCreator { this: AstCreator =>
 
   private def classMembers(clazz: BabelNodeInfo, withConstructor: Boolean = true): Seq[Value] = {
     val allMembers = Try(clazz.json("body")("body").arr).toOption.toSeq.flatten
-    if (withConstructor) {
+    val dynamicallyDeclaredMembers =
       allMembers
+        .find(isConstructor)
+        .flatMap(c => Try(c("body")("body").arr).toOption)
+        .toSeq
+        .flatten
+        .filter(isInitializedMember)
+    if (withConstructor) {
+      allMembers ++ dynamicallyDeclaredMembers
     } else {
-      allMembers.filterNot(isConstructor)
+      allMembers.filterNot(isConstructor) ++ dynamicallyDeclaredMembers
     }
   }
 
@@ -178,6 +185,10 @@ trait AstForTypesCreator { this: AstCreator =>
         addModifier(function, nodeInfo.json)
         val dynamicTypeHintFullName = Option(function.fullName)
         createMemberNode(function.name, nodeInfo, dynamicTypeHintFullName)
+      case ExpressionStatement if isInitializedMember(classElement) =>
+        val memberNodeInfo = createBabelNodeInfo(nodeInfo.json("expression")("left")("property"))
+        val name           = memberNodeInfo.code
+        createMemberNode(name, nodeInfo, dynamicTypeOption = None)
       case _ =>
         val name = nodeInfo.node match {
           case ClassProperty        => code(nodeInfo.json("key"))
@@ -265,7 +276,18 @@ trait AstForTypesCreator { this: AstCreator =>
     nodeInfo != ClassMethod && nodeInfo != ClassPrivateMethod && isStatic
   }
 
-  private def isInitializedMember(json: Value): Boolean = hasKey(json, "value") && !json("value").isNull
+  private def isInitializedMember(json: Value): Boolean = {
+    val hasInitializedValue = hasKey(json, "value") && !json("value").isNull
+    val isAssignment = createBabelNodeInfo(json) match {
+      case node if node.node == ExpressionStatement =>
+        val exprNode = createBabelNodeInfo(node.json("expression"))
+        exprNode.node == AssignmentExpression &&
+        createBabelNodeInfo(exprNode.json("left")).node == MemberExpression &&
+        code(exprNode.json("left")("object")) == "this"
+      case _ => false
+    }
+    hasInitializedValue || isAssignment
+  }
 
   private def isStaticInitBlock(json: Value): Boolean = createBabelNodeInfo(json).node == StaticBlock
 


Test patch:
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 e4a4bb6507e1..c494bcaccea6 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
@@ -103,13 +103,24 @@ class JsClassesAstCreationPassTest extends AbstractPassTest {
         |  static {
         |    this.d = false
         |  }
+        |  constructor(param1, param2) {
+        |    // also register e and f as dynamically declared members
+        |    this.e = param1;
+        |    this.f = param2;
+        |    // chained access should not result in member creation
+        |    this.f.g = param2;
+        |  }
         |}""".stripMargin) { cpg =>
       val List(classATypeDecl) = cpg.typeDecl.nameExact("ClassA").fullNameExact("code.js::program:ClassA").l
-      val List(a, b)           = classATypeDecl.member.not(_.isStatic).l
+      val List(a, b, e, f)     = classATypeDecl.member.not(_.isStatic).l
       a.name shouldBe "a"
       a.code shouldBe "a = 1"
       b.name shouldBe "b"
       b.code shouldBe """b = "foo""""
+      e.name shouldBe "e"
+      e.code shouldBe "this.e = param1;"
+      f.name shouldBe "f"
+      f.code shouldBe "this.f = param2;"
 
       val List(c, d) = classATypeDecl.member.isStatic.l
       c.name shouldBe "c"
@@ -124,9 +135,12 @@ class JsClassesAstCreationPassTest extends AbstractPassTest {
 
       val List(constructor) =
         cpg.typeDecl.nameExact("ClassA").method.nameExact(io.joern.x2cpg.Defines.ConstructorMethodName).l
-      val List(aInitCall, bInitCall) = constructor.block.assignment.l
+      val List(aInitCall, bInitCall, eInitCall, fInitCall, gCall) = constructor.block.assignment.l
       aInitCall.code shouldBe "a = 1"
       bInitCall.code shouldBe """b = "foo""""
+      eInitCall.code shouldBe "this.e = param1"
+      fInitCall.code shouldBe "this.f = param2"
+      gCall.code shouldBe "this.f.g = param2"
     }
 
     "have method for non-static method in ClassA AST" in AstFixture("""
@@ -139,7 +153,7 @@ class JsClassesAstCreationPassTest extends AbstractPassTest {
       methodFoo.code shouldBe "foo() {}"
     }
 
-    "have TYPE_REF to <meta> for ClassA" in AstFixture("var x = class ClassA {}") { cpg =>
+    "have TYPE_REF to ClassA" in AstFixture("var x = class ClassA {}") { cpg =>
       val List(program)         = cpg.method.nameExact(":program").l
       val List(programBlock)    = program.astChildren.isBlock.l
       val List(assignmentToTmp) = programBlock.astChildren.isCall.l