Repository: joernio/joern (scala)

Issue:
[Bug] pysrc2cpg: Symbols in Member Expressions Become Members
**Describe the bug**
There is a strange phenomenon where certain symbols that form arguments in an expression at a member declaration become members to the types themselves. An example of where valid Python triggers this:

```python
import models

class SocialApp():
    member1 = "1"

class SocialAccount():
    member2 = "2"

class SocialToken(models.Model):
    app = models.ForeignKey(SocialApp, on_delete=models.CASCADE)
    account = models.ForeignKey(SocialAccount, on_delete=models.CASCADE)
```

Running `cpg.typeDecl("SocialToken<meta>").member.name.foreach(println)` returns:
```
SocialApp
models
app
SocialAccount
account
<fakeNew>
```

Only `app` and `account` should be members here, but all of these symbols (`models`, `SocialApp`, etc.) run through `PythonAstVisitor.convert(name: ast.Name): nodes.NewNode` where the AstParent is a `NewMethod`, (in this case, `SocialToken<body>`) which triggers the matcher to send this `ast.Name` to    `createAndRegisterMember(identifier.name, lineAndColOf(name))`.

One can even replace `app = models.ForeignKey(SocialApp, on_delete=models.CASCADE)` with `app = models.ForeignKey(BlahBlah, on_delete=models.CASCADE)` and see `BlahBlah` become a member.

I will be investigating a fix for this. Originally brought to my attention by @dbMundada.

cc: @ml86 

**To Reproduce**
Steps to reproduce the behavior:
1. Run the above code using `pysrc2cpg` frontend
2. The query `cpg.typeDecl("SocialToken<meta>").member.name.l` should present the issue

**Expected behavior**
Only `appOnly `app` and `account` should be members here.

**Desktop (please complete the following information):**
 - OS MacOS Ventura
 - Joern Version v1.1.1583
 - Java version 18

Pull request:
* Added `toString` overrides to make memory operations easier to debug
* Added the condition that only identifiers under classes with `STORE` operations become members

Resolves #2498

Patch:
diff --git a/joern-cli/frontends/pysrc2cpg/src/main/scala/io/joern/pysrc2cpg/PythonAstVisitor.scala b/joern-cli/frontends/pysrc2cpg/src/main/scala/io/joern/pysrc2cpg/PythonAstVisitor.scala
index da761ac72d97..ece4567b915f 100644
--- a/joern-cli/frontends/pysrc2cpg/src/main/scala/io/joern/pysrc2cpg/PythonAstVisitor.scala
+++ b/joern-cli/frontends/pysrc2cpg/src/main/scala/io/joern/pysrc2cpg/PythonAstVisitor.scala
@@ -1834,7 +1834,7 @@ class PythonAstVisitor(
     val memoryOperation = memOpMap.get(name).get
     val identifier      = createIdentifierNode(name.id, memoryOperation, lineAndColOf(name))
     contextStack.astParent match {
-      case method: NewMethod if method.name.endsWith("<body>") =>
+      case method: NewMethod if method.name.endsWith("<body>") && memoryOperation == Store =>
         createAndRegisterMember(identifier.name, lineAndColOf(name))
       case _ =>
     }
diff --git a/joern-cli/frontends/pysrc2cpg/src/main/scala/io/joern/pysrc2cpg/memop/AstNodeToMemoryOperationMap.scala b/joern-cli/frontends/pysrc2cpg/src/main/scala/io/joern/pysrc2cpg/memop/AstNodeToMemoryOperationMap.scala
index bdf3b96d4253..41852f8dc53e 100644
--- a/joern-cli/frontends/pysrc2cpg/src/main/scala/io/joern/pysrc2cpg/memop/AstNodeToMemoryOperationMap.scala
+++ b/joern-cli/frontends/pysrc2cpg/src/main/scala/io/joern/pysrc2cpg/memop/AstNodeToMemoryOperationMap.scala
@@ -18,6 +18,8 @@ class AstNodeToMemoryOperationMap {
     override def hashCode(): Int = {
       System.identityHashCode(astNode)
     }
+
+    override def toString: String = astNode.toString
   }
 
   private val astNodeToMemOp = mutable.HashMap.empty[IdentityHashWrapper, MemoryOperation]
diff --git a/joern-cli/frontends/pysrc2cpg/src/main/scala/io/joern/pysrc2cpg/memop/MemoryOperation.scala b/joern-cli/frontends/pysrc2cpg/src/main/scala/io/joern/pysrc2cpg/memop/MemoryOperation.scala
index 79ecc9deda54..4196c34bd59c 100644
--- a/joern-cli/frontends/pysrc2cpg/src/main/scala/io/joern/pysrc2cpg/memop/MemoryOperation.scala
+++ b/joern-cli/frontends/pysrc2cpg/src/main/scala/io/joern/pysrc2cpg/memop/MemoryOperation.scala
@@ -1,6 +1,8 @@
 package io.joern.pysrc2cpg.memop
 
-sealed trait MemoryOperation
+sealed trait MemoryOperation {
+  override def toString: String = getClass.getSimpleName
+}
 object Store extends MemoryOperation
 object Load  extends MemoryOperation
 object Del   extends MemoryOperation


Test patch:
diff --git a/joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/cpg/MemberCpgTests.scala b/joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/cpg/MemberCpgTests.scala
index 6862e131ac2b..5a9e34ea580c 100644
--- a/joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/cpg/MemberCpgTests.scala
+++ b/joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/cpg/MemberCpgTests.scala
@@ -2,9 +2,9 @@ package io.joern.pysrc2cpg.cpg
 
 import io.joern.pysrc2cpg.Py2CpgTestContext
 import io.shiftleft.codepropertygraph.generated.nodes.Member
+import io.shiftleft.semanticcpg.language._
 import org.scalatest.freespec.AnyFreeSpec
 import org.scalatest.matchers.should.Matchers
-import io.shiftleft.semanticcpg.language._
 
 class MemberCpgTests extends AnyFreeSpec with Matchers {
 
@@ -77,4 +77,24 @@ class MemberCpgTests extends AnyFreeSpec with Matchers {
 
   }
 
+  "A class variable instantiated by some expression" - {
+
+    lazy val cpg = Py2CpgTestContext.buildCpg("""import models
+        |
+        |class SocialApp():
+        |    member1 = "1"
+        |
+        |class SocialAccount():
+        |    member2 = "2"
+        |
+        |class SocialToken(models.Model):
+        |    app = models.ForeignKey(SocialApp, on_delete=models.CASCADE)
+        |    account = models.ForeignKey(SocialAccount, on_delete=models.CASCADE)
+        |""".stripMargin)
+
+    "should only render the LHS of the expression as the member and not the RHS" in {
+      cpg.typeDecl("SocialToken<meta>").member.name.l shouldBe List("app", "account", "<fakeNew>")
+    }
+  }
+
 }