From 339a0c715c751d66bbb284a65c2e9754985b8e3e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Max=20Leuth=C3=A4user?=
 <1417198+max-leuthaeuser@users.noreply.github.com>
Date: Mon, 19 Feb 2024 13:27:52 +0100
Subject: [PATCH] [jssrc2cpg] Enable file content and content for method and
 type decl (#4186)

Fixes: https://github.com/joernio/joern/issues/4185
---
 .../jssrc2cpg/astcreation/AstCreator.scala    | 19 ++++-
 .../astcreation/AstCreatorHelper.scala        | 16 ++--
 .../io/CodeDumperFromContentTest.scala        | 78 +++++++++++++++++++
 ...est.scala => CodeDumperFromFileTest.scala} |  2 +-
 .../scala/io/joern/x2cpg/AstNodeBuilder.scala |  6 +-
 5 files changed, 112 insertions(+), 9 deletions(-)
 create mode 100644 joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/io/CodeDumperFromContentTest.scala
 rename joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/io/{CodeDumperTest.scala => CodeDumperFromFileTest.scala} (97%)

diff --git a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstCreator.scala b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstCreator.scala
index 005307d290e1..82b04979505d 100644
--- a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstCreator.scala
+++ b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstCreator.scala
@@ -62,7 +62,9 @@ class AstCreator(val config: Config, val global: Global, val parserResult: Parse
     positionLookupTables(parserResult.fileContent)
 
   override def createAst(): DiffGraphBuilder = {
-    val fileNode       = NewFile().name(parserResult.filename).order(1)
+    val fileContent = if (!config.disableFileContent) Option(parserResult.fileContent) else None
+    val fileNode    = NewFile().name(parserResult.filename).order(1)
+    fileContent.foreach(fileNode.content(_))
     val namespaceBlock = globalNamespaceBlock()
     methodAstParentStack.push(namespaceBlock)
     val ast = Ast(fileNode).withChild(Ast(namespaceBlock).withChild(createProgramMethod()))
@@ -257,4 +259,19 @@ class AstCreator(val config: Config, val global: Global, val parserResult: Parse
   protected def lineEnd(node: BabelNodeInfo): Option[Integer]   = node.lineNumberEnd
   protected def columnEnd(node: BabelNodeInfo): Option[Integer] = node.columnNumberEnd
   protected def code(node: BabelNodeInfo): String               = node.code
+
+  protected def nodeOffsets(node: Value): Option[(Int, Int)] = {
+    for {
+      startOffset <- start(node)
+      endOffset   <- end(node)
+    } yield (startOffset, endOffset)
+  }
+
+  override protected def offset(node: BabelNodeInfo): Option[(Int, Int)] = {
+    Option
+      .when(!config.disableFileContent) {
+        nodeOffsets(node.json)
+      }
+      .flatten
+  }
 }
diff --git a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstCreatorHelper.scala b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstCreatorHelper.scala
index c512e98a2dc3..aa33ef29256f 100644
--- a/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstCreatorHelper.scala
+++ b/joern-cli/frontends/jssrc2cpg/src/main/scala/io/joern/jssrc2cpg/astcreation/AstCreatorHelper.scala
@@ -8,9 +8,10 @@ import io.joern.x2cpg.utils.NodeBuilders.{newClosureBindingNode, newLocalNode}
 import io.joern.x2cpg.{Ast, ValidationMode}
 import io.shiftleft.codepropertygraph.generated.nodes.*
 import io.shiftleft.codepropertygraph.generated.{EdgeTypes, EvaluationStrategies}
+import io.shiftleft.codepropertygraph.generated.nodes.File.PropertyDefaults
 import ujson.Value
 
-import scala.collection.{SortedMap, mutable}
+import scala.collection.{mutable, SortedMap}
 import scala.util.{Success, Try}
 
 trait AstCreatorHelper(implicit withSchemaValidation: ValidationMode) { this: AstCreator =>
@@ -70,9 +71,12 @@ trait AstCreatorHelper(implicit withSchemaValidation: ValidationMode) { this: As
   }
 
   protected def code(node: Value): String = {
-    val startIndex = start(node).getOrElse(0)
-    val endIndex   = Math.min(end(node).getOrElse(0), parserResult.fileContent.length)
-    shortenCode(parserResult.fileContent.substring(startIndex, endIndex).trim)
+    nodeOffsets(node) match {
+      case Some((startOffset, endOffset))
+          if startOffset < endOffset && startOffset >= 0 && endOffset <= parserResult.fileContent.length =>
+        shortenCode(parserResult.fileContent.substring(startOffset, endOffset).trim)
+      case _ => PropertyDefaults.Code
+    }
   }
 
   protected def hasKey(node: Value, key: String): Boolean = Try(node(key)).isSuccess
@@ -90,9 +94,9 @@ trait AstCreatorHelper(implicit withSchemaValidation: ValidationMode) { this: As
     case _                                => None
   }
 
-  private def start(node: Value): Option[Int] = Try(node("start").num.toInt).toOption
+  protected def start(node: Value): Option[Int] = Try(node("start").num.toInt).toOption
 
-  private def end(node: Value): Option[Int] = Try(node("end").num.toInt).toOption
+  protected def end(node: Value): Option[Int] = Try(node("end").num.toInt).toOption
 
   protected def pos(node: Value): Option[Int] = Try(node("start").num.toInt).toOption
 
diff --git a/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/io/CodeDumperFromContentTest.scala b/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/io/CodeDumperFromContentTest.scala
new file mode 100644
index 000000000000..dd9f484474ac
--- /dev/null
+++ b/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/io/CodeDumperFromContentTest.scala
@@ -0,0 +1,78 @@
+package io.joern.jssrc2cpg.io
+
+import io.joern.jssrc2cpg.testfixtures.JsSrc2CpgSuite
+import io.joern.jssrc2cpg.Config
+import io.shiftleft.semanticcpg.language.*
+
+class CodeDumperFromContentTest extends JsSrc2CpgSuite {
+
+  private implicit val finder: NodeExtensionFinder = DefaultNodeExtensionFinder
+
+  "dumping code from content" should {
+    val cpg = code(
+      """
+     |// A comment
+     |function my_func(param1)
+     |{
+     |   var x = foo(param1);
+     |}""".stripMargin,
+      "index.js"
+    ).withConfig(Config().withDisableFileContent(false))
+
+    "allow one to dump a method node's source code from `File.contents`" in {
+      inside(cpg.method.nameExact("my_func").dumpRaw.l) {
+        case content :: Nil =>
+          content.linesIterator.map(_.strip).l shouldBe List(
+            "function my_func(param1) /* <=== index.js::program:my_func */",
+            "{",
+            "var x = foo(param1);",
+            "}"
+          )
+        case content => fail(s"Expected exactly 1 content dump, but got: $content")
+      }
+    }
+  }
+
+  "code from method content" should {
+    val myFuncContent =
+      """function my_func(param1)
+        |{
+        |  var x = foo(param1);
+        |}""".stripMargin
+
+    val cpg = code(
+      s"""
+        |// A comment
+        |$myFuncContent
+        |""".stripMargin,
+      "index.js"
+    ).withConfig(Config().withDisableFileContent(false))
+
+    "allow one to dump a method node's source code from `Method.content`" in {
+      val List(content) = cpg.method.nameExact("my_func").content.l
+      content shouldBe myFuncContent
+    }
+  }
+
+  "code from typedecl content" should {
+    val myClassContent =
+      """class Foo
+        |{
+        |  x = 'foo';
+        |}""".stripMargin
+
+    val cpg = code(
+      s"""
+         |// A comment
+         |$myClassContent
+         |""".stripMargin,
+      "index.js"
+    ).withConfig(Config().withDisableFileContent(false))
+
+    "allow one to dump a method node's source code from `TypeDecl.content`" in {
+      val List(content) = cpg.typeDecl.nameExact("Foo").content.l
+      content shouldBe myClassContent
+    }
+  }
+
+}
diff --git a/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/io/CodeDumperTest.scala b/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/io/CodeDumperFromFileTest.scala
similarity index 97%
rename from joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/io/CodeDumperTest.scala
rename to joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/io/CodeDumperFromFileTest.scala
index 8c82e9458385..3e0865672ae9 100644
--- a/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/io/CodeDumperTest.scala
+++ b/joern-cli/frontends/jssrc2cpg/src/test/scala/io/joern/jssrc2cpg/io/CodeDumperFromFileTest.scala
@@ -7,7 +7,7 @@ import io.shiftleft.semanticcpg.language._
 
 import java.util.regex.Pattern
 
-class CodeDumperTest extends JsSrc2CpgSuite {
+class CodeDumperFromFileTest extends JsSrc2CpgSuite {
 
   implicit val finder: NodeExtensionFinder = DefaultNodeExtensionFinder
 
diff --git a/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/AstNodeBuilder.scala b/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/AstNodeBuilder.scala
index c37e4ab90742..109b9095d914 100644
--- a/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/AstNodeBuilder.scala
+++ b/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/AstNodeBuilder.scala
@@ -142,7 +142,7 @@ trait AstNodeBuilder[Node, NodeProcessor] { this: NodeProcessor =>
     inherits: Seq[String] = Seq.empty,
     alias: Option[String] = None
   ): NewTypeDecl = {
-    NewTypeDecl()
+    val node_ = NewTypeDecl()
       .name(name)
       .fullName(fullName)
       .code(code)
@@ -154,6 +154,10 @@ trait AstNodeBuilder[Node, NodeProcessor] { this: NodeProcessor =>
       .aliasTypeFullName(alias)
       .lineNumber(line(node))
       .columnNumber(column(node))
+    offset(node).foreach { case (offset, offsetEnd) =>
+      node_.offset(offset).offsetEnd(offsetEnd)
+    }
+    node_
   }
 
   protected def parameterInNode(