Skip to content

Commit

Permalink
[ruby] Singleton Method-Member Bindings (#4679)
Browse files Browse the repository at this point in the history
* Method-Members for singletons are now implemented and bound to their respective types.
* Nested type decl members are under singletons now
* TypeDecl members hint towards singletons now
* Removed `:program` Type Decl
  • Loading branch information
DavidBakerEffendi authored Jun 19, 2024
1 parent 0709d86 commit 821c865
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,13 @@ class AstCreator(
.fullName(fullName)

scope.pushNewScope(NamespaceScope(fullName))
val (rubyFakeMethod, rubyFakeMethodAst) = astInFakeMethod(rootStatements)
val rubyFakeTypeDecl = astInFakeTypeDecl(rootStatements, rubyFakeMethod)
val rubyFakeMethodAst = astInFakeMethod(rootStatements)
scope.popScope()

Ast(fileNode).withChild(Ast(namespaceBlock).withChild(rubyFakeMethodAst).withChild(rubyFakeTypeDecl))
Ast(fileNode).withChild(Ast(namespaceBlock).withChild(rubyFakeMethodAst))
}

private def astInFakeMethod(rootNode: StatementList): (NewMethod, Ast) = {
private def astInFakeMethod(rootNode: StatementList): Ast = {
val name = Defines.Program
val fullName = computeMethodFullName(name)
val code = rootNode.text
Expand All @@ -92,7 +91,7 @@ class AstCreator(
)
val methodReturn = methodReturnNode(rootNode, Defines.Any)

methodNode_ -> scope.newProgramScope
scope.newProgramScope
.map { moduleScope =>
scope.pushNewScope(moduleScope)
val block = blockNode(rootNode)
Expand All @@ -112,36 +111,6 @@ class AstCreator(
.getOrElse(Ast())
}

private def astInFakeTypeDecl(rootNode: StatementList, method: NewMethod): Ast = {
val typeDeclNode_ = typeDeclNode(rootNode, method.name, method.fullName, method.filename, Nil, None)
val members = rootNode.statements
.collect {
case m: MethodDeclaration =>
val methodName = m.methodName
NewMember()
.name(methodName)
.code(methodName)
.typeFullName(Defines.Any)
.dynamicTypeHintFullName(s"${method.fullName}:$methodName" :: Nil)
case t: TypeDeclaration =>
val typeName = t.name.text
NewMember()
.name(t.name.text)
.code(typeName)
.typeFullName(Defines.Any)
.dynamicTypeHintFullName(s"${method.fullName}.$typeName" :: Nil)
}
.map(Ast.apply)

val bindingNode = newBindingNode("", "", method.fullName)
diffGraph.addEdge(typeDeclNode_, bindingNode, EdgeTypes.BINDS)
diffGraph.addEdge(bindingNode, method, EdgeTypes.REF)

Ast(typeDeclNode_)
.withChildren(Ast(newModifierNode(ModifierTypes.MODULE)) :: Ast(newModifierNode(ModifierTypes.VIRTUAL)) :: Nil)
.withChildren(members)
}

}

/** Determines till what depth the AST creator will parse until.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,18 @@ trait AstForFunctionsCreator(implicit withSchemaValidation: ValidationMode) { th
astParentType = astParentType,
astParentFullName = astParentFullName
)
val methodTypeDecl = Ast(
typeDeclNode(
node,
node.methodName,
fullName,
relativeFileName,
code(node),
astParentType = astParentType.getOrElse("<empty>"),
astParentFullName = astParentFullName.getOrElse("<empty>")
)
)
createMethodTypeBindings(method, methodTypeDecl :: Nil)

val thisParameterAst = Ast(
newThisParameterNode(
Expand Down Expand Up @@ -415,7 +427,7 @@ trait AstForFunctionsCreator(implicit withSchemaValidation: ValidationMode) { th
Ast.storeInDiffGraph(_methodAst, diffGraph)
Nil
} else {
createMethodRefPointer(method) :: _methodAst :: Nil
createMethodRefPointer(method) :: _methodAst :: methodTypeDecl :: Nil
}
case targetNode =>
logger.warn(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@ import io.joern.rubysrc2cpg.astcreation.RubyIntermediateAst.*
import io.joern.rubysrc2cpg.datastructures.{BlockScope, MethodScope, ModuleScope, TypeScope}
import io.joern.rubysrc2cpg.passes.Defines
import io.joern.x2cpg.utils.NodeBuilders.newModifierNode
import io.joern.x2cpg.{Ast, ValidationMode, Defines as XDefines}
import io.shiftleft.codepropertygraph.generated.nodes.{
NewCall,
NewFieldIdentifier,
NewIdentifier,
NewMethod,
NewTypeDecl,
NewTypeRef
import io.joern.x2cpg.{Ast, ValidationMode}
import io.shiftleft.codepropertygraph.generated.nodes.*
import io.shiftleft.codepropertygraph.generated.{
DispatchTypes,
EvaluationStrategies,
ModifierTypes,
NodeTypes,
Operators
}
import io.shiftleft.codepropertygraph.generated.{DispatchTypes, EvaluationStrategies, ModifierTypes, Operators}

import scala.collection.immutable.List
import scala.collection.mutable
Expand Down Expand Up @@ -151,6 +150,17 @@ trait AstForTypesCreator(implicit withSchemaValidation: ValidationMode) { this:
.partition(_._1)

scope.popScope()

if scope.surroundingAstLabel.contains(NodeTypes.TYPE_DECL) then {
val typeDeclMember = NewMember()
.name(className)
.code(className)
.dynamicTypeHintFullName(Seq(s"$classFullName<class>"))
.astParentType(NodeTypes.TYPE_DECL)
scope.surroundingScopeFullName.map(x => s"$x<class>").foreach(typeDeclMember.astParentFullName(_))
diffGraph.addNode(typeDeclMember)
}

val prefixAst = createTypeRefPointer(typeDecl)
val typeDeclAst = Ast(typeDecl)
.withChildren(classModifiers)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ class SingleAssignmentTests extends RubyCode2CpgFixture(withPostProcessing = tru
val flows = sink.reachableByFlows(source).map(flowToResultPairs).distinct.sortBy(_.length).l
val List(flow1, flow2, flow3, flow4, flow5) = flows
flow1 shouldBe List(("y = 1", 2), ("puts y", 3))
flow2 shouldBe List(("y = 1", 2), ("puts y", 3), ("puts x", 4))
flow3 shouldBe List(("y = 1", 2), ("x = y = 1", 2), ("puts x", 4))
flow2 shouldBe List(("y = 1", 2), ("x = y = 1", 2), ("puts x", 4))
flow3 shouldBe List(("y = 1", 2), ("puts y", 3), ("puts x", 4))
flow4 shouldBe List(("y = 1", 2), ("x = y = 1", 2), ("z = x = y = 1", 2), ("puts z", 5))
flow5 shouldBe List(("y = 1", 2), ("x = y = 1", 2), ("puts x", 4), ("puts z", 5))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
package io.joern.rubysrc2cpg.querying

import io.joern.rubysrc2cpg.passes.{GlobalTypes, Defines as RubyDefines}
import io.joern.rubysrc2cpg.testfixtures.RubyCode2CpgFixture
import io.joern.x2cpg.Defines
import io.shiftleft.codepropertygraph.generated.{ModifierTypes, Operators}
import io.shiftleft.codepropertygraph.generated.nodes.{
Block,
Call,
FieldIdentifier,
Identifier,
Literal,
MethodRef,
Modifier,
Return
}
import io.shiftleft.codepropertygraph.generated.Operators
import io.shiftleft.codepropertygraph.generated.nodes.*
import io.shiftleft.semanticcpg.language.*
import io.joern.rubysrc2cpg.passes.{GlobalTypes, Defines as RubyDefines}

class ClassTests extends RubyCode2CpgFixture {

Expand Down Expand Up @@ -204,6 +195,61 @@ class ClassTests extends RubyCode2CpgFixture {
memberF.dynamicTypeHintFullName.toSet should contain(methodF.fullName)
}

"`M.method` in a module `M` should have a method bound to a member under the module's singleton type declaration" in {
val cpg = code("""
|module M
| def M.method(x)
| x
| end
|end
|def main(p)
| M::method(p)
|end
|""".stripMargin)

// Obtain the nodes first
val regularTypeDecl = cpg.typeDecl.nameExact("M").head
val singletonTypeDecl = cpg.typeDecl.nameExact("M<class>").head
val method = regularTypeDecl.method.nameExact("method").head
val methodTypeDecl = cpg.typeDecl.fullNameExact(method.fullName).head
val methodMember = singletonTypeDecl.member.nameExact("method").head
// Now determine the properties and potential edges
methodMember.dynamicTypeHintFullName.toSet should contain(method.fullName)
methodTypeDecl.methodBinding.flatMap(_.boundMethod).head shouldBe method
}

"a method in a nested module should have the nested module's member-type and nested types' method" in {
val cpg = code("""
|module MMM
| module Nested
| def self.method(x)
| x
| end
| end
|end
|def outer(aaa)
| MMM::Nested::method(aaa)
|end
|""".stripMargin)

val nestedTypeDecl = cpg.typeDecl("Nested").head
val nestedSingleton = cpg.typeDecl("Nested<class>").head
val nestedTypeDeclMember = cpg.member("Nested").head
val singletonTypeDecl = cpg.typeDecl.nameExact("Nested<class>").head

val method = nestedTypeDecl.method.nameExact("method").head
val methodTypeDecl = cpg.typeDecl.fullNameExact(method.fullName).head
val methodMember = singletonTypeDecl.member.nameExact("method").head

nestedTypeDeclMember.typeDecl.name shouldBe "MMM<class>"
nestedTypeDeclMember.dynamicTypeHintFullName.toSet should contain(nestedSingleton.fullName)

singletonTypeDecl.astParent.asInstanceOf[TypeDecl].name shouldBe "MMM"

methodMember.dynamicTypeHintFullName.toSet should contain(method.fullName)
methodTypeDecl.methodBinding.flatMap(_.boundMethod).head shouldBe method
}

"`def initialize() ... end` directly inside a class has the constructor modifier" in {
val cpg = code("""
|class C
Expand Down Expand Up @@ -255,6 +301,21 @@ class ClassTests extends RubyCode2CpgFixture {
cpg.method.nameExact(RubyDefines.Initialize).where(_.isConstructor).literal.code.l should be(empty)
}

"Constants should be defined under the respective singleton" in {
val cpg = code("""
|module MMM
| MConst = 2
| module Nested
| NConst = 4
| end
|end
|
|""".stripMargin)

cpg.member("MConst").typeDecl.fullName.head shouldBe "Test0.rb:<global>::program.MMM<class>"
cpg.member("NConst").typeDecl.fullName.head shouldBe "Test0.rb:<global>::program.MMM.Nested<class>"
}

"a basic anonymous class" should {
val cpg = code("""
|a = Class.new do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,6 @@ class MethodTests extends RubyCode2CpgFixture {
val List(m) = cpg.method.nameExact(RDefines.Program).l
m.fullName shouldBe "Test0.rb:<global>::program"
m.isModule.nonEmpty shouldBe true

val List(t) = cpg.typeDecl.nameExact(RDefines.Program).l
m.fullName shouldBe "Test0.rb:<global>::program"
m.isModule.nonEmpty shouldBe true
t.methodBinding.methodFullName.toSet should contain(m.fullName)
}
}

Expand Down

0 comments on commit 821c865

Please sign in to comment.