Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[c#] not requiring exact match for extension methods #5248

Merged
merged 1 commit into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")
}
}
}
Loading