From 6e1f578a19217ce2561db2c759c39076c519b2f8 Mon Sep 17 00:00:00 2001
From: Xavier Pinho <mail@xavierp.org>
Date: Wed, 8 Jan 2025 15:07:40 +0000
Subject: [PATCH] [c#] allow resolving fully-qualified names without importing
 them first

---
 .../datastructures/CSharpScope.scala          |  9 +-
 .../querying/ast/FieldAccessTests.scala       | 94 +++++++++++++++----
 .../io/joern/x2cpg/utils/ListUtils.scala      |  3 +
 3 files changed, 88 insertions(+), 18 deletions(-)

diff --git a/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/datastructures/CSharpScope.scala b/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/datastructures/CSharpScope.scala
index a0840dfbdcbf..a31f78ee9633 100644
--- a/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/datastructures/CSharpScope.scala
+++ b/joern-cli/frontends/csharpsrc2cpg/src/main/scala/io/joern/csharpsrc2cpg/datastructures/CSharpScope.scala
@@ -2,6 +2,7 @@ package io.joern.csharpsrc2cpg.datastructures
 
 import io.joern.x2cpg.Defines
 import io.joern.x2cpg.datastructures.{OverloadableScope, Scope, ScopeElement, TypedScope, TypedScopeElement}
+import io.joern.x2cpg.utils.ListUtils.singleOrNone
 import io.shiftleft.codepropertygraph.generated.nodes.DeclarationNew
 
 import scala.collection.mutable
@@ -62,7 +63,13 @@ class CSharpScope(summary: CSharpProgramSummary)
     if (typeName == "this") {
       surroundingTypeDeclFullName.flatMap(summary.matchingTypes).headOption
     } else {
-      super.tryResolveTypeReference(typeName)
+      super.tryResolveTypeReference(typeName) match
+        case Some(x) => Some(x)
+        case None    =>
+          // typeName might be a fully-qualified name e.g. System.Console, in which case, even if we
+          // don't import System (i.e. System is not in typesInScope), we should still find it if it's
+          // in the type summaries and there's exactly 1 match.
+          Some(typeName).filter(_.contains(".")).flatMap(summary.matchingTypes.andThen(singleOrNone))
     }
   }
 
diff --git a/joern-cli/frontends/csharpsrc2cpg/src/test/scala/io/joern/csharpsrc2cpg/querying/ast/FieldAccessTests.scala b/joern-cli/frontends/csharpsrc2cpg/src/test/scala/io/joern/csharpsrc2cpg/querying/ast/FieldAccessTests.scala
index 7880650dab94..726a2b81478d 100644
--- a/joern-cli/frontends/csharpsrc2cpg/src/test/scala/io/joern/csharpsrc2cpg/querying/ast/FieldAccessTests.scala
+++ b/joern-cli/frontends/csharpsrc2cpg/src/test/scala/io/joern/csharpsrc2cpg/querying/ast/FieldAccessTests.scala
@@ -6,7 +6,7 @@ import io.shiftleft.semanticcpg.language.*
 
 class FieldAccessTests extends CSharpCode2CpgFixture {
 
-  "Console.WriteLine call" should {
+  "Console.WriteLine call while importing System" should {
     val cpg = code("""
         |using System;
         |Console.WriteLine("foo");
@@ -15,15 +15,32 @@ class FieldAccessTests extends CSharpCode2CpgFixture {
     "have WriteLine call correctly set" in {
       inside(cpg.call.nameExact("WriteLine").l) {
         case writeLine :: Nil =>
+          writeLine.code shouldBe "Console.WriteLine(\"foo\")"
           writeLine.methodFullName shouldBe "System.Console.WriteLine:System.Void(System.String)"
-          writeLine.argument(0).code shouldBe "Console"
-          writeLine.argument(1).code shouldBe "\"foo\""
         case xs => fail(s"Expected single WriteLine call, but got $xs")
       }
     }
+
+    "have foo literal correctly set" in {
+      inside(cpg.call.nameExact("WriteLine").argument(1).isLiteral.l) {
+        case foo :: Nil =>
+          foo.typeFullName shouldBe "System.String"
+          foo.code shouldBe "\"foo\""
+        case xs => fail(s"Expected single literal argument to WriteLine, but got $xs")
+      }
+    }
+
+    "have Console correctly set" in {
+      inside(cpg.call.nameExact("WriteLine").argument(0).isIdentifier.l) {
+        case console :: Nil =>
+          console.code shouldBe "Console"
+          console.typeFullName shouldBe "System.Console"
+        case xs => fail(s"Expected single Console identifier, but got $xs")
+      }
+    }
   }
 
-  "System.Console.WriteLine call" should {
+  "System.Console.WriteLine call while importing System" should {
     val cpg = code("""
         |using System;
         |System.Console.WriteLine("foo");
@@ -33,22 +50,65 @@ class FieldAccessTests extends CSharpCode2CpgFixture {
       inside(cpg.call.nameExact("WriteLine").l) {
         case writeLine :: Nil =>
           writeLine.methodFullName shouldBe "System.Console.WriteLine:System.Void(System.String)"
-          inside(writeLine.argument(0).fieldAccess.l) {
-            case sysConsole :: Nil =>
-              sysConsole.typeFullName shouldBe "System.Console"
-              sysConsole.code shouldBe "System.Console"
-              sysConsole.fieldIdentifier.code.l shouldBe List("Console")
-            case xs => fail(s"Expected single fieldAccess to the left of WriteLine, but got $xs")
-          }
-          inside(writeLine.argument(1).start.isLiteral.l) {
-            case foo :: Nil =>
-              foo.typeFullName shouldBe "System.String"
-              foo.code shouldBe "\"foo\""
-            case xs => fail(s"Expected single literal argument to WriteLine, but got $xs")
-          }
+          writeLine.code shouldBe "System.Console.WriteLine(\"foo\")"
         case xs => fail(s"Expected single WriteLine call, but got $xs")
       }
     }
+
+    "have foo literal correctly set" in {
+      inside(cpg.call.nameExact("WriteLine").argument(1).isLiteral.l) {
+        case foo :: Nil =>
+          foo.typeFullName shouldBe "System.String"
+          foo.code shouldBe "\"foo\""
+        case xs => fail(s"Expected single literal argument to WriteLine, but got $xs")
+      }
+    }
+
+    "have System.Console correctly set" in {
+      inside(cpg.call.nameExact("WriteLine").argument(0).fieldAccess.l) {
+        case sysConsole :: Nil =>
+          sysConsole.typeFullName shouldBe "System.Console"
+          sysConsole.code shouldBe "System.Console"
+          sysConsole.fieldIdentifier.code.l shouldBe List("Console")
+          sysConsole.fieldIdentifier.canonicalName.l shouldBe List("Console")
+        case xs => fail(s"Expected single fieldAccess to the left of WriteLine, but got $xs")
+      }
+    }
+  }
+
+  "System.Console.WriteLine call without importing System" should {
+    val cpg = code("""
+        |System.Console.WriteLine("foo");
+        |""".stripMargin)
+
+    "have WriteLine call correctly set" in {
+      inside(cpg.call.nameExact("WriteLine").l) {
+        case writeLine :: Nil =>
+          writeLine.methodFullName shouldBe "System.Console.WriteLine:System.Void(System.String)"
+          writeLine.code shouldBe "System.Console.WriteLine(\"foo\")"
+        case xs => fail(s"Expected single WriteLine call, but got $xs")
+      }
+    }
+
+    "have foo literal correctly set" in {
+      inside(cpg.call.nameExact("WriteLine").argument(1).isLiteral.l) {
+        case foo :: Nil =>
+          foo.typeFullName shouldBe "System.String"
+          foo.code shouldBe "\"foo\""
+        case xs => fail(s"Expected single literal argument to WriteLine, but got $xs")
+      }
+    }
+
+    "have System.Console correctly set" in {
+      inside(cpg.call.nameExact("WriteLine").argument(0).fieldAccess.l) {
+        case sysConsole :: Nil =>
+          sysConsole.typeFullName shouldBe "System.Console"
+          sysConsole.code shouldBe "System.Console"
+          sysConsole.fieldIdentifier.code.l shouldBe List("Console")
+          sysConsole.fieldIdentifier.canonicalName.l shouldBe List("Console")
+        case xs => fail(s"Expected single fieldAccess to the left of WriteLine, but got $xs")
+      }
+    }
   }
 
   "field access via explicit `this.X`" should {
diff --git a/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/utils/ListUtils.scala b/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/utils/ListUtils.scala
index dc3c75e8ec24..9d83c91fd5f8 100644
--- a/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/utils/ListUtils.scala
+++ b/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/utils/ListUtils.scala
@@ -16,5 +16,8 @@ object ListUtils {
         case _                   => Nil
       }
     }
+
+    /** Returns the single element, or None if the list is empty or contains more than one element. */
+    def singleOrNone: Option[T] = if list.size == 1 then list.headOption else None
   }
 }