From 821c865de6cd392c4aee124b362ce29b60f9b017 Mon Sep 17 00:00:00 2001 From: David Baker Effendi Date: Wed, 19 Jun 2024 21:11:27 +0200 Subject: [PATCH] [ruby] Singleton Method-Member Bindings (#4679) * 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 --- .../rubysrc2cpg/astcreation/AstCreator.scala | 39 +-------- .../astcreation/AstForFunctionsCreator.scala | 14 ++- .../astcreation/AstForTypesCreator.scala | 28 ++++-- .../dataflow/SingleAssignmentTests.scala | 4 +- .../rubysrc2cpg/querying/ClassTests.scala | 85 ++++++++++++++++--- .../rubysrc2cpg/querying/MethodTests.scala | 5 -- 6 files changed, 111 insertions(+), 64 deletions(-) diff --git a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstCreator.scala b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstCreator.scala index ff138c237416..ea422185d4e1 100644 --- a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstCreator.scala +++ b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstCreator.scala @@ -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 @@ -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) @@ -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. 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 6930f59cd685..bd720e6ca143 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 @@ -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(""), + astParentFullName = astParentFullName.getOrElse("") + ) + ) + createMethodTypeBindings(method, methodTypeDecl :: Nil) val thisParameterAst = Ast( newThisParameterNode( @@ -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( diff --git a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstForTypesCreator.scala b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstForTypesCreator.scala index f68179507857..db07e87ff9ca 100644 --- a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstForTypesCreator.scala +++ b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstForTypesCreator.scala @@ -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 @@ -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")) + .astParentType(NodeTypes.TYPE_DECL) + scope.surroundingScopeFullName.map(x => s"$x").foreach(typeDeclMember.astParentFullName(_)) + diffGraph.addNode(typeDeclMember) + } + val prefixAst = createTypeRefPointer(typeDecl) val typeDeclAst = Ast(typeDecl) .withChildren(classModifiers) diff --git a/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/dataflow/SingleAssignmentTests.scala b/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/dataflow/SingleAssignmentTests.scala index 695e5cdf7897..7f20321567c8 100644 --- a/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/dataflow/SingleAssignmentTests.scala +++ b/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/dataflow/SingleAssignmentTests.scala @@ -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)) } diff --git a/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/ClassTests.scala b/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/ClassTests.scala index 4b9008b80aff..e6db696abd6a 100644 --- a/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/ClassTests.scala +++ b/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/ClassTests.scala @@ -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 { @@ -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").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").head + val nestedTypeDeclMember = cpg.member("Nested").head + val singletonTypeDecl = cpg.typeDecl.nameExact("Nested").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" + 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 @@ -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:::program.MMM" + cpg.member("NConst").typeDecl.fullName.head shouldBe "Test0.rb:::program.MMM.Nested" + } + "a basic anonymous class" should { val cpg = code(""" |a = Class.new do diff --git a/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/MethodTests.scala b/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/MethodTests.scala index f5f4a4714176..2ab3117ade98 100644 --- a/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/MethodTests.scala +++ b/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/MethodTests.scala @@ -42,11 +42,6 @@ class MethodTests extends RubyCode2CpgFixture { val List(m) = cpg.method.nameExact(RDefines.Program).l m.fullName shouldBe "Test0.rb:::program" m.isModule.nonEmpty shouldBe true - - val List(t) = cpg.typeDecl.nameExact(RDefines.Program).l - m.fullName shouldBe "Test0.rb:::program" - m.isModule.nonEmpty shouldBe true - t.methodBinding.methodFullName.toSet should contain(m.fullName) } }