diff --git a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstCreatorHelper.scala b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstCreatorHelper.scala index 70996124a5b0..17ada2277a55 100644 --- a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstCreatorHelper.scala +++ b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstCreatorHelper.scala @@ -110,8 +110,15 @@ trait AstCreatorHelper(implicit withSchemaValidation: ValidationMode) { this: As callAst(assignment, Seq(lhs, rhs)) } - protected def memberForMethod(method: NewMethod): NewMember = { - NewMember().name(method.name).code(method.name).dynamicTypeHintFullName(Seq(method.fullName)) + protected def memberForMethod( + method: NewMethod, + astParentType: Option[String] = None, + astParentFullName: Option[String] = None + ): NewMember = { + val member = NewMember().name(method.name).code(method.name).dynamicTypeHintFullName(Seq(method.fullName)) + astParentType.foreach(member.astParentType(_)) + astParentFullName.foreach(member.astParentFullName(_)) + member } protected val UnaryOperatorNames: Map[String, String] = Map( diff --git a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstForExpressionsCreator.scala b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstForExpressionsCreator.scala index 986b5386ede2..98563d5919a9 100644 --- a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstForExpressionsCreator.scala +++ b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/astcreation/AstForExpressionsCreator.scala @@ -205,10 +205,15 @@ trait AstForExpressionsCreator(implicit withSchemaValidation: ValidationMode) { protected def astForObjectInstantiation(node: RubyNode & ObjectInstantiation): Ast = { val className = node.target.text - val methodName = XDefines.ConstructorMethodName + val callName = "new" + val methodName = Defines.Initialize + /* + We short-cut the call edge from `new` call to `initialize` method, however we keep the modelling of the receiver + as referring to the singleton class. + */ val (receiverTypeFullName, fullName) = scope.tryResolveTypeReference(className) match { - case Some(typeMetaData) => typeMetaData.name -> s"${typeMetaData.name}:$methodName" - case None => XDefines.Any -> XDefines.DynamicCallUnknownFullName + case Some(typeMetaData) => s"${typeMetaData.name}" -> s"${typeMetaData.name}:$methodName" + case None => XDefines.Any -> XDefines.DynamicCallUnknownFullName } /* Similarly to some other frontends, we lower the constructor into two operations, e.g., @@ -221,7 +226,7 @@ trait AstForExpressionsCreator(implicit withSchemaValidation: ValidationMode) { val tmp = SimpleIdentifier(Option(className))(node.span.spanStart(tmpGen.fresh)) def tmpIdentifier = { val tmpAst = astForSimpleIdentifier(tmp) - tmpAst.root.collect { case x: NewIdentifier => x.typeFullName(receiverTypeFullName) } + tmpAst.root.collect { case x: NewIdentifier => x.typeFullName(receiverTypeFullName.stripSuffix("")) } tmpAst } @@ -248,7 +253,7 @@ trait AstForExpressionsCreator(implicit withSchemaValidation: ValidationMode) { x.arguments.map(astForMethodCallArgument) :+ methodRef } - val constructorCall = callNode(node, code(node), methodName, fullName, DispatchTypes.DYNAMIC_DISPATCH) + val constructorCall = callNode(node, code(node), callName, fullName, DispatchTypes.DYNAMIC_DISPATCH) val constructorCallAst = callAst(constructorCall, argumentAsts, Option(tmpIdentifier)) scope.popScope() 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 b19e0d8f1a7f..ea535c60fc5a 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 @@ -39,9 +39,11 @@ trait AstForFunctionsCreator(implicit withSchemaValidation: ValidationMode) { th protected def astForMethodDeclaration(node: MethodDeclaration, isClosure: Boolean = false): Seq[Ast] = { // Special case constructor methods - val isInTypeDecl = scope.surroundingAstLabel.contains(NodeTypes.TYPE_DECL) - val isConstructor = node.methodName == "initialize" && isInTypeDecl - val methodName = if isConstructor then XDefines.ConstructorMethodName else node.methodName + val isInTypeDecl = scope.surroundingAstLabel.contains(NodeTypes.TYPE_DECL) + val isConstructor = + (node.methodName == Defines.Initialize || node.methodName == Defines.InitializeClass) && isInTypeDecl + val isSingletonConstructor = node.methodName == Defines.InitializeClass && isInTypeDecl + val methodName = if isSingletonConstructor then Defines.Initialize else node.methodName // TODO: body could be a try val fullName = computeMethodFullName(methodName) val method = methodNode( @@ -52,7 +54,9 @@ trait AstForFunctionsCreator(implicit withSchemaValidation: ValidationMode) { th signature = None, fileName = relativeFileName, astParentType = scope.surroundingAstLabel, - astParentFullName = scope.surroundingScopeFullName + astParentFullName = scope.surroundingScopeFullName.map { tn => + if isSingletonConstructor then s"$tn" else tn + } ) if (isConstructor) scope.pushNewScope(ConstructorScope(fullName)) @@ -80,7 +84,9 @@ trait AstForFunctionsCreator(implicit withSchemaValidation: ValidationMode) { th relativeFileName, code(node), astParentType = scope.surroundingAstLabel.getOrElse(""), - astParentFullName = scope.surroundingScopeFullName.getOrElse("") + astParentFullName = scope.surroundingScopeFullName + .map { tn => if isSingletonConstructor then s"$tn" else tn } + .getOrElse("") ), typeRefNode(node, methodName, fullName), methodRefNode(node, methodName, fullName, methodReturn.typeFullName) @@ -94,13 +100,14 @@ trait AstForFunctionsCreator(implicit withSchemaValidation: ValidationMode) { th val baseStmtBlockAst = astForMethodBody(node.body, optionalStatementList) transformAsClosureBody(refs, baseStmtBlockAst) } else { - if (methodName != XDefines.ConstructorMethodName && node.methodName != XDefines.StaticInitMethodName) { + if (methodName != Defines.Initialize && methodName != Defines.InitializeClass) { astForMethodBody(node.body, optionalStatementList) } else { astForConstructorMethodBody(node.body, optionalStatementList) } } + // For yield statements where there isn't an explicit proc parameter val anonProcParam = scope.anonProcParam.map { param => val paramNode = ProcParameter(param)(node.span.spanStart(s"&$param")) val nextIndex = @@ -118,18 +125,42 @@ trait AstForFunctionsCreator(implicit withSchemaValidation: ValidationMode) { th val prefixMemberAst = if isClosure || scope.isSurroundedByProgramScope then Ast() // program scope members are set elsewhere - else Ast(memberForMethod(method)) + else { + // Singleton constructors that initialize @@ fields should have their members linked under the singleton class + val methodMember = scope.surroundingTypeFullName.map { + case x if isSingletonConstructor => s"$x" + case x => x + } match { + case Some(astParentTfn) => memberForMethod(method, Option(NodeTypes.TYPE_DECL), Option(astParentTfn)) + case None => memberForMethod(method) + } + if (isSingletonConstructor) { + diffGraph.addNode(methodMember) + Ast() + } else { + Ast(memberForMethod(method)) + } + } val prefixRefAssignAst = if isClosure then Ast() else createMethodRefPointer(method) // For closures, we also want the method/type refs for upstream use val suffixAsts = if isClosure then refs else refs.filter(_.root.exists(_.isInstanceOf[NewTypeDecl])) - val methodAsts = prefixMemberAst :: prefixRefAssignAst :: - methodAst( + val methodAst_ = { + val mAst = methodAst( method, parameterAsts ++ anonProcParam, stmtBlockAst, methodReturn, modifiers.map(newModifierNode).toSeq - ) :: suffixAsts + ) + // AstLinker will link the singleton as the parent + if isSingletonConstructor then { + Ast.storeInDiffGraph(mAst, diffGraph) + Ast() + } else { + mAst + } + } + val methodAsts = prefixMemberAst :: prefixRefAssignAst :: methodAst_ :: suffixAsts methodAsts.filterNot(_.root.isEmpty) } @@ -177,7 +208,7 @@ trait AstForFunctionsCreator(implicit withSchemaValidation: ValidationMode) { th /** Creates the bindings between the method and its types. This is useful for resolving function pointers and imports. */ - private def createMethodTypeBindings(method: NewMethod, refs: List[Ast]): Unit = { + protected def createMethodTypeBindings(method: NewMethod, refs: List[Ast]): Unit = { refs.flatMap(_.root).collectFirst { case typeRef: NewTypeDecl => val bindingNode = newBindingNode("", "", method.fullName) diffGraph.addEdge(typeRef, bindingNode, EdgeTypes.BINDS) @@ -294,8 +325,9 @@ trait AstForFunctionsCreator(implicit withSchemaValidation: ValidationMode) { th } // This will link the type decl to the surrounding context via base overlays - val typeDeclAst = astForClassDeclaration(node).last + val Seq(_, typeDeclAst, singletonAsts) = astForClassDeclaration(node).take(3) Ast.storeInDiffGraph(typeDeclAst, diffGraph) + Ast.storeInDiffGraph(singletonAsts, diffGraph) typeDeclAst.nodes .collectFirst { case typeDecl: NewTypeDecl => @@ -310,7 +342,7 @@ trait AstForFunctionsCreator(implicit withSchemaValidation: ValidationMode) { th } protected def astForSingletonMethodDeclaration(node: SingletonMethodDeclaration): Seq[Ast] = { - node.target match + node.target match { case targetNode: SingletonMethodIdentifier => val fullName = computeMethodFullName(node.methodName) @@ -321,7 +353,7 @@ trait AstForFunctionsCreator(implicit withSchemaValidation: ValidationMode) { th val baseType = node.target.span.text scope.surroundingTypeFullName.map(_.split("[.]").last) match { case Some(typ) if typ == baseType => - (scope.surroundingAstLabel, scope.surroundingTypeFullName, baseType, false) + (scope.surroundingAstLabel, scope.surroundingScopeFullName, baseType, false) case Some(typ) => scope.tryResolveTypeReference(baseType) match { case Some(typ) => @@ -366,6 +398,10 @@ trait AstForFunctionsCreator(implicit withSchemaValidation: ValidationMode) { th scope.popScope() + // The member for these types refers to the singleton class + val member = memberForMethod(method, Option(NodeTypes.TYPE_DECL), astParentFullName.map(x => s"$x")) + diffGraph.addNode(member) + val _methodAst = methodAst( method, @@ -384,6 +420,7 @@ trait AstForFunctionsCreator(implicit withSchemaValidation: ValidationMode) { th s"Target node type for singleton method declarations are not supported yet: ${targetNode.text} (${targetNode.getClass.getSimpleName}), skipping" ) astForUnknown(node) :: Nil + } } private def createMethodRefPointer(method: NewMethod): Ast = { 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 660645978ff4..e6ca91c02902 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 @@ -3,15 +3,17 @@ package io.joern.rubysrc2cpg.astcreation 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.shiftleft.codepropertygraph.generated.{DispatchTypes, EvaluationStrategies, Operators} +import io.shiftleft.codepropertygraph.generated.{DispatchTypes, EvaluationStrategies, ModifierTypes, Operators} import scala.collection.immutable.List @@ -65,39 +67,77 @@ trait AstForTypesCreator(implicit withSchemaValidation: ValidationMode) { this: inherits = inheritsFrom, alias = None ) + /* + In Ruby, there are semantic differences between the ordinary class and singleton class (think "meta" class in + Python). Similar to how Java allows both static and dynamic methods/fields/etc. within the same type declaration, + Ruby allows `self` methods and @@ fields to be defined alongside ordinary methods and @ fields. However, both + classes are more dynamic and have separate behaviours in Ruby and we model it as such. - node match { - case _: ModuleDeclaration => scope.pushNewScope(ModuleScope(classFullName)) - case _: TypeDeclaration => scope.pushNewScope(TypeScope(classFullName, List.empty)) + To signify the singleton type, we add the tag. + */ + val singletonTypeDecl = typeDecl.copy + .name(s"$className") + .fullName(s"$classFullName") + .inheritsFromTypeFullName(inheritsFrom.map(x => s"$x")) + + val (typeDeclModifiers, singletonModifiers) = node match { + case _: ModuleDeclaration => + scope.pushNewScope(ModuleScope(classFullName)) + ( + ModifierTypes.VIRTUAL :: Nil map newModifierNode map Ast.apply, + ModifierTypes.VIRTUAL :: ModifierTypes.FINAL :: Nil map newModifierNode map Ast.apply + ) + case _: TypeDeclaration => + scope.pushNewScope(TypeScope(classFullName, List.empty)) + ( + ModifierTypes.VIRTUAL :: Nil map newModifierNode map Ast.apply, + ModifierTypes.VIRTUAL :: Nil map newModifierNode map Ast.apply + ) } val classBody = node.body.asInstanceOf[StatementList] // for now (bodyStatement is a superset of stmtList) - val classBodyAsts = classBody.statements.flatMap(astsForStatement) match { + val classBodyAsts = classBody.statements.flatMap { + case n: SingletonMethodDeclaration => + val singletonMethodAst = astsForStatement(n) + // Create binding from singleton methods to singleton type decls + singletonMethodAst.flatMap(_.root).collectFirst { case n: NewMethod => + createMethodTypeBindings(n, Ast(singletonTypeDecl) :: Nil) + } + // Method declaration remains in the normal type decl body + singletonMethodAst + case n => astsForStatement(n) + } match { case bodyAsts if scope.shouldGenerateDefaultConstructor && this.parseLevel == AstParseLevel.FULL_AST => - val bodyStart = classBody.span.spanStart() - val initBody = StatementList(List())(bodyStart) - val methodDecl = astForMethodDeclaration( - MethodDeclaration(XDefines.ConstructorMethodName, List(), initBody)(bodyStart) - ) + val bodyStart = classBody.span.spanStart() + val initBody = StatementList(List())(bodyStart) + val methodDecl = astForMethodDeclaration(MethodDeclaration(Defines.Initialize, List(), initBody)(bodyStart)) methodDecl ++ bodyAsts case bodyAsts => bodyAsts } - val fieldMemberNodes = node match { + val (fieldTypeMemberNodes, fieldSingletonMemberNodes) = node match { case classDecl: ClassDeclaration => - classDecl.fields.map { x => - val name = code(x) - Ast(memberNode(x, name, name, Defines.Any)) - } - case _ => Seq.empty + classDecl.fields + .map { x => + val name = code(x) + x.isInstanceOf[InstanceFieldIdentifier] -> Ast(memberNode(x, name, name, Defines.Any)) + } + .partition(_._1) + case _ => Seq.empty -> Seq.empty } scope.popScope() - val prefixAst = createTypeRefPointer(typeDecl) - val typeDeclAsts = prefixAst :: Ast(typeDecl).withChildren(fieldMemberNodes).withChildren(classBodyAsts) :: Nil - typeDeclAsts.filterNot(_.root.isEmpty) + val prefixAst = createTypeRefPointer(typeDecl) + val typeDeclAst = Ast(typeDecl) + .withChildren(typeDeclModifiers) + .withChildren(fieldTypeMemberNodes.map(_._2)) + .withChildren(classBodyAsts) + val singletonTypeDeclAst = + Ast(singletonTypeDecl).withChildren(singletonModifiers).withChildren(fieldSingletonMemberNodes.map(_._2)) + + prefixAst :: typeDeclAst :: singletonTypeDeclAst :: Nil filterNot (_.root.isEmpty) } private def createTypeRefPointer(typeDecl: NewTypeDecl): Ast = { diff --git a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/datastructures/RubyProgramSummary.scala b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/datastructures/RubyProgramSummary.scala index 51bb9006b29b..355eb8352b94 100644 --- a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/datastructures/RubyProgramSummary.scala +++ b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/datastructures/RubyProgramSummary.scala @@ -5,6 +5,7 @@ import io.joern.x2cpg.Defines as XDefines import io.joern.x2cpg.datastructures.{FieldLike, MethodLike, ProgramSummary, StubbedType, TypeLike} import io.joern.x2cpg.typestub.{TypeStubMetaData, TypeStubUtil} import org.slf4j.LoggerFactory +import io.joern.rubysrc2cpg.passes.Defines import upickle.default.* import java.io.{ByteArrayInputStream, InputStream} @@ -165,7 +166,7 @@ case class RubyType(name: String, methods: List[RubyMethod], fields: List[RubyFi } def hasConstructor: Boolean = { - methods.exists(_.name == XDefines.ConstructorMethodName) + methods.exists(_.name == Defines.Initialize) } } diff --git a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/parser/RubyNodeCreator.scala b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/parser/RubyNodeCreator.scala index ca3a91851549..996c1b008078 100644 --- a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/parser/RubyNodeCreator.scala +++ b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/parser/RubyNodeCreator.scala @@ -887,16 +887,13 @@ class RubyNodeCreator extends RubyParserBaseVisitor[RubyNode] { val (instanceFieldsInMethodDecls, classFieldsInMethodDecls) = partitionRubyFields(fieldsInMethodDecls) - val initializeMethod = methodDecls.collectFirst { x => - x.methodName match - case "initialize" => x - } + val initializeMethod = methodDecls.collectFirst { case x if x.methodName == Defines.Initialize => x } val initStmtListStatements = genSingleAssignmentStmtList(instanceFields, instanceFieldsInMethodDecls) val clinitStmtList = genSingleAssignmentStmtList(classFields, classFieldsInMethodDecls) val clinitMethod = - MethodDeclaration(XDefines.StaticInitMethodName, List.empty, StatementList(clinitStmtList)(stmtList.span))( + MethodDeclaration(Defines.InitializeClass, List.empty, StatementList(clinitStmtList)(stmtList.span))( stmtList.span ) @@ -914,7 +911,7 @@ class RubyNodeCreator extends RubyParserBaseVisitor[RubyNode] { } case None => val newInitMethod = - MethodDeclaration("initialize", List.empty, StatementList(initStmtListStatements)(stmtList.span))( + MethodDeclaration(Defines.Initialize, List.empty, StatementList(initStmtListStatements)(stmtList.span))( stmtList.span ) val initializers = newInitMethod :: clinitMethod :: Nil diff --git a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/passes/Defines.scala b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/passes/Defines.scala index 2c620a8956c2..e2a453d333de 100644 --- a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/passes/Defines.scala +++ b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/passes/Defines.scala @@ -2,25 +2,27 @@ package io.joern.rubysrc2cpg.passes object Defines { - val Any: String = "ANY" - val Undefined: String = "Undefined" - val Object: String = "Object" - val NilClass: String = "NilClass" - val TrueClass: String = "TrueClass" - val FalseClass: String = "FalseClass" - val Numeric: String = "Numeric" - val Integer: String = "Integer" - val Float: String = "Float" - val String: String = "String" - val Symbol: String = "Symbol" - val Array: String = "Array" - val Hash: String = "Hash" - val Encoding: String = "Encoding" - val Regexp: String = "Regexp" - val Lambda: String = "lambda" - val Proc: String = "proc" - val Loop: String = "loop" - val Self: String = "self" + val Any: String = "ANY" + val Undefined: String = "Undefined" + val Object: String = "Object" + val NilClass: String = "NilClass" + val TrueClass: String = "TrueClass" + val FalseClass: String = "FalseClass" + val Numeric: String = "Numeric" + val Integer: String = "Integer" + val Float: String = "Float" + val String: String = "String" + val Symbol: String = "Symbol" + val Array: String = "Array" + val Hash: String = "Hash" + val Encoding: String = "Encoding" + val Regexp: String = "Regexp" + val Lambda: String = "lambda" + val Proc: String = "proc" + val Loop: String = "loop" + val Self: String = "self" + val Initialize: String = "initialize" + val InitializeClass: String = "initialize" // simply contains the @@ field initialization val Program: String = ":program" diff --git a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/passes/RubyTypeRecoveryPassGenerator.scala b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/passes/RubyTypeRecoveryPassGenerator.scala index 826de5d7fdb2..dc89e234e9a8 100644 --- a/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/passes/RubyTypeRecoveryPassGenerator.scala +++ b/joern-cli/frontends/rubysrc2cpg/src/main/scala/io/joern/rubysrc2cpg/passes/RubyTypeRecoveryPassGenerator.scala @@ -38,7 +38,7 @@ private class RecoverForRubyFile(cpg: Cpg, cu: File, builder: DiffGraphBuilder, /** A heuristic method to determine if a call name is a constructor or not. */ override protected def isConstructor(name: String): Boolean = - !name.isBlank && (name == "new" || name == XDefines.ConstructorMethodName) + !name.isBlank && (name == "new" || name == Defines.Initialize) override protected def hasTypes(node: AstNode): Boolean = node match { case x: Call if !x.methodFullName.startsWith("") => diff --git a/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/CallTests.scala b/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/CallTests.scala index f104b6a1b8da..7630f499e237 100644 --- a/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/CallTests.scala +++ b/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/CallTests.scala @@ -4,7 +4,6 @@ import io.joern.rubysrc2cpg.passes.{GlobalTypes, Defines as RubyDefines} import io.joern.rubysrc2cpg.passes.Defines.RubyOperators import io.joern.rubysrc2cpg.passes.GlobalTypes.kernelPrefix import io.joern.rubysrc2cpg.testfixtures.RubyCode2CpgFixture -import io.joern.x2cpg.Defines import io.shiftleft.codepropertygraph.generated.nodes.* import io.shiftleft.codepropertygraph.generated.{DispatchTypes, Operators} import io.shiftleft.semanticcpg.language.* @@ -181,7 +180,7 @@ class CallTests extends RubyCode2CpgFixture { } "create a call to the object's constructor, with the temp variable receiver" in { - inside(cpg.call.nameExact(Defines.ConstructorMethodName).l) { + inside(cpg.call.nameExact("new").l) { case constructor :: Nil => inside(constructor.argument.l) { case (a: Identifier) :: Nil => 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 eca614013217..d9f13018e0fe 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 @@ -29,8 +29,16 @@ class ClassTests extends RubyCode2CpgFixture { classC.fullName shouldBe "Test0.rb:::program.C" classC.lineNumber shouldBe Some(2) classC.baseType.l shouldBe List() - classC.member.name.l shouldBe List("", "") - classC.method.name.l shouldBe List("", "") + classC.member.name.l shouldBe List(RubyDefines.Initialize) + classC.method.name.l shouldBe List(RubyDefines.Initialize) + + val List(singletonC) = cpg.typeDecl.nameExact("C").l + singletonC.inheritsFromTypeFullName shouldBe List() + singletonC.fullName shouldBe "Test0.rb:::program.C" + singletonC.lineNumber shouldBe Some(2) + singletonC.baseType.l shouldBe List() + singletonC.member.name.l shouldBe List(RubyDefines.Initialize) + singletonC.method.name.l shouldBe List(RubyDefines.Initialize) } "`class C < D` is represented by a TYPE_DECL node inheriting from `D`" in { @@ -45,11 +53,19 @@ class ClassTests extends RubyCode2CpgFixture { classC.inheritsFromTypeFullName shouldBe List("D") classC.fullName shouldBe "Test0.rb:::program.C" classC.lineNumber shouldBe Some(2) - classC.member.name.l shouldBe List("", "") - classC.method.name.l shouldBe List("", "") + classC.member.name.l shouldBe List(RubyDefines.Initialize) + classC.method.name.l shouldBe List(RubyDefines.Initialize) val List(typeD) = classC.baseType.l typeD.name shouldBe "D" + + val List(singletonC) = cpg.typeDecl.nameExact("C").l + + singletonC.inheritsFromTypeFullName shouldBe List("D") + singletonC.fullName shouldBe "Test0.rb:::program.C" + singletonC.lineNumber shouldBe Some(2) + singletonC.member.name.l shouldBe List(RubyDefines.Initialize) + singletonC.method.name.l shouldBe List(RubyDefines.Initialize) } "`attr_reader :a` is represented by a `@a` MEMBER node" in { @@ -64,6 +80,9 @@ class ClassTests extends RubyCode2CpgFixture { aMember.code shouldBe "attr_reader :a" aMember.lineNumber shouldBe Some(3) + + val List(singletonC) = cpg.typeDecl.name("C").l + singletonC.member.nameExact("@a").isEmpty shouldBe true } "`attr_reader :'abc'` is represented by a `@abc` MEMBER node" in { @@ -185,7 +204,7 @@ class ClassTests extends RubyCode2CpgFixture { memberF.dynamicTypeHintFullName.toSet should contain(methodF.fullName) } - "`def initialize() ... end` directly inside a class has method name ``" in { + "`def initialize() ... end` directly inside a class has the constructor modifier" in { val cpg = code(""" |class C | def initialize() @@ -194,9 +213,10 @@ class ClassTests extends RubyCode2CpgFixture { |""".stripMargin) val List(classC) = cpg.typeDecl.name("C").l - val List(methodInit) = classC.method.name("").l + val List(methodInit) = classC.method.name(RubyDefines.Initialize).l - methodInit.fullName shouldBe "Test0.rb:::program.C:" + methodInit.fullName shouldBe s"Test0.rb:::program.C:${RubyDefines.Initialize}" + methodInit.isConstructor.isEmpty shouldBe false } "`class C end` has default constructor" in { @@ -206,12 +226,12 @@ class ClassTests extends RubyCode2CpgFixture { |""".stripMargin) val List(classC) = cpg.typeDecl.name("C").l - val List(methodInit) = classC.method.name("").l + val List(methodInit) = classC.method.name(RubyDefines.Initialize).l - methodInit.fullName shouldBe "Test0.rb:::program.C:" + methodInit.fullName shouldBe s"Test0.rb:::program.C:${RubyDefines.Initialize}" } - "`def initialize() ... end` not directly under class has method name `initialize`" in { + "only `def initialize() ... end` directly under class has the constructor modifier" in { val cpg = code(""" |def initialize() | 1 @@ -232,7 +252,7 @@ class ClassTests extends RubyCode2CpgFixture { |end |""".stripMargin) - cpg.method.name("").literal.code.l should be(empty) + cpg.method.nameExact(RubyDefines.Initialize).where(_.isConstructor).literal.code.l should be(empty) } "a basic anonymous class" should { @@ -251,8 +271,8 @@ class ClassTests extends RubyCode2CpgFixture { anonClass.fullName shouldBe "Test0.rb:::program." inside(anonClass.method.l) { case defaultConstructor :: hello :: Nil => - defaultConstructor.name shouldBe Defines.ConstructorMethodName - defaultConstructor.fullName shouldBe s"Test0.rb:::program.:${Defines.ConstructorMethodName}" + defaultConstructor.name shouldBe RubyDefines.Initialize + defaultConstructor.fullName shouldBe s"Test0.rb:::program.:${RubyDefines.Initialize}" hello.name shouldBe "hello" hello.fullName shouldBe "Test0.rb:::program.:hello" @@ -266,7 +286,6 @@ class ClassTests extends RubyCode2CpgFixture { inside(cpg.method(":program").assignment.l) { case aAssignment :: Nil => aAssignment.target.code shouldBe "a" - // TODO: Constructors are not supported, we simply check the `code` property aAssignment.source.code shouldBe "Class.new" case xs => fail(s"Expected a single assignment, but got [${xs.map(x => x.label -> x.code).mkString(",")}]") } @@ -274,7 +293,8 @@ class ClassTests extends RubyCode2CpgFixture { } - "a basic singleton class" should { + // TODO: This should be remodelled as a property access `animal.bark = METHOD_REF` + "a basic singleton class" ignore { val cpg = code("""class Animal; end |animal = Animal.new | @@ -326,7 +346,7 @@ class ClassTests extends RubyCode2CpgFixture { |""".stripMargin) inside(cpg.typeDecl.name("User").l) { case userType :: Nil => - inside(userType.method.name(Defines.ConstructorMethodName).l) { + inside(userType.method.name(RubyDefines.Initialize).l) { case constructor :: Nil => inside(constructor.astChildren.isBlock.l) { case methodBlock :: Nil => @@ -360,7 +380,7 @@ class ClassTests extends RubyCode2CpgFixture { inside(cpg.typeDecl.name("AdminController").l) { case adminTypeDecl :: Nil => - inside(adminTypeDecl.method.name(Defines.ConstructorMethodName).l) { + inside(adminTypeDecl.method.name(RubyDefines.Initialize).l) { case constructor :: Nil => inside(constructor.astChildren.isBlock.l) { case methodBlock :: Nil => @@ -462,7 +482,7 @@ class ClassTests extends RubyCode2CpgFixture { "create nil assignments under the class initializer" in { inside(cpg.typeDecl.name("Foo").l) { case fooType :: Nil => - inside(fooType.method.name(Defines.ConstructorMethodName).l) { + inside(fooType.method.name(RubyDefines.Initialize).l) { case initMethod :: Nil => inside(initMethod.block.astChildren.isCall.name(Operators.assignment).l) { case aAssignment :: bAssignment :: cAssignment :: dAssignment :: oAssignment :: Nil => @@ -520,7 +540,7 @@ class ClassTests extends RubyCode2CpgFixture { |""".stripMargin) "create respective member nodes" in { - inside(cpg.typeDecl.name("Foo").l) { + inside(cpg.typeDecl.nameExact("Foo").l) { case fooType :: Nil => inside(fooType.member.name("@.*").l) { case aMember :: bMember :: cMember :: dMember :: oMember :: Nil => @@ -537,9 +557,9 @@ class ClassTests extends RubyCode2CpgFixture { } "create nil assignments under the class initializer" in { - inside(cpg.typeDecl.name("Foo").l) { + inside(cpg.typeDecl.name("Foo").l) { case fooType :: Nil => - inside(fooType.method.name(Defines.StaticInitMethodName).l) { + inside(fooType.method.name(RubyDefines.Initialize).l) { case clinitMethod :: Nil => inside(clinitMethod.block.astChildren.isCall.name(Operators.assignment).l) { case aAssignment :: bAssignment :: cAssignment :: dAssignment :: oAssignment :: Nil => @@ -621,7 +641,7 @@ class ClassTests extends RubyCode2CpgFixture { "be moved to constructor method" in { inside(cpg.typeDecl.name("Foo").l) { case fooClass :: Nil => - inside(fooClass.method.name(Defines.ConstructorMethodName).l) { + inside(fooClass.method.name(RubyDefines.Initialize).l) { case initMethod :: Nil => inside(initMethod.astChildren.isBlock.astChildren.isCall.l) { case scopeCall :: Nil => @@ -655,7 +675,7 @@ class ClassTests extends RubyCode2CpgFixture { "correct method full name for method ref under call" in { inside(cpg.typeDecl.name("Foo").l) { case fooClass :: Nil => - inside(fooClass.method.name(Defines.ConstructorMethodName).l) { + inside(fooClass.method.name(RubyDefines.Initialize).l) { case initMethod :: Nil => inside(initMethod.astChildren.isBlock.l) { case methodBlock :: Nil => @@ -666,7 +686,7 @@ class ClassTests extends RubyCode2CpgFixture { base.code shouldBe "self.scope" self.name shouldBe "self" literal.code shouldBe ":hits_by_ip" - methodRef.methodFullName shouldBe "Test0.rb:::program.Foo::0" + methodRef.methodFullName shouldBe s"Test0.rb:::program.Foo:${RubyDefines.Initialize}:0" methodRef.referencedMethod.parameter.indexGt(0).name.l shouldBe List("ip", "col") case xs => fail(s"Expected three children, got ${xs.code.mkString(", ")} instead") } @@ -680,14 +700,14 @@ class ClassTests extends RubyCode2CpgFixture { } } - "correct method def under block" in { - inside(cpg.typeDecl.name("Foo").l) { + "correct method def under initialize method" in { + inside(cpg.typeDecl.name("Foo").l) { case fooClass :: Nil => - inside(fooClass.method.name(Defines.ConstructorMethodName).l) { + inside(fooClass.method.name(RubyDefines.Initialize).l) { case initMethod :: Nil => inside(initMethod.astChildren.isMethod.l) { case lambdaMethod :: Nil => - lambdaMethod.fullName shouldBe "Test0.rb:::program.Foo::0" + lambdaMethod.fullName shouldBe s"Test0.rb:::program.Foo:${RubyDefines.Initialize}:0" case xs => fail(s"Expected method decl for lambda, got ${xs.code.mkString(", ")} instead") } case xs => fail(s"Expected one init method, got ${xs.code.mkString(", ")} instead") diff --git a/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/DependencyTests.scala b/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/DependencyTests.scala index 8eac8c7d9d3b..d868692aad06 100644 --- a/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/DependencyTests.scala +++ b/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/DependencyTests.scala @@ -94,9 +94,9 @@ class DownloadDependencyTest extends RubyCode2CpgFixture(downloadDependencies = case (v: Identifier) :: (block: Block) :: Nil => v.dynamicTypeHintFullName should contain("dummy_logger.rb:::program.Main_module.Main_outer_class") - inside(block.astChildren.isCall.name(Defines.ConstructorMethodName).headOption) { + inside(block.astChildren.isCall.nameExact("new").headOption) { case Some(constructorCall) => - constructorCall.methodFullName shouldBe "dummy_logger.rb:::program.Main_module.Main_outer_class:" + constructorCall.methodFullName shouldBe s"dummy_logger.rb:::program.Main_module.Main_outer_class:${RubyDefines.Initialize}" case None => fail(s"Expected constructor call, did not find one") } case xs => fail(s"Expected two arguments under the constructor assignment, got [${xs.code.mkString(", ")}]") @@ -113,7 +113,7 @@ class DownloadDependencyTest extends RubyCode2CpgFixture(downloadDependencies = inside(block.astChildren.isCall.name(Defines.ConstructorMethodName).headOption) { case Some(constructorCall) => - constructorCall.methodFullName shouldBe "utils/help.rb:::program.Help:" + constructorCall.methodFullName shouldBe "utils/help.rb:::program.Help:initialize" case None => fail(s"Expected constructor call, did not find one") } case xs => fail(s"Expected two arguments under the constructor assignment, got [${xs.code.mkString(", ")}]") 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 7a8bfd6322a6..d944062f5b8c 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 @@ -250,8 +250,8 @@ class DoBlockTests extends RubyCode2CpgFixture { tmpLocal.name shouldBe "" tmpAssign.code shouldBe " = Array.new(x) { |i| i += 1 }" - newCall.name shouldBe Defines.ConstructorMethodName - newCall.methodFullName shouldBe s"$kernelPrefix.Array:" + newCall.name shouldBe "new" + newCall.methodFullName shouldBe s"$kernelPrefix.Array:initialize" inside(newCall.argument.l) { case (_: Identifier) :: (x: Identifier) :: (closure: MethodRef) :: Nil => diff --git a/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/ImportTests.scala b/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/ImportTests.scala index a7b627185f01..7e4981e8f44c 100644 --- a/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/ImportTests.scala +++ b/joern-cli/frontends/rubysrc2cpg/src/test/scala/io/joern/rubysrc2cpg/querying/ImportTests.scala @@ -59,7 +59,7 @@ class ImportTests extends RubyCode2CpgFixture with Inspectors { ) val List(newCall) = - cpg.method.name(":program").filename("t1.rb").ast.isCall.methodFullName(".*:").methodFullName.l + cpg.method.name(":program").filename("t1.rb").ast.isCall.methodFullName(".*:initialize").methodFullName.l newCall should startWith(s"${path}.rb:") } } @@ -173,7 +173,7 @@ class ImportTests extends RubyCode2CpgFixture with Inspectors { case csvParseCall :: csvTableInitCall :: ppCall :: Nil => csvParseCall.methodFullName shouldBe "csv.CSV:parse" ppCall.methodFullName shouldBe "pp.PP:pp" - csvTableInitCall.methodFullName shouldBe "csv.CSV.Table:" + csvTableInitCall.methodFullName shouldBe "csv.CSV.Table:initialize" case xs => fail(s"Expected three calls, got [${xs.code.mkString(",")}] instead") } } 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 6b0e423e792f..9ed1d8e58dc8 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 @@ -1,20 +1,10 @@ package io.joern.rubysrc2cpg.querying -import io.joern.rubysrc2cpg.testfixtures.RubyCode2CpgFixture -import io.joern.x2cpg.Defines import io.joern.rubysrc2cpg.passes.Defines as RDefines import io.joern.rubysrc2cpg.passes.GlobalTypes.kernelPrefix -import io.shiftleft.codepropertygraph.generated.{ControlStructureTypes, ModifierTypes, NodeTypes, Operators} -import io.shiftleft.codepropertygraph.generated.nodes.{ - Call, - Identifier, - Literal, - Method, - MethodRef, - Return, - TypeDecl, - TypeRef -} +import io.joern.rubysrc2cpg.testfixtures.RubyCode2CpgFixture +import io.shiftleft.codepropertygraph.generated.nodes.* +import io.shiftleft.codepropertygraph.generated.{ControlStructureTypes, NodeTypes, Operators} import io.shiftleft.semanticcpg.language.* class MethodTests extends RubyCode2CpgFixture { @@ -317,7 +307,7 @@ class MethodTests extends RubyCode2CpgFixture { "create a method under `Foo` for both `x=`, `x`, and `bar=`, where `bar=` forwards parameters to a call to `x=`" in { inside(cpg.typeDecl("Foo").l) { case foo :: Nil => - inside(foo.method.nameNot(Defines.ConstructorMethodName, Defines.StaticInitMethodName).l) { + inside(foo.method.nameNot(RDefines.Initialize).l) { case xeq :: x :: bar :: Nil => xeq.name shouldBe "x=" x.name shouldBe "x" @@ -386,6 +376,11 @@ class MethodTests extends RubyCode2CpgFixture { } } + "have bindings to the singleton module TYPE_DECL" in { + // Note: we cannot bind baz as this is a dynamic assignment to `F` which is trickier to determine + cpg.typeDecl.name("F").methodBinding.methodFullName.l shouldBe List("Test0.rb:::program.F:bar") + } + "baz should not exist in the :program block" in { inside(cpg.method.name(":program").l) { case prog :: Nil => @@ -666,7 +661,7 @@ class MethodTests extends RubyCode2CpgFixture { "be placed directly before each entity's definition" in { inside(cpg.method.name(RDefines.Program).filename("t1.rb").block.astChildren.l) { - case (a1: Call) :: (_: TypeDecl) :: (a2: Call) :: (_: TypeDecl) :: (a3: Call) :: (_: Method) :: (_: TypeDecl) :: Nil => + case (a1: Call) :: (_: TypeDecl) :: (_: TypeDecl) :: (a2: Call) :: (_: TypeDecl) :: (_: TypeDecl) :: (a3: Call) :: (_: Method) :: (_: TypeDecl) :: Nil => a1.code shouldBe "self.A = class A (...)" a2.code shouldBe "self.B = class B (...)" a3.code shouldBe "self.c = def c (...)"