Repository: joernio/joern (scala)

Issue:
Add locations to dot-graphs
When the graphs are exported (eg. PDG) it does not include the line number information. Is it possible to add line numbers to the exported graphs so when there are parsed we can easily map dependencies back to corresponding lines?

Pull request description:
- Switching from quoted labels to HTML labels on nodes
- Added line number info as subscript if present

Resolves #381 

Example:
```java
class Foo {
 int foo(int x, int y) {
  if (y < 10)
    return -1;
  if (x < 10) {
   sink(x);
  }
  System.out.println("foo");
  return 0;
 }
}
```
![graphviz-5](https://user-images.githubusercontent.com/28294550/166698615-28a7dea6-8217-47a8-8d70-8be913d668a4.png)

Patch:
diff --git a/semanticcpg/src/main/scala/io/shiftleft/semanticcpg/dotgenerator/DotSerializer.scala b/semanticcpg/src/main/scala/io/shiftleft/semanticcpg/dotgenerator/DotSerializer.scala
index d56be72469df..c5035ed24f8c 100644
--- a/semanticcpg/src/main/scala/io/shiftleft/semanticcpg/dotgenerator/DotSerializer.scala
+++ b/semanticcpg/src/main/scala/io/shiftleft/semanticcpg/dotgenerator/DotSerializer.scala
@@ -1,10 +1,13 @@
 package io.shiftleft.semanticcpg.dotgenerator
 
+import io.shiftleft.codepropertygraph.generated.PropertyNames
 import io.shiftleft.codepropertygraph.generated.nodes._
 import io.shiftleft.semanticcpg.language._
 import io.shiftleft.semanticcpg.utils.MemberAccess
 
+import java.util.Optional
 import scala.collection.immutable.HashMap
+import scala.language.postfixOps
 
 object DotSerializer {
 
@@ -57,6 +60,7 @@ object DotSerializer {
   }
 
   private def stringRepr(vertex: StoredNode): String = {
+    val maybeLineNo: Optional[AnyRef] = vertex.propertyOption(PropertyNames.LINE_NUMBER)
     escape(vertex match {
       case call: Call                            => (call.name, call.code).toString
       case expr: Expression                      => (expr.label, expr.code, toCfgNode(expr).code).toString
@@ -70,7 +74,7 @@ object DotSerializer {
       case annoParam: AnnotationParameter        => (annoParam.label, annoParam.code).toString()
       case typ: Type                             => (typ.label, typ.name).toString()
       case _                                     => ""
-    })
+    }) + (if (maybeLineNo.isPresent) s"<SUB>${maybeLineNo.get()}</SUB>" else "")
   }
 
   private def toCfgNode(node: StoredNode): CfgNode = {
@@ -89,7 +93,7 @@ object DotSerializer {
   }
 
   private def nodeToDot(node: StoredNode): String = {
-    s""""${node.id}" [label = "${stringRepr(node)}" ]""".stripMargin
+    s""""${node.id}" [label = <${stringRepr(node)}> ]""".stripMargin
   }
 
   private def edgeToDot(edge: Edge, withEdgeTypes: Boolean): String = {
@@ -112,15 +116,15 @@ object DotSerializer {
        |""".stripMargin
   }
 
+  /** Escapes common characters that do not conform to HTML character sets.
+    * @see
+    *   https://www.w3.org/TR/html4/sgml/entities.html
+    */
   private def escapedChar(ch: Char): String = ch match {
-    case '\b' => "\\b"
-    case '\t' => "\\t"
-    case '\n' => "\\n"
-    case '\f' => "\\f"
-    case '\r' => "\\r"
-    case '"'  => "\\\""
-    case '\'' => "\\\'"
-    case '\\' => "\\\\"
+    case '"' => "&quot;"
+    case '<' => "&lt;"
+    case '>' => "&gt;"
+    case '&' => "&amp;"
     case _ =>
       if (ch.isControl) "\\0" + Integer.toOctalString(ch.toInt)
       else String.valueOf(ch)


Test patch:
diff --git a/joern-cli/frontends/c2cpg/src/test/scala/io/joern/c2cpg/dotgenerator/DotAstGeneratorTests.scala b/joern-cli/frontends/c2cpg/src/test/scala/io/joern/c2cpg/dotgenerator/DotAstGeneratorTests.scala
index f44c12c43574..73dcdf19c5e8 100644
--- a/joern-cli/frontends/c2cpg/src/test/scala/io/joern/c2cpg/dotgenerator/DotAstGeneratorTests.scala
+++ b/joern-cli/frontends/c2cpg/src/test/scala/io/joern/c2cpg/dotgenerator/DotAstGeneratorTests.scala
@@ -33,7 +33,7 @@ class DotAstGeneratorTests extends CCodeToCpgSuite {
       inside(cpg.method.name("my_func").dotAst.l) { case List(x) =>
         x should (
           startWith("digraph \"my_func\"") and
-            include("""[label = "(CONTROL_STRUCTURE,if (y > 42),if (y > 42))" ]""") and
+            include("""[label = <(CONTROL_STRUCTURE,if (y &gt; 42),if (y &gt; 42))<SUB>5</SUB>> ]""") and
             endWith("}\n")
         )
       }
@@ -53,7 +53,7 @@ class DotAstGeneratorTests extends CCodeToCpgSuite {
 
     "allow plotting sub trees of methods" in {
       inside(cpg.method.ast.isControlStructure.code(".*y > 42.*").dotAst.l) { case List(x, _) =>
-        x should (include("y > 42") and include("IDENTIFIER,y") and not include "x * 2")
+        x should (include("y &gt; 42") and include("IDENTIFIER,y") and not include "x * 2")
       }
     }
 
@@ -61,8 +61,10 @@ class DotAstGeneratorTests extends CCodeToCpgSuite {
       inside(cpg.method.name("lemon").dotAst.l) { case List(x) =>
         x should (
           startWith("digraph \"lemon\"") and
-            include("""[label = "(goog,goog(\"\\\"yes\\\"\"))" ]""") and
-            include("""[label = "(LITERAL,\"\\\"yes\\\"\",goog(\"\\\"yes\\\"\"))" ]""") and
+            include("""[label = <(goog,goog(&quot;\&quot;yes\&quot;&quot;))<SUB>18</SUB>> ]""") and
+            include(
+              """[label = <(LITERAL,&quot;\&quot;yes\&quot;&quot;,goog(&quot;\&quot;yes\&quot;&quot;))<SUB>18</SUB>> ]"""
+            ) and
             endWith("}\n")
         )
       }
diff --git a/joern-cli/frontends/c2cpg/src/test/scala/io/joern/c2cpg/dotgenerator/DotCdgGeneratorTests.scala b/joern-cli/frontends/c2cpg/src/test/scala/io/joern/c2cpg/dotgenerator/DotCdgGeneratorTests.scala
index 2a14eaa793c1..f831c3cb7f90 100644
--- a/joern-cli/frontends/c2cpg/src/test/scala/io/joern/c2cpg/dotgenerator/DotCdgGeneratorTests.scala
+++ b/joern-cli/frontends/c2cpg/src/test/scala/io/joern/c2cpg/dotgenerator/DotCdgGeneratorTests.scala
@@ -20,9 +20,9 @@ class DotCdgGeneratorTest1 extends DataFlowCodeToCpgSuite {
       inside(cpg.method.name("foo").dotCdg.l) { case List(x) =>
         x should (
           startWith("digraph \"foo\"") and
-            include("""[label = "(<operator>.greaterThan,x > 8)" ]""") and
-            include("""[label = "(<operator>.assignment,z = a(x))" ]""") and
-            include("""[label = "(a,a(x))" ]""") and
+            include("""[label = <(&lt;operator&gt;.greaterThan,x &gt; 8)<SUB>3</SUB>> ]""") and
+            include("""[label = <(&lt;operator&gt;.assignment,z = a(x))<SUB>4</SUB>> ]""") and
+            include("""[label = <(a,a(x))<SUB>4</SUB>> ]""") and
             endWith("}\n")
         )
         val lines = x.split("\n")
@@ -50,9 +50,9 @@ class DotCdgGeneratorTest2 extends DataFlowCodeToCpgSuite {
       inside(cpg.method.name("foo").dotCdg.l) { case List(x) =>
         x should (
           startWith("digraph \"foo\"") and
-            include("""[label = "(<operator>.greaterThan,x > 8)" ]""") and
-            include("""[label = "(<operator>.assignment,z = a(x))" ]""") and
-            include("""[label = "(a,a(x))" ]""") and
+            include("""[label = <(&lt;operator&gt;.greaterThan,x &gt; 8)<SUB>3</SUB>> ]""") and
+            include("""[label = <(&lt;operator&gt;.assignment,z = a(x))<SUB>4</SUB>> ]""") and
+            include("""[label = <(a,a(x))<SUB>4</SUB>> ]""") and
             endWith("}\n")
         )
         val lines = x.split("\n")
diff --git a/joern-cli/frontends/c2cpg/src/test/scala/io/joern/c2cpg/dotgenerator/DotCfgGeneratorTests.scala b/joern-cli/frontends/c2cpg/src/test/scala/io/joern/c2cpg/dotgenerator/DotCfgGeneratorTests.scala
index f71214a83335..f3cb275f27b9 100644
--- a/joern-cli/frontends/c2cpg/src/test/scala/io/joern/c2cpg/dotgenerator/DotCfgGeneratorTests.scala
+++ b/joern-cli/frontends/c2cpg/src/test/scala/io/joern/c2cpg/dotgenerator/DotCfgGeneratorTests.scala
@@ -23,7 +23,7 @@ class DotCfgGeneratorTest1 extends CCodeToCpgSuite {
       inside(cpg.method.name("main").dotCfg.l) { case List(dotStr) =>
         dotStr should (
           startWith("digraph \"main\" {") and
-            include("(<operator>.assignment,i = 0)") and
+            include("(&lt;operator&gt;.assignment,i = 0)") and
             endWith("}\n")
         )
       }
diff --git a/joern-cli/frontends/fuzzyc2cpg/src/test/scala/io/joern/fuzzyc2cpg/dotgenerator/DotAstGeneratorTests.scala b/joern-cli/frontends/fuzzyc2cpg/src/test/scala/io/joern/fuzzyc2cpg/dotgenerator/DotAstGeneratorTests.scala
index 4fe5307461ca..98d95a3da7c1 100644
--- a/joern-cli/frontends/fuzzyc2cpg/src/test/scala/io/joern/fuzzyc2cpg/dotgenerator/DotAstGeneratorTests.scala
+++ b/joern-cli/frontends/fuzzyc2cpg/src/test/scala/io/joern/fuzzyc2cpg/dotgenerator/DotAstGeneratorTests.scala
@@ -33,7 +33,7 @@ class DotAstGeneratorTests extends FuzzyCCodeToCpgSuite {
       cpg.method.name("my_func").dotAst.l match {
         case x :: _ =>
           x.startsWith("digraph \"my_func\"") shouldBe true
-          x.contains("""[label = "(CONTROL_STRUCTURE,if (y > 42),if (y > 42))" ]""") shouldBe true
+          x.contains("""[label = <(CONTROL_STRUCTURE,if (y &gt; 42),if (y &gt; 42))<SUB>5</SUB>> ]""") shouldBe true
           x.endsWith("}\n") shouldBe true
         case _ => fail()
       }
@@ -56,7 +56,7 @@ class DotAstGeneratorTests extends FuzzyCCodeToCpgSuite {
     "allow plotting sub trees of methods" in {
       cpg.method.ast.isControlStructure.code(".*y > 42.*").dotAst.l match {
         case x :: _ =>
-          x.contains("y > 42") shouldBe true
+          x.contains("y &gt; 42") shouldBe true
           x.contains("IDENTIFIER,y") shouldBe true
           x.contains("x * 2") shouldBe false
         case _ => fail()
@@ -68,8 +68,10 @@ class DotAstGeneratorTests extends FuzzyCCodeToCpgSuite {
         case x :: _ =>
           x should (
             startWith("digraph \"lemon\"") and
-              include("""[label = "(goog,goog(\"\\\"yes\\\"\"))" ]""") and
-              include("""[label = "(LITERAL,\"\\\"yes\\\"\",goog(\"\\\"yes\\\"\"))" ]""") and
+              include("""[label = <(goog,goog(&quot;\&quot;yes\&quot;&quot;))<SUB>18</SUB>>""") and
+              include(
+                """[label = <(LITERAL,&quot;\&quot;yes\&quot;&quot;,goog(&quot;\&quot;yes\&quot;&quot;))<SUB>18</SUB>> ]"""
+              ) and
               endWith("}\n")
           )
         case _ => fail()
diff --git a/joern-cli/frontends/fuzzyc2cpg/src/test/scala/io/joern/fuzzyc2cpg/dotgenerator/DotCfgGeneratorTests.scala b/joern-cli/frontends/fuzzyc2cpg/src/test/scala/io/joern/fuzzyc2cpg/dotgenerator/DotCfgGeneratorTests.scala
index 5c01569da1e3..1444c0d370f7 100644
--- a/joern-cli/frontends/fuzzyc2cpg/src/test/scala/io/joern/fuzzyc2cpg/dotgenerator/DotCfgGeneratorTests.scala
+++ b/joern-cli/frontends/fuzzyc2cpg/src/test/scala/io/joern/fuzzyc2cpg/dotgenerator/DotCfgGeneratorTests.scala
@@ -23,7 +23,7 @@ class DotCfgGeneratorTests extends FuzzyCCodeToCpgSuite {
       cpg.method.name("main").dotCfg.l match {
         case x :: _ =>
           x.startsWith("digraph \"main\" {") shouldBe true
-          x.contains("(<operator>.assignment,i = 0)") shouldBe true
+          x.contains("(&lt;operator&gt;.assignment,i = 0)") shouldBe true
           x.endsWith("}\n") shouldBe true
         case _ => fail()
       }