Repository: joernio/joern (scala)

Issue:
[ruby] Add `.call` Member to Proc/Lambda Types
Proc parameters and lambdas can be executed via `.call`, and thus need the corresponding `call` member.
```ruby
def method(p1, &block)
  return block.call(p1)
end
```
At the call site of such a lambda, e.g.
```ruby
method("foo") do |x|
  puts x
end
```
The type corresponding to the lambda should create the bound lambda type decl with this member, with a `dynamicTypeHintFullName` of this bound type.

Pull request:
Given the code block below:
```ruby
def foo &block
 puts block.call
end

foo do
 "world!"
end
```
Proc parameters from do-blocks (interpreted with the usual CPG lambda handling), suggests that there is a `call` member on the type bound to the type decl.

This PR adds this member, with the dynamic type hint referring to the type decl bound to this method.

Resolves #4700

cc @ml86

Patch:
diff --git a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstForFunctionsCreator.scala b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstForFunctionsCreator.scala
index ed8a876727c5..4bdc09643f52 100644
--- a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstForFunctionsCreator.scala
+++ b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstForFunctionsCreator.scala
@@ -104,7 +104,14 @@ trait AstForFunctionsCreator(implicit withSchemaValidation: ValidationMode) { th
       scope.surroundingAstLabel.foreach(typeDeclNode_.astParentType(_))
       scope.surroundingScopeFullName.foreach(typeDeclNode_.astParentFullName(_))
       createMethodTypeBindings(method, typeDeclNode_)
-      if isClosure then Ast(typeDeclNode_).withChild(Ast(newModifierNode(ModifierTypes.LAMBDA))) else Ast(typeDeclNode_)
+      if isClosure then
+        Ast(typeDeclNode_)
+          .withChild(Ast(newModifierNode(ModifierTypes.LAMBDA)))
+          .withChild(
+            // This member refers back to itself, as itself is the type decl bound to the respective method
+            Ast(NewMember().name("call").code("call").dynamicTypeHintFullName(Seq(fullName)).typeFullName(Defines.Any))
+          )
+      else Ast(typeDeclNode_)
     }
 
     val modifiers = mutable.Buffer(ModifierTypes.VIRTUAL)


Test patch:
diff --git a/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/DoBlockTests.scala b/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/DoBlockTests.scala
index 717ffd243467..4b89a14b3c87 100644
--- a/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/DoBlockTests.scala
+++ b/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/DoBlockTests.scala
@@ -2,6 +2,7 @@ package io.joern.rubysrc2cpg.querying
 
 import io.joern.rubysrc2cpg.passes.GlobalTypes.builtinPrefix
 import io.joern.rubysrc2cpg.testfixtures.RubyCode2CpgFixture
+import io.joern.x2cpg.Defines
 import io.shiftleft.codepropertygraph.generated.nodes.*
 import io.shiftleft.semanticcpg.language.*
 
@@ -36,6 +37,10 @@ class DoBlockTests extends RubyCode2CpgFixture {
             case closureType :: Nil =>
               closureType.name shouldBe "<lambda>0"
               closureType.fullName shouldBe "Test0.rb:<global>::program:<lambda>0"
+
+              val callMember = closureType.member.nameExact("call").head
+              callMember.typeFullName shouldBe Defines.Any
+              callMember.dynamicTypeHintFullName shouldBe Seq("Test0.rb:<global>::program:<lambda>0")
             case xs => fail(s"Expected a one closure type node, instead got [${xs.code.mkString(", ")}]")
           }
         case xs => fail(s"Expected a single program module, instead got [${xs.code.mkString(", ")}]")