Skip to content

Commit

Permalink
[c#] a few more extension method tests (#5248)
Browse files Browse the repository at this point in the history
  • Loading branch information
xavierpinho authored Jan 24, 2025
1 parent 16c8afb commit 00d5d2c
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,6 @@ class CSharpScope(summary: CSharpProgramSummary)
callName: String,
argTypes: List[String]
): Option[(CSharpMethod, String)] = {
baseTypeFullName.flatMap { tfn =>
extensionsInScopeFor(tfn, callName, argTypes).take(2).toList match {
case x :: Nil => Some((x.methods.head, x.name))
case _ => None
}
}
baseTypeFullName.flatMap(extensionsInScopeFor(_, callName, argTypes).headOption).map(x => (x.methods.head, x.name))
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package io.joern.csharpsrc2cpg.querying.ast

import io.joern.csharpsrc2cpg.testfixtures.CSharpCode2CpgFixture
import io.shiftleft.codepropertygraph.generated.ModifierTypes
import io.shiftleft.codepropertygraph.generated.nodes.Identifier
import io.shiftleft.codepropertygraph.generated.{DispatchTypes, ModifierTypes}
import io.shiftleft.codepropertygraph.generated.nodes.{Call, Identifier}
import io.shiftleft.semanticcpg.language.*

class ExtensionMethodTests extends CSharpCode2CpgFixture {
Expand Down Expand Up @@ -55,6 +55,7 @@ class ExtensionMethodTests extends CSharpCode2CpgFixture {
case doStuff :: Nil =>
doStuff.code shouldBe "x.DoStuff()"
doStuff.methodFullName shouldBe "Extensions.DoStuff:void(MyClass)"
doStuff.dispatchType shouldBe DispatchTypes.STATIC_DISPATCH
case xs => fail(s"Expected single DoStuff call, but got $xs")
}
}
Expand Down Expand Up @@ -105,4 +106,136 @@ class ExtensionMethodTests extends CSharpCode2CpgFixture {
}
}
}

"two same-named extension methods involving explicit sub-types" should {

"map to the compile-time type (1)" in {
val cpg = code("""
|var x = new MyConcrete();
|x.DoStuff();
|
|abstract class MyAbstract;
|class MyConcrete : MyAbstract;
|
|static class Extensions
|{
| public static int DoStuff(this MyAbstract myAbstract) => 1;
| public static int DoStuff(this MyConcrete myConcrete) => 2;
|}
|""".stripMargin)
cpg.call.nameExact("DoStuff").methodFullName.l shouldBe List("Extensions.DoStuff:System.Int32(MyConcrete)")
}

"map to the compile-time type (2)" in {
val cpg = code("""
|MyAbstract x = new MyConcrete();
|x.DoStuff();
|
|abstract class MyAbstract;
|class MyConcrete : MyAbstract;
|
|static class Extensions
|{
| public static int DoStuff(this MyAbstract myAbstract) => 1;
| public static int DoStuff(this MyConcrete myConcrete) => 2;
|}
|""".stripMargin)
cpg.call.nameExact("DoStuff").methodFullName.l shouldBe List("Extensions.DoStuff:System.Int32(MyAbstract)")
}
}

"calling an extension method for `List<string>`" should {

"resolve correctly if the receiver is of type `List<string>`" in {
val cpg = code("""
|using System.Collections.Generic;
|
|var x = new List<string>();
|x.DoStuff();
|
|static class Extensions
|{
| public static int DoStuff(this List<string> myList) => 1;
|}
|""".stripMargin)

cpg.call.nameExact("DoStuff").methodFullName.l shouldBe List("Extensions.DoStuff:System.Int32(List)")
}

"resolve correctly if there's only 1 type-parametric extension for `List<T>`" in {
val cpg = code("""
|using System.Collections.Generic;
|
|var x = new List<string>();
|x.DoStuff();
|
|static class Extensions
|{
| public static int DoStuff<T>(this List<T> myList) => 1;
|}
|""".stripMargin)

cpg.call.nameExact("DoStuff").methodFullName.l shouldBe List("Extensions.DoStuff:System.Int32(List)")
}

// TODO: The two `DoStuff` methods have the same methodFullName.
"resolve correctly if there are 2 possible extensions, one for `List<string>` and another for `List<T>`" ignore {
val cpg = code("""
|using System.Collections.Generic;
|
|var x = new List<string>();
|x.DoStuff();
|
|static class Extensions
|{
| public static int DoStuff<T>(this List<T> myList) { return 1; }
| public static int DoStuff(this List<string> myList) { return 2; }
|}
|""".stripMargin)

cpg.call.nameExact("DoStuff").callee.l shouldBe cpg.literal("2").method.l
}
}

"consecutive unary extension method calls" should {
val cpg = code("""
|var x = new MyClass();
|var y = x.Foo().Bar();
|
|class MyClass {}
|static class Extensions
|{
| public static MyClass Foo(this MyClass c) => c;
| public static int Bar(this MyClass c) => 1;
|}
|""".stripMargin)

"have correct properties and arguments" in {
inside(cpg.call.nameExact("Bar").l) {
case bar :: Nil =>
bar.code shouldBe "x.Foo().Bar()"
bar.methodFullName shouldBe "Extensions.Bar:System.Int32(MyClass)"
bar.dispatchType shouldBe DispatchTypes.STATIC_DISPATCH
inside(bar.argument.sortBy(_.argumentIndex).l) {
case (foo: Call) :: Nil =>
foo.code shouldBe "x.Foo()"
foo.methodFullName shouldBe "Extensions.Foo:MyClass(MyClass)"
foo.dispatchType shouldBe DispatchTypes.STATIC_DISPATCH
inside(foo.argument.sortBy(_.argumentIndex).l) {
case (x: Identifier) :: Nil =>
x.code shouldBe "x"
x.name shouldBe "x"
x.typeFullName shouldBe "MyClass"
case xs => fail(s"Expected identifier argument to Foo, but got $xs")
}
case xs => fail(s"Expected single call argument to Bar, but got $xs")
}
case xs => fail(s"Expected single call to Bar, but got $xs")
}
}

"have correct properties for the result of the chained call" in {
cpg.assignment.target.isIdentifier.nameExact("y").typeFullName.l shouldBe List("System.Int32")
}
}
}

0 comments on commit 00d5d2c

Please sign in to comment.