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 } }