Repository: joernio/joern (scala)

Issue:
javasrc2cpg: member variable initialization missing
For the following code
```
class Foo {
    int x = 10;
    String a = "foo";
}
```
two `MEMBER` nodes are created, but the literals and initializations are nowhere to be found in the graph. I take it they should be present in the artificial constructor function?

Pull request description:
This PR fixes https://github.com/joernio/joern/issues/1757 by adding member initializers to constructors. For consistency with java bytecode, the initializers are added to the start of every constructor body, except if that body contains a `this(...)` call. All of the assignments are also assignments to a `fieldAccess` (e.g. `int x = 12` would be represented as `this.x = 12`), again for consistency with bytecode.

Patch:
diff --git a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/passes/AstCreator.scala b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/passes/AstCreator.scala
index 2a5ccd81ae42..8aa4c030a662 100644
--- a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/passes/AstCreator.scala
+++ b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/passes/AstCreator.scala
@@ -402,16 +402,14 @@ class AstCreator(filename: String, javaParserAst: CompilationUnit, global: Globa
           astForFieldVariable(variable, fieldDeclaration)
         }
 
-        val staticInitAsts = if (fieldDeclaration.isStatic) {
-          val assignments = assignmentsForVarDecl(
-            fieldDeclaration.getVariables.asScala.toList,
-            line(fieldDeclaration),
-            column(fieldDeclaration)
-          )
-          assignments
-        } else {
-          Nil
-        }
+        val assignments = assignmentsForVarDecl(
+          fieldDeclaration.getVariables.asScala.toList,
+          line(fieldDeclaration),
+          column(fieldDeclaration)
+        )
+
+        val staticInitAsts = if (fieldDeclaration.isStatic) assignments else Nil
+        if (!fieldDeclaration.isStatic) scopeStack.addMemberInitializersToScope(assignments)
 
         AstWithStaticInit(memberAsts, staticInitAsts)
 
@@ -445,7 +443,7 @@ class AstCreator(filename: String, javaParserAst: CompilationUnit, global: Globa
     identifierNode(name, typeFullName)
   }
 
-  private def clinitAstsFromStaticInits(staticInits: Seq[Ast]): Option[Ast] = {
+  private def clinitAstFromStaticInits(staticInits: Seq[Ast]): Option[Ast] = {
     Option.when(staticInits.nonEmpty) {
       val signature = composeMethodLikeSignature(TypeConstants.Void, Nil)
       val fullName = scopeStack.getEnclosingTypeDecl
@@ -585,7 +583,7 @@ class AstCreator(filename: String, javaParserAst: CompilationUnit, global: Globa
 
     val annotationAsts = typ.getAnnotations.asScala.map(astForAnnotationExpr)
 
-    val clinitAst = clinitAstsFromStaticInits(staticInits.toSeq)
+    val clinitAst = clinitAstFromStaticInits(staticInits.toSeq)
 
     val lambdaMethods = scopeStack.getLambdaMethodsInScope.toSeq
 
@@ -636,7 +634,7 @@ class AstCreator(filename: String, javaParserAst: CompilationUnit, global: Globa
       .isExternal(false)
 
     val thisAst = Ast(thisNodeForMethod(typeFullName, lineNumber = None))
-    val bodyAst = Ast(NewBlock())
+    val bodyAst = Ast(NewBlock()).withChildren(scopeStack.getMemberInitializers)
 
     val returnNode = methodReturnNode(TypeConstants.Void, None, None, None)
 
@@ -735,6 +733,7 @@ class AstCreator(filename: String, javaParserAst: CompilationUnit, global: Globa
     val fullName       = composeMethodFullName(typeFullName, NameConstants.Init, signature)
 
     val constructorNode = createPartialMethod(constructorDeclaration)
+      .name(NameConstants.Init)
       .fullName(fullName)
       .signature(signature)
 
@@ -749,7 +748,7 @@ class AstCreator(filename: String, javaParserAst: CompilationUnit, global: Globa
     scopeStack.addToScope(thisNode, thisNode.name, thisNode.typeFullName)
     val thisAst = Ast(thisNode)
 
-    val bodyAst      = astForMethodBody(Some(constructorDeclaration.getBody))
+    val bodyAst      = astForConstructorBody(Some(constructorDeclaration.getBody))
     val methodReturn = constructorReturnNode(constructorDeclaration)
 
     val annotationAsts = constructorDeclaration.getAnnotations.asScala.map(astForAnnotationExpr).toList
@@ -983,7 +982,7 @@ class AstCreator(filename: String, javaParserAst: CompilationUnit, global: Globa
       scopeStack.addToScope(node, node.name, node.typeFullName)
     }
 
-    val bodyAst = astForMethodBody(methodDeclaration.getBody.toScala)
+    val bodyAst = methodDeclaration.getBody.toScala.map(astForBlockStatement(_)).getOrElse(Ast(NewBlock()))
     val methodReturn =
       methodReturnNode(returnTypeFullName, None, line(methodDeclaration.getType), column(methodDeclaration.getType))
 
@@ -1024,10 +1023,23 @@ class AstCreator(filename: String, javaParserAst: CompilationUnit, global: Globa
     methodNode
   }
 
-  private def astForMethodBody(body: Option[BlockStmt]): Ast = {
+  private def astForConstructorBody(body: Option[BlockStmt]): Ast = {
+    val containsThisInvocation =
+      body
+        .flatMap(_.getStatements.asScala.headOption)
+        .collect { case e: ExplicitConstructorInvocationStmt => e }
+        .exists(_.isThis)
+
+    val memberInitializers =
+      if (containsThisInvocation)
+        Seq.empty
+      else
+        scopeStack.getMemberInitializers
+
     body match {
-      case Some(b) => astForBlockStatement(b)
-      case None    => Ast(NewBlock())
+      case Some(b) => astForBlockStatement(b, prefixAsts = memberInitializers)
+
+      case None => Ast(NewBlock()).withChildren(memberInitializers)
     }
   }
 
@@ -1983,7 +1995,7 @@ class AstCreator(filename: String, javaParserAst: CompilationUnit, global: Globa
         case List(identifier: NewIdentifier) =>
           // In this case we have a simple assign. No block needed.
           // e.g. Foo f = new Foo();
-          val initAst = completeInitForConstructor(partialConstructor, identifier)
+          val initAst = completeInitForConstructor(partialConstructor, Ast(identifier.copy))
           Seq(callAst(callNode, targetAst ++ argsAsts), initAst)
 
         case _ =>
@@ -2009,6 +2021,33 @@ class AstCreator(filename: String, javaParserAst: CompilationUnit, global: Globa
     }.toList
   }
 
+  private def copyAstForVarDeclInit(targetAst: Ast): Ast = {
+    targetAst.root match {
+      case Some(identifier: NewIdentifier) => Ast(identifier.copy)
+
+      case Some(fieldAccess: NewCall) if fieldAccess.name == Operators.fieldAccess =>
+        val maybeIdentifier = targetAst.nodes.collectFirst { case node if node.isInstanceOf[NewIdentifier] => node }
+        val maybeField = targetAst.nodes.collectFirst { case node if node.isInstanceOf[NewFieldIdentifier] => node }
+
+        (maybeIdentifier, maybeField) match {
+          case (Some(identifier), Some(fieldIdentifier)) =>
+            val args = List(identifier, fieldIdentifier).map(node => Ast(node.copy))
+            callAst(fieldAccess.copy, args)
+
+          case _ =>
+            logger.warn(s"Attempting to copy field access without required children: ${fieldAccess.code}")
+            Ast()
+        }
+
+      case Some(root) =>
+        logger.warn(s"Attempting to copy unhandled root type for var decl init: $root")
+        Ast()
+
+      case None =>
+        Ast()
+    }
+  }
+
   private def assignmentsForVarDecl(
     variables: Iterable[VariableDeclarator],
     lineNumber: Option[Integer],
@@ -2039,14 +2078,20 @@ class AstCreator(filename: String, javaParserAst: CompilationUnit, global: Globa
 
       val callNode = operatorCallNode(Operators.assignment, code, Some(typeFullName), lineNumber, columnNumber)
 
-      val identifier          = identifierNode(name, typeFullName, line(variable), column(variable))
-      val localCorrespToIdent = scopeStack.lookupVariable(name).map(_.node)
-      val targetAst           = Ast(identifier).withRefEdges(identifier, localCorrespToIdent.toList)
+      val targetAst = scopeStack.lookupVariable(name) match {
+        case Some(nodeTypeInfo) if nodeTypeInfo.isField && !nodeTypeInfo.isStatic =>
+          val thisType = scopeStack.getEnclosingTypeDecl.map(_.fullName).getOrElse("<empty>")
+          fieldAccessAst(NameConstants.This, thisType, name, Some(typeFullName), line(variable), column(variable))
+
+        case maybeCorrespNode =>
+          val identifier = identifierNode(name, typeFullName, line(variable), column(variable))
+          Ast(identifier).withRefEdges(identifier, maybeCorrespNode.map(_.node).toList)
+      }
 
       // Since all partial constructors will be dealt with here, don't pass them up.
       val declAst = callAst(callNode, Seq(targetAst) ++ initializerAsts)
 
-      val constructorAsts = partialConstructorQueue.map(completeInitForConstructor(_, identifier))
+      val constructorAsts = partialConstructorQueue.map(completeInitForConstructor(_, copyAstForVarDeclInit(targetAst)))
       partialConstructorQueue.clear()
 
       Seq(declAst) ++ constructorAsts
@@ -2055,14 +2100,11 @@ class AstCreator(filename: String, javaParserAst: CompilationUnit, global: Globa
     assignments.toList
   }
 
-  private def completeInitForConstructor(partialConstructor: PartialConstructor, identifier: NewIdentifier): Ast = {
+  private def completeInitForConstructor(partialConstructor: PartialConstructor, targetAst: Ast): Ast = {
     val initNode = partialConstructor.initNode
+    val args     = partialConstructor.initArgs
 
-    val objectNode = identifier.copy
-
-    val args = partialConstructor.initArgs
-
-    callAst(initNode, args.toList, Some(Ast(objectNode)), withRecvArgEdge = true)
+    callAst(initNode, args.toList, Some(targetAst), withRecvArgEdge = true)
   }
 
   def astsForVariableDecl(varDecl: VariableDeclarationExpr): Seq[Ast] = {
@@ -2155,6 +2197,34 @@ class AstCreator(filename: String, javaParserAst: CompilationUnit, global: Globa
     callAst(callNode, exprAst ++ Seq(typeAst))
   }
 
+  private def fieldAccessAst(
+    identifierName: String,
+    identifierType: String,
+    fieldIdentifierName: String,
+    returnType: Option[String],
+    lineNo: Option[Integer],
+    columnNo: Option[Integer]
+  ): Ast = {
+    val identifier       = identifierNode(identifierName, identifierType, lineNo, columnNo)
+    val maybeCorrespNode = scopeStack.lookupVariable(identifierName).map(_.node)
+
+    val fieldIdentifier = NewFieldIdentifier()
+      .code(fieldIdentifierName)
+      .canonicalName(fieldIdentifierName)
+      .lineNumber(lineNo)
+      .columnNumber(columnNo)
+
+    val fieldAccessCode = s"$identifierName.$fieldIdentifierName"
+    val fieldAccess =
+      operatorCallNode(Operators.fieldAccess, fieldAccessCode, returnType, lineNo, columnNo)
+
+    val identifierAst = Ast(identifier)
+    val fieldIdentAst = Ast(fieldIdentifier)
+
+    callAst(fieldAccess, Seq(identifierAst, fieldIdentAst))
+      .withRefEdges(identifier, maybeCorrespNode.toList)
+  }
+
   def astForNameExpr(nameExpr: NameExpr, expectedType: Option[ExpectedType]): Ast = {
     val name = nameExpr.getName.toString
     val typeFullName = expressionReturnTypeFullName(nameExpr)
@@ -2180,21 +2250,14 @@ class AstCreator(filename: String, javaParserAst: CompilationUnit, global: Globa
               typeInfoCalc.fullName(fieldDecl.declaringType())
           }
 
-        val identifier = identifierNode(identifierName, identifierTypeFullName, line(nameExpr), column(nameExpr))
-
-        val fieldIdentifier = NewFieldIdentifier()
-          .code(nameExpr.toString)
-          .canonicalName(name)
-          .lineNumber(line(nameExpr))
-          .columnNumber(column(nameExpr))
-
-        val fieldAccess =
-          operatorCallNode(Operators.fieldAccess, name, Some(typeFullName), line(nameExpr), column(nameExpr))
-
-        val identifierAst = Ast(identifier)
-        val fieldIdentAst = Ast(fieldIdentifier)
-
-        callAst(fieldAccess, Seq(identifierAst, fieldIdentAst))
+        fieldAccessAst(
+          identifierName,
+          identifierTypeFullName,
+          name,
+          Some(typeFullName),
+          line(nameExpr),
+          column(nameExpr)
+        )
 
       case _ =>
         val identifier = identifierNode(name, typeFullName, line(nameExpr), column(nameExpr))
diff --git a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/util/Scope.scala b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/util/Scope.scala
index 1a3894fb20a2..84c93fc005be 100644
--- a/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/util/Scope.scala
+++ b/joern-cli/frontends/javasrc2cpg/src/main/scala/io/joern/javasrc2cpg/util/Scope.scala
@@ -24,12 +24,14 @@ class Scope extends X2CpgScope[String, NodeTypeInfo, ScopeType] {
   private var typeDeclStack: List[NewTypeDecl]              = Nil
   private var lambdaMethods: List[mutable.ArrayBuffer[Ast]] = Nil
   private var lambdaDecls: List[mutable.ArrayBuffer[Ast]]   = Nil
+  private var memberInits: List[mutable.ArrayBuffer[Ast]]   = Nil
 
   override def pushNewScope(scope: ScopeType): Unit = {
     scope match {
       case TypeDeclScope(declNode) =>
         typeDeclStack = declNode :: typeDeclStack
         lambdaMethods = mutable.ArrayBuffer[Ast]() :: lambdaMethods
+        memberInits = mutable.ArrayBuffer[Ast]() :: memberInits
       case NamespaceScope =>
         lambdaDecls = mutable.ArrayBuffer[Ast]() :: lambdaMethods
       case _ => // Nothing to do in this case
@@ -52,6 +54,7 @@ class Scope extends X2CpgScope[String, NodeTypeInfo, ScopeType] {
       case Some(TypeDeclScope(typeDecl)) =>
         typeDeclStack = typeDeclStack.tail
         lambdaMethods = lambdaMethods.tail
+        memberInits = memberInits.tail
         Some(TypeDeclScope(typeDecl))
 
       case Some(NamespaceScope) =>
@@ -120,6 +123,22 @@ class Scope extends X2CpgScope[String, NodeTypeInfo, ScopeType] {
       )
   }
 
+  def addMemberInitializersToScope(inits: Seq[Ast]): Unit = {
+    memberInits match {
+      case currMembers :: _ => currMembers.addAll(inits)
+      case Nil              => logger.warn("Attempting to add static initializes without memberInits in scope.")
+    }
+  }
+
+  def getMemberInitializers: Seq[Ast] = {
+    memberInits match {
+      case currMembers :: _ => currMembers.toSeq
+      case Nil =>
+        logger.warn("Attemping to fetch initializers from scope with uninitialized memberInits. Inits may be missing.")
+        Seq.empty
+    }
+  }
+
   private def getWildcardType(identifier: String): Option[String] = {
     lookupVariableType(WildcardImportName) map { importName =>
       s"$importName.$identifier"


Test patch:
diff --git a/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/AnnotationTests.scala b/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/AnnotationTests.scala
index ca82625b92ff..74baa88a3cd1 100644
--- a/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/AnnotationTests.scala
+++ b/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/AnnotationTests.scala
@@ -120,7 +120,7 @@ class AnnotationTests extends JavaSrcCode2CpgFixture {
 
     "test annotation node properties" in {
       import scala.jdk.CollectionConverters._
-      val annotationNode = cpg.method.nameExact("SomeClass").annotation.head
+      val annotationNode = cpg.method.nameExact("<init>").annotation.head
       annotationNode.code shouldBe "@MarkerAnnotation()"
       annotationNode.name shouldBe "MarkerAnnotation"
       annotationNode.fullName shouldBe "some.MarkerAnnotation"
diff --git a/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/CallTests.scala b/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/CallTests.scala
index 331dcb444768..027148d9e7bb 100644
--- a/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/CallTests.scala
+++ b/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/CallTests.scala
@@ -435,7 +435,7 @@ class CallTests extends JavaSrcCodeToCpgFixture {
 
     argument.name shouldBe Operators.fieldAccess
     argument.typeFullName shouldBe "test.MyObject"
-    argument.code shouldBe "obj"
+    argument.code shouldBe "this.obj"
     argument.order shouldBe 2
     argument.argumentIndex shouldBe 1
 
diff --git a/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/ConstructorInvocationTests.scala b/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/ConstructorInvocationTests.scala
index 72b77d4d8d7b..ddccf0746a58 100644
--- a/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/ConstructorInvocationTests.scala
+++ b/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/ConstructorInvocationTests.scala
@@ -36,7 +36,7 @@ class NewConstructorInvocationTests extends JavaSrcCode2CpgFixture {
         |}
         |""".stripMargin)
     "create the correct Ast for the constructor" in {
-      fooCpg.method.name("Foo").l match {
+      fooCpg.typeDecl.name("Foo").method.nameExact("<init>").l match {
         case List(cons: Method) =>
           cons.fullName shouldBe "Foo.<init>:void(int)"
           cons.signature shouldBe "void(int)"
@@ -107,7 +107,7 @@ class ConstructorInvocationTests extends JavaSrcCodeToCpgFixture {
 
   "it should create correct method nodes for constructors" in {
 
-    cpg.method.name("Bar").l match {
+    cpg.typeDecl.name("Bar").method.nameExact("<init>").l match {
       case List(cons1: Method, cons2: Method) =>
         cons1.fullName shouldBe "Bar.<init>:void(int)"
         cons1.signature shouldBe "void(int)"
diff --git a/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/MemberTests.scala b/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/MemberTests.scala
index 85720845780b..a6ba8c6de11b 100644
--- a/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/MemberTests.scala
+++ b/joern-cli/frontends/javasrc2cpg/src/test/scala/io/joern/javasrc2cpg/querying/MemberTests.scala
@@ -1,11 +1,242 @@
 package io.joern.javasrc2cpg.querying
 
-import io.joern.javasrc2cpg.testfixtures.JavaSrcCodeToCpgFixture
+import io.joern.javasrc2cpg.testfixtures.{JavaSrcCode2CpgFixture, JavaSrcCodeToCpgFixture}
 import io.shiftleft.codepropertygraph.generated.{DispatchTypes, ModifierTypes, Operators, PropertyNames}
-import io.shiftleft.codepropertygraph.generated.nodes.{Call, FieldIdentifier, Identifier, Member}
+import io.shiftleft.codepropertygraph.generated.nodes.{Call, FieldIdentifier, Identifier, Literal, Member}
 import io.shiftleft.semanticcpg.language._
 import org.scalatest.Ignore
 
+class NewMemberTests extends JavaSrcCode2CpgFixture {
+  "non-static member initializers" should {
+    "be added to the default constructor in classes with no constructor" in {
+      val cpg = code("""
+			 |class Foo {
+             |    int x = 1;
+             |}""".stripMargin)
+
+      val constructor = cpg.method.nameExact("<init>").l match {
+        case constructor :: Nil => constructor
+        case result             => fail(s"Expected single constructor method but found $result")
+      }
+
+      constructor.fullName shouldBe "Foo.<init>:void()"
+      val xAssign = constructor.body.astChildren.l match {
+        case List(xAssign: Call) => xAssign
+        case result              => fail(s"Expected xAssign in constructor but found $result")
+      }
+
+      xAssign.name shouldBe Operators.assignment
+      xAssign.methodFullName shouldBe Operators.assignment
+
+      xAssign.argument.l match {
+        case List(fieldAccess: Call, value: Literal) =>
+          fieldAccess.name shouldBe Operators.fieldAccess
+          fieldAccess.argument.l match {
+            case List(identifier: Identifier, fieldIdentifier: FieldIdentifier) =>
+              identifier.name shouldBe "this"
+              identifier.typeFullName shouldBe "Foo"
+              fieldIdentifier.canonicalName shouldBe "x"
+
+            case result => fail(s"Expected field identifier args but got $result")
+          }
+
+          value.code shouldBe "1"
+          value.typeFullName shouldBe "int"
+          value.argumentIndex shouldBe 2
+
+        case result => fail(s"Expected field assign args but got $result")
+      }
+    }
+
+    "be added to an empty constructor" in {
+      val cpg = code("""
+             |class Foo {
+             |    int x = 1;
+             |    public Foo() {}
+             |}""".stripMargin)
+      val constructor = cpg.method.nameExact("<init>").l match {
+        case constructor :: Nil => constructor
+        case result             => fail(s"Expected single constructor method but found $result")
+      }
+
+      constructor.fullName shouldBe "Foo.<init>:void()"
+      val xAssign = constructor.body.astChildren.l match {
+        case List(xAssign: Call) => xAssign
+        case result              => fail(s"Expected xAssign in constructor but found $result")
+      }
+
+      xAssign.name shouldBe Operators.assignment
+      xAssign.methodFullName shouldBe Operators.assignment
+
+      xAssign.argument.l match {
+        case List(fieldAccess: Call, value: Literal) =>
+          fieldAccess.name shouldBe Operators.fieldAccess
+          fieldAccess.argument.l match {
+            case List(identifier: Identifier, fieldIdentifier: FieldIdentifier) =>
+              identifier.name shouldBe "this"
+              identifier.typeFullName shouldBe "Foo"
+              fieldIdentifier.canonicalName shouldBe "x"
+
+            case result => fail(s"Expected field identifier args but got $result")
+          }
+
+          value.code shouldBe "1"
+          value.typeFullName shouldBe "int"
+          value.argumentIndex shouldBe 2
+
+        case result => fail(s"Expected field assign args but got $result")
+      }
+    }
+
+    "be added to a not-empty constructor without an explicit constructor invocation" in {
+      val cpg = code("""
+			 |class Foo {
+			 |    int x = 1;
+			 |    int y;
+			 |
+			 |    public Foo(int y) {
+			 |      this.y = y;
+			 |    }
+			 |}""".stripMargin)
+
+      val constructor = cpg.method.nameExact("<init>").l match {
+        case constructor :: Nil => constructor
+        case result             => fail(s"Expected single constructor method but found $result")
+      }
+
+      constructor.fullName shouldBe "Foo.<init>:void(int)"
+      val (xAssign, yAssign) = constructor.body.astChildren.l match {
+        case List(xAssign: Call, yAssign: Call) => (xAssign, yAssign)
+        case result                             => fail(s"Expected two assigns in constructor but found $result")
+      }
+
+      xAssign.name shouldBe Operators.assignment
+      xAssign.methodFullName shouldBe Operators.assignment
+
+      xAssign.argument.l match {
+        case List(fieldAccess: Call, value: Literal) =>
+          fieldAccess.name shouldBe Operators.fieldAccess
+          fieldAccess.argument.l match {
+            case List(identifier: Identifier, fieldIdentifier: FieldIdentifier) =>
+              identifier.name shouldBe "this"
+              identifier.typeFullName shouldBe "Foo"
+              fieldIdentifier.canonicalName shouldBe "x"
+
+            case result => fail(s"Expected field identifier args but got $result")
+          }
+
+          value.code shouldBe "1"
+          value.typeFullName shouldBe "int"
+          value.argumentIndex shouldBe 2
+
+        case result => fail(s"Expected field assign args but got $result")
+      }
+
+      yAssign.name shouldBe Operators.assignment
+      yAssign.methodFullName shouldBe Operators.assignment
+
+      yAssign.argument.l match {
+        case List(fieldAccess: Call, value: Identifier) =>
+          fieldAccess.name shouldBe Operators.fieldAccess
+          fieldAccess.argument.l match {
+            case List(identifier: Identifier, fieldIdentifier: FieldIdentifier) =>
+              identifier.name shouldBe "this"
+              identifier.typeFullName shouldBe "Foo"
+              fieldIdentifier.canonicalName shouldBe "y"
+
+            case result => fail(s"Expected field identifier args but got $result")
+          }
+
+          value.name shouldBe "y"
+          value.code shouldBe "y"
+          value.typeFullName shouldBe "int"
+          value.argumentIndex shouldBe 2
+
+        case result => fail(s"Expected field assign args but got $result")
+      }
+    }
+
+    "not be added to a constructor containing an explicit constructor invocation" in {
+      val cpg = code("""
+			 |class Foo {
+			 |    int x = 1;
+			 |    int y;
+			 |
+			 |    public Foo(int y) {
+			 |        this.y = y;
+			 |    }
+			 |
+			 |    public Foo() {
+			 |        this(42);
+			 |    }
+			 |}""".stripMargin)
+
+      val constructorWithParam = cpg.method.fullNameExact("Foo.<init>:void(int)").l match {
+        case constructor :: Nil => constructor
+        case result             => fail(s"Expected single constructor method but found $result")
+      }
+
+      val (xAssign, yAssign) = constructorWithParam.body.astChildren.l match {
+        case List(xAssign: Call, yAssign: Call) => (xAssign, yAssign)
+        case result                             => fail(s"Expected two assigns in constructor but found $result")
+      }
+
+      xAssign.name shouldBe Operators.assignment
+      xAssign.methodFullName shouldBe Operators.assignment
+
+      xAssign.argument.l match {
+        case List(fieldAccess: Call, value: Literal) =>
+          fieldAccess.name shouldBe Operators.fieldAccess
+          fieldAccess.argument.l match {
+            case List(identifier: Identifier, fieldIdentifier: FieldIdentifier) =>
+              identifier.name shouldBe "this"
+              identifier.typeFullName shouldBe "Foo"
+              fieldIdentifier.canonicalName shouldBe "x"
+
+            case result => fail(s"Expected field identifier args but got $result")
+          }
+
+          value.code shouldBe "1"
+          value.typeFullName shouldBe "int"
+          value.argumentIndex shouldBe 2
+
+        case result => fail(s"Expected field assign args but got $result")
+      }
+
+      yAssign.name shouldBe Operators.assignment
+      yAssign.methodFullName shouldBe Operators.assignment
+
+      yAssign.argument.l match {
+        case List(fieldAccess: Call, value: Identifier) =>
+          fieldAccess.name shouldBe Operators.fieldAccess
+          fieldAccess.argument.l match {
+            case List(identifier: Identifier, fieldIdentifier: FieldIdentifier) =>
+              identifier.name shouldBe "this"
+              identifier.typeFullName shouldBe "Foo"
+              fieldIdentifier.canonicalName shouldBe "y"
+
+            case result => fail(s"Expected field identifier args but got $result")
+          }
+
+          value.name shouldBe "y"
+          value.code shouldBe "y"
+          value.typeFullName shouldBe "int"
+          value.argumentIndex shouldBe 2
+
+        case result => fail(s"Expected field assign args but got $result")
+      }
+
+      val ctorWithExplInvocation = cpg.method.fullNameExact("Foo.<init>:void()")
+      ctorWithExplInvocation.body.astChildren.l match {
+        case List(explConsInvocation: Call) =>
+          explConsInvocation.methodFullName shouldBe "Foo.<init>:void(int)"
+
+        case result => fail(s"Expected single explicit constructor invocation call but found $result")
+      }
+    }
+  }
+}
+
 class MemberTests extends JavaSrcCodeToCpgFixture {
 
   override val code: String =