diff --git a/joern-cli/frontends/php2cpg/src/main/scala/io/joern/php2cpg/astcreation/AstCreator.scala b/joern-cli/frontends/php2cpg/src/main/scala/io/joern/php2cpg/astcreation/AstCreator.scala index 8ad2b4c0750e..7f2ac72cff20 100644 --- a/joern-cli/frontends/php2cpg/src/main/scala/io/joern/php2cpg/astcreation/AstCreator.scala +++ b/joern-cli/frontends/php2cpg/src/main/scala/io/joern/php2cpg/astcreation/AstCreator.scala @@ -288,17 +288,20 @@ object AstCreator { } object NameConstants { - val Default: String = "default" - val HaltCompiler: String = "__halt_compiler" - val This: String = "this" - val Self: String = "self" - val Unknown: String = "UNKNOWN" - val Closure: String = "__closure" - val Class: String = "class" - val True: String = "true" - val False: String = "false" - val NullName: String = "null" - val Invoke: String = "__invoke" + val Default: String = "default" + val HaltCompiler: String = "__halt_compiler" + val This: String = "this" + val Self: String = "self" + val Unknown: String = "UNKNOWN" + val Closure: String = "__closure" + val Class: String = "class" + val True: String = "true" + val False: String = "false" + val NullName: String = "null" + val Invoke: String = "__invoke" + val Static: String = "static" + val Parent: String = "parent" + val StaticReceiver: String = "" def isBoolean(name: String): Boolean = { List(True, False).contains(name) diff --git a/joern-cli/frontends/php2cpg/src/main/scala/io/joern/php2cpg/astcreation/AstCreatorHelper.scala b/joern-cli/frontends/php2cpg/src/main/scala/io/joern/php2cpg/astcreation/AstCreatorHelper.scala index 56202527bb81..e65e96562cd3 100644 --- a/joern-cli/frontends/php2cpg/src/main/scala/io/joern/php2cpg/astcreation/AstCreatorHelper.scala +++ b/joern-cli/frontends/php2cpg/src/main/scala/io/joern/php2cpg/astcreation/AstCreatorHelper.scala @@ -90,6 +90,9 @@ trait AstCreatorHelper(disableFileContent: Boolean)(implicit withSchemaValidatio protected def getTypeDeclPrefix: Option[String] = scope.getEnclosingTypeDeclTypeName.filterNot(_ == NamespaceTraversal.globalNamespaceName) + protected def getInheritedTypeFullName: Option[String] = + scope.getEnclosingTypeDecl.flatMap(_.inheritsFromTypeFullName.headOption) + protected def codeForMethodCall(call: PhpCallExpr, targetAst: Ast, name: String): String = { val callOperator = if (call.isNullSafe) s"?$InstanceMethodDelimiter" else InstanceMethodDelimiter s"${targetAst.rootCodeOrEmpty}$callOperator$name" @@ -277,10 +280,20 @@ trait AstCreatorHelper(disableFileContent: Boolean)(implicit withSchemaValidatio .last } + protected def getSimpleName(fullName: String): String = fullName.split("\\\\").last + protected def getMfn(call: PhpCallExpr, name: String): String = { - lazy val default = s"$UnresolvedNamespace$MethodDelimiter$name" - lazy val maybeResolvedFunction = scope.resolveFunctionIdentifier(name) + lazy val default = s"$UnresolvedNamespace$MethodDelimiter$name" + lazy val maybeResolvedFunction = scope.resolveFunctionIdentifier(name) + lazy val maybeInheritedTypeFullName = getInheritedTypeFullName call.target match { + case Some(nameExpr: PhpNameExpr) if call.isStatic && nameExpr.name == NameConstants.Static => + // static:: late static binding call handled separately as we consider it a dynamic call + composeMethodFullNameForCall(name) + case Some(nameExpr: PhpNameExpr) + if call.isStatic && nameExpr.name == NameConstants.Parent && maybeInheritedTypeFullName.isDefined => + // Static parent:: method call + s"${maybeInheritedTypeFullName.get}$MethodDelimiter$name" case Some(nameExpr: PhpNameExpr) if call.isStatic => // Static method call with a simple receiver if (nameExpr.name == NameConstants.Self) diff --git a/joern-cli/frontends/php2cpg/src/main/scala/io/joern/php2cpg/astcreation/AstForExpressionsCreator.scala b/joern-cli/frontends/php2cpg/src/main/scala/io/joern/php2cpg/astcreation/AstForExpressionsCreator.scala index a6e27dfcd4fa..af66afcf5eaf 100644 --- a/joern-cli/frontends/php2cpg/src/main/scala/io/joern/php2cpg/astcreation/AstForExpressionsCreator.scala +++ b/joern-cli/frontends/php2cpg/src/main/scala/io/joern/php2cpg/astcreation/AstForExpressionsCreator.scala @@ -84,8 +84,11 @@ trait AstForExpressionsCreator(implicit withSchemaValidation: ValidationMode) { call.target match { case None if isCallOnVariable(call) => astForDynamicCall(call, name, arguments, None) case None => astForStaticCall(call, name, arguments) - case _ if call.isStatic => astForStaticCall(call, name, arguments) - case maybeTarget => astForDynamicCall(call, name, arguments, maybeTarget) + case maybeTarget @ Some(expr: PhpNameExpr) if call.isStatic && expr.name == NameConstants.Static => + // Late static binding calls (static::foo()) are dynamic calls resolved at runtime. + astForDynamicCall(call, name, arguments, maybeTarget, isLateStaticBindingCall = true) + case _ if call.isStatic => astForStaticCall(call, name, arguments) + case maybeTarget => astForDynamicCall(call, name, arguments, maybeTarget) } } @@ -93,12 +96,22 @@ trait AstForExpressionsCreator(implicit withSchemaValidation: ValidationMode) { call: PhpCallExpr, name: String, arguments: Seq[Ast], - maybeTarget: Option[PhpExpr] + maybeTarget: Option[PhpExpr], + isLateStaticBindingCall: Boolean = false ): Ast = { - val argsCode = getArgsCode(call, arguments) - val targetAst = maybeTarget.map(astForExpr) - val codePrefix = targetAst.map(codeForMethodCall(call, _, name)).getOrElse(name) - val code = s"$codePrefix($argsCode)" + val argsCode = getArgsCode(call, arguments) + val targetAst = if (isLateStaticBindingCall) { + maybeTarget.flatMap(astForClassScopeResolutionTarget) + } else { + maybeTarget.map(astForExpr) + } + + val codePrefix = if (isLateStaticBindingCall && targetAst.isDefined) { + s"${NameConstants.Static}::$name" + } else { + targetAst.map(codeForMethodCall(call, _, name)).getOrElse(name) + } + val code = s"$codePrefix($argsCode)" val dispatchType = DispatchTypes.DYNAMIC_DISPATCH @@ -126,6 +139,19 @@ trait AstForExpressionsCreator(implicit withSchemaValidation: ValidationMode) { } + private def astForClassScopeResolutionTarget(expr: PhpExpr): Option[Ast] = { + expr match { + case t: PhpNameExpr => + scope.surroundingMethodReceiver.map { recv => + val target = t.copy(name = recv) + astForNameExpr(target, code = Some(recv)) + } + case expr => + logger.warn(s"Expected a PhpNameExpr target but got $expr.") + None + } + } + private def astForStaticCall(call: PhpCallExpr, name: String, arguments: Seq[Ast]): Ast = { val argsCode = getArgsCode(call, arguments) val codePrefix = codeForStaticMethodCall(call, name) @@ -139,7 +165,17 @@ trait AstForExpressionsCreator(implicit withSchemaValidation: ValidationMode) { case _ => getMfn(call, name) } + val targetArgument = call.target.collect { + case nameExpr: PhpNameExpr if Set(NameConstants.Parent, NameConstants.Self).contains(nameExpr.name) => + astForClassScopeResolutionTarget(nameExpr) + case nameExpr: PhpNameExpr => + val typ = typeRefNode(nameExpr, getSimpleName(nameExpr.name), nameExpr.name) + Some(Ast(typ)) + }.flatten + val staticReceiver = call.target.collect { + case nameExpr: PhpNameExpr if nameExpr.name == NameConstants.Parent => + getInheritedTypeFullName case nameExpr: PhpNameExpr if nameExpr.name == NameConstants.Self => getTypeDeclPrefix case nameExpr: PhpNameExpr => @@ -148,7 +184,9 @@ trait AstForExpressionsCreator(implicit withSchemaValidation: ValidationMode) { val callRoot = callNode(call, code, name, fullName, dispatchType, None, Some(Defines.Any), staticReceiver) - callAst(callRoot, arguments) + val allArgs = List(targetArgument, arguments).flatten + val startArgumentIndex = Option.when(targetArgument.isDefined)(0) + staticCallAst(callRoot, allArgs, startArgumentIndex = startArgumentIndex) } protected def simpleAssignAst(origin: PhpNode, target: Ast, source: Ast): Ast = { @@ -697,8 +735,8 @@ trait AstForExpressionsCreator(implicit withSchemaValidation: ValidationMode) { valueAst } - private def astForNameExpr(expr: PhpNameExpr): Ast = { - val identifier = identifierNode(expr, expr.name, expr.name, Defines.Any) + private def astForNameExpr(expr: PhpNameExpr, code: Option[String] = None): Ast = { + val identifier = identifierNode(expr, expr.name, code.getOrElse(expr.name), Defines.Any) val declaringNode = handleVariableOccurrence(expr, identifier.name) diff --git a/joern-cli/frontends/php2cpg/src/main/scala/io/joern/php2cpg/astcreation/AstForFunctionsCreator.scala b/joern-cli/frontends/php2cpg/src/main/scala/io/joern/php2cpg/astcreation/AstForFunctionsCreator.scala index 1154bcc61f44..854b10c638ca 100644 --- a/joern-cli/frontends/php2cpg/src/main/scala/io/joern/php2cpg/astcreation/AstForFunctionsCreator.scala +++ b/joern-cli/frontends/php2cpg/src/main/scala/io/joern/php2cpg/astcreation/AstForFunctionsCreator.scala @@ -124,7 +124,7 @@ trait AstForFunctionsCreator(implicit withSchemaValidation: ValidationMode) { th val fullName = fullNameOverride.getOrElse(composeMethodFullName(methodName)) val constructorModifier = Option.when(isConstructor)(ModifierTypes.CONSTRUCTOR) - val virtualModifier = Option.unless(isStatic || isConstructor)(ModifierTypes.VIRTUAL) + val virtualModifier = Option.unless(isConstructor)(ModifierTypes.VIRTUAL) val defaultAccessModifier = Option.unless(containsAccessModifier(decl.modifiers))(ModifierTypes.PUBLIC) val allModifiers = virtualModifier ++: constructorModifier ++: defaultAccessModifier ++: decl.modifiers @@ -158,9 +158,13 @@ trait AstForFunctionsCreator(implicit withSchemaValidation: ValidationMode) { th MethodScope(method, methodBodyNode, method.fullName, decl.params.map(_.name), methodRef, isArrowClosure) ) + val staticReceiver = Option.when(decl.isClassMethod && isStatic) { + staticReceiverAstForMethod(decl) + } + val thisParam = if (!isAnonymousMethod && decl.isClassMethod && !isStatic) Option(thisParamAstForMethod(decl)) else None - val parameters = thisParam.toList ++ decl.params.zipWithIndex.map { case (param, idx) => + val parameters = thisParam.toList ++ staticReceiver.toList ++ decl.params.zipWithIndex.map { case (param, idx) => astForParam(param, idx + 1) } parameters.flatMap(_.root).foreach { @@ -254,12 +258,29 @@ trait AstForFunctionsCreator(implicit withSchemaValidation: ValidationMode) { th isVariadic = false, evaluationStrategy = EvaluationStrategies.BY_SHARING, typeFullName = typeFullName - ).dynamicTypeHintFullName(typeFullName :: Nil) - // TODO Add dynamicTypeHintFullName to parameterInNode param list + ) Ast(thisNode) } + private def staticReceiverAstForMethod(originNode: PhpNode): Ast = { + val typeFullName = scope.getEnclosingTypeDeclTypeFullName.getOrElse(Defines.Any) + + val node = parameterInNode( + originNode, + name = NameConstants.StaticReceiver, + code = NameConstants.StaticReceiver, + index = 0, + isVariadic = false, + evaluationStrategy = EvaluationStrategies.BY_SHARING, + typeFullName = typeFullName + ) + + scope.addToScope(NameConstants.StaticReceiver, node) + + Ast(node) + } + protected def thisIdentifier(originNode: PhpNode): NewIdentifier = { val typ = scope.getEnclosingTypeDeclTypeName identifierNode(originNode, NameConstants.This, s"$$${NameConstants.This}", typ.getOrElse(Defines.Any), typ.toList) @@ -309,14 +330,18 @@ trait AstForFunctionsCreator(implicit withSchemaValidation: ValidationMode) { th val typeFullName = param.paramType.map(_.name).getOrElse(Defines.Any) - val byRefCodePrefix = if (param.byRef) "&" else "" - val code = s"$byRefCodePrefix$$${param.name}" + val code = getParamCode(param) val paramNode = parameterInNode(param, param.name, code, index, param.isVariadic, evaluationStrategy, typeFullName) val attributeAsts = param.attributeGroups.flatMap(astForAttributeGroup) Ast(paramNode).withChildren(attributeAsts) } + private def getParamCode(param: PhpParam): String = { + val byRefCodePrefix = if (param.byRef) "&" else "" + s"$byRefCodePrefix$$${param.name}" + } + protected def astForStaticAndConstInits(node: PhpNode): Option[Ast] = { scope.getConstAndStaticInits match { case Nil => None diff --git a/joern-cli/frontends/php2cpg/src/main/scala/io/joern/php2cpg/utils/Scope.scala b/joern-cli/frontends/php2cpg/src/main/scala/io/joern/php2cpg/utils/Scope.scala index 84c6405cd187..d517e7d79efd 100644 --- a/joern-cli/frontends/php2cpg/src/main/scala/io/joern/php2cpg/utils/Scope.scala +++ b/joern-cli/frontends/php2cpg/src/main/scala/io/joern/php2cpg/utils/Scope.scala @@ -248,6 +248,12 @@ class Scope(summary: Map[String, Seq[SymbolSummary]] = Map.empty) def surroundingMethodParams: List[String] = stack.map(_.scopeNode).collectFirst { case ms: MethodScope => ms }.map(_.parameterNames.toList).get + def surroundingMethodReceiver: Option[String] = + stack + .collectFirst { case scopeEl @ ScopeElement(_: MethodScope, vars) => vars.headOption.map(_.head) } + .flatten + .filter(el => Set(NameConstants.StaticReceiver, NameConstants.This).contains(el)) + def getConstAndStaticInits: List[PhpInit] = { getInits(constAndStaticInits) } diff --git a/joern-cli/frontends/php2cpg/src/test/scala/io/joern/php2cpg/querying/CallTests.scala b/joern-cli/frontends/php2cpg/src/test/scala/io/joern/php2cpg/querying/CallTests.scala index 6b745b82f8bb..5b4c6997525e 100644 --- a/joern-cli/frontends/php2cpg/src/test/scala/io/joern/php2cpg/querying/CallTests.scala +++ b/joern-cli/frontends/php2cpg/src/test/scala/io/joern/php2cpg/querying/CallTests.scala @@ -1,11 +1,19 @@ package io.joern.php2cpg.querying import io.joern.php2cpg.astcreation.AstCreator.NameConstants -import io.joern.php2cpg.testfixtures.PhpCode2CpgFixture import io.joern.php2cpg.parser.Domain +import io.joern.php2cpg.testfixtures.PhpCode2CpgFixture import io.joern.x2cpg.Defines +import io.shiftleft.codepropertygraph.generated.nodes.{ + Call, + Identifier, + Literal, + Local, + MethodParameterIn, + Type, + TypeRef +} import io.shiftleft.codepropertygraph.generated.{DispatchTypes, Operators} -import io.shiftleft.codepropertygraph.generated.nodes.{Call, FieldIdentifier, Identifier, Literal} import io.shiftleft.semanticcpg.language.* class CallTests extends PhpCode2CpgFixture { @@ -103,14 +111,18 @@ class CallTests extends PhpCode2CpgFixture { } "have the correct arguments" in { - inside(cpg.call.argument.l) { case List(xArg: Identifier) => + inside(cpg.call.argument.l) { case List(fooTypeRef: TypeRef, xArg: Identifier) => + fooTypeRef.typeFullName shouldBe "Foo" + fooTypeRef.code shouldBe "Foo" xArg.name shouldBe "x" xArg.code shouldBe "$x" } } "have the correct child nodes" in { - inside(cpg.call.astChildren.l) { case List(arg: Identifier) => + inside(cpg.call.astChildren.l) { case List(fooTypeRef: TypeRef, arg: Identifier) => + fooTypeRef.typeFullName shouldBe "Foo" + fooTypeRef.code shouldBe "Foo" arg.name shouldBe "x" } } @@ -337,6 +349,18 @@ class CallTests extends PhpCode2CpgFixture { test1.staticReceiver shouldBe Some("Foo\\Bar\\baz") } + "have typeRef 'baz' as argument 0 of the test1 call" in { + inside(cpg.call.name("test1").argument(0).l) { case (typeRef: TypeRef) :: Nil => + typeRef.code shouldBe "baz" + + inside(typeRef.typ.l) { case (typ: Type) :: Nil => + typ.fullName shouldBe """Foo\Bar\baz""" + typ.name shouldBe """Foo\Bar\baz""" + typ.typeDeclFullName shouldBe """Foo\Bar\baz""" + } + } + } + "be unknown in the case of dynamic calls" in { val test2 = cpg.call("test2").head test2.name shouldBe "test2" @@ -417,4 +441,380 @@ class CallTests extends PhpCode2CpgFixture { construct.typeFullName shouldBe Defines.Any construct.dynamicTypeHintFullName shouldBe Seq.empty } + + "late static binding call in static method to a static method" should { + val cpg = code(""" + call.methodFullName shouldBe "Foo.bar" + call.code shouldBe "static::bar()" + call.dispatchType shouldBe DispatchTypes.DYNAMIC_DISPATCH + } + } + + "have '' as the call receiver" in { + inside(cpg.call("bar").receiver.isIdentifier.l) { case (identifier: Identifier) :: Nil => + identifier.name shouldBe NameConstants.StaticReceiver + identifier.code shouldBe NameConstants.StaticReceiver + + inside(identifier.refOut.l) { case (param: MethodParameterIn) :: Nil => + param.code shouldBe NameConstants.StaticReceiver + param.name shouldBe NameConstants.StaticReceiver + } + } + } + } + + "late static binding call in a non-static method to a static method" should { + val cpg = code(""" + call.methodFullName shouldBe "Foo.bar" + call.code shouldBe "static::bar()" + call.dispatchType shouldBe DispatchTypes.DYNAMIC_DISPATCH + } + } + + "have 'this' as the call receiver" in { + inside(cpg.call("bar").receiver.isIdentifier.l) { case (identifier: Identifier) :: Nil => + identifier.name shouldBe NameConstants.This + identifier.code shouldBe NameConstants.This + + inside(identifier.refOut.l) { case (param: MethodParameterIn) :: Nil => + param.code shouldBe NameConstants.This + param.name shouldBe NameConstants.This + } + } + } + } + + "late static binding call in a non-static method to a non-static method" should { + val cpg = code(""" + call.methodFullName shouldBe "Foo.bar" + call.code shouldBe "static::bar()" + call.dispatchType shouldBe DispatchTypes.DYNAMIC_DISPATCH + } + } + + "have 'this' as the call receiver" in { + inside(cpg.call("bar").receiver.isIdentifier.l) { case (identifier: Identifier) :: Nil => + identifier.name shouldBe NameConstants.This + identifier.code shouldBe NameConstants.This + + inside(identifier.refOut.l) { case (param: MethodParameterIn) :: Nil => + param.code shouldBe NameConstants.This + param.name shouldBe NameConstants.This + } + } + } + } + + "self call to a static function within a static function" should { + val cpg = code(""" + call.methodFullName shouldBe "Foo.bar" + call.code shouldBe "self::bar()" + call.dispatchType shouldBe DispatchTypes.STATIC_DISPATCH + } + } + + "have the correct staticReceiver property" in { + inside(cpg.call("bar").l) { case (call: Call) :: Nil => + call.staticReceiver shouldBe Some("Foo") + } + } + + "have as argument 0 of the call" in { + inside(cpg.call("bar").argument(0).l) { case (identifier: Identifier) :: Nil => + identifier.name shouldBe NameConstants.StaticReceiver + identifier.code shouldBe NameConstants.StaticReceiver + + inside(identifier.refOut.isParameter.l) { case (param: MethodParameterIn) :: Nil => + param.code shouldBe NameConstants.StaticReceiver + param.name shouldBe NameConstants.StaticReceiver + } + } + } + } + + "self call to a static function within a non-static function" should { + val cpg = code(""" + call.methodFullName shouldBe "Foo.bar" + call.code shouldBe "self::bar()" + call.dispatchType shouldBe DispatchTypes.STATIC_DISPATCH + } + } + + "have the correct staticReceiver property." in { + inside(cpg.call("bar").l) { case (call: Call) :: Nil => + call.staticReceiver shouldBe Some("Foo") + } + } + + "have 'this' as argument 0 to the 'bar' call" in { + inside(cpg.call("bar").argument(0).l) { case (identifier: Identifier) :: Nil => + identifier.name shouldBe NameConstants.This + identifier.code shouldBe NameConstants.This + + inside(identifier.refOut.l) { case (param: MethodParameterIn) :: Nil => + param.code shouldBe NameConstants.This + param.name shouldBe NameConstants.This + } + } + } + } + + "self call to a non-static function within a non-static function" should { + val cpg = code(""" + call.methodFullName shouldBe "Foo.bar" + call.code shouldBe "self::bar()" + call.dispatchType shouldBe DispatchTypes.STATIC_DISPATCH + } + } + + "have the correct staticReceiver property." in { + inside(cpg.call("bar").l) { case (call: Call) :: Nil => + call.staticReceiver shouldBe Some("Foo") + } + } + + "have 'this' as argument 0 to the 'bar' call" in { + inside(cpg.call("bar").argument(0).l) { case (identifier: Identifier) :: Nil => + identifier.name shouldBe NameConstants.This + identifier.code shouldBe NameConstants.This + + inside(identifier.refOut.l) { case (param: MethodParameterIn) :: Nil => + param.code shouldBe NameConstants.This + param.name shouldBe NameConstants.This + } + } + } + } + + "parent call to a static function from a static function" should { + val cpg = code(""" + call.methodFullName shouldBe "Base.bar" + call.code shouldBe "parent::bar()" + call.dispatchType shouldBe DispatchTypes.STATIC_DISPATCH + } + } + + "have the correct staticReceiver property." in { + inside(cpg.call("bar").l) { case (call: Call) :: Nil => + call.staticReceiver shouldBe Some("Base") + } + } + + "have '' as argument 0 to the 'bar' call" in { + inside(cpg.call("bar").argument(0).l) { case (identifier: Identifier) :: Nil => + identifier.name shouldBe NameConstants.StaticReceiver + identifier.code shouldBe NameConstants.StaticReceiver + + inside(identifier.refOut.l) { case (param: MethodParameterIn) :: Nil => + param.code shouldBe NameConstants.StaticReceiver + param.name shouldBe NameConstants.StaticReceiver + } + } + } + } + + "parent call to a static function from a non-static function" should { + val cpg = code(""" + call.methodFullName shouldBe "Base.bar" + call.code shouldBe "parent::bar()" + call.dispatchType shouldBe DispatchTypes.STATIC_DISPATCH + } + } + + "have the correct staticReceiver property." in { + inside(cpg.call("bar").l) { case (call: Call) :: Nil => + call.staticReceiver shouldBe Some("Base") + } + } + + "have 'this' as argument 0 to the 'bar' call" in { + inside(cpg.call("bar").argument(0).l) { case (identifier: Identifier) :: Nil => + identifier.name shouldBe NameConstants.This + identifier.code shouldBe NameConstants.This + + inside(identifier.refOut.l) { case (param: MethodParameterIn) :: Nil => + param.code shouldBe NameConstants.This + param.name shouldBe NameConstants.This + } + } + } + } + + "parent call to a non-static function from a non-static function" should { + val cpg = code(""" + call.methodFullName shouldBe "Base.bar" + call.code shouldBe "parent::bar()" + call.dispatchType shouldBe DispatchTypes.STATIC_DISPATCH + } + } + + "have the correct staticReceiver property." in { + inside(cpg.call("bar").l) { case (call: Call) :: Nil => + call.staticReceiver shouldBe Some("Base") + } + } + + "have 'this' as argument 0 to the 'bar' call" in { + inside(cpg.call("bar").argument(0).l) { case (identifier: Identifier) :: Nil => + identifier.name shouldBe NameConstants.This + identifier.code shouldBe NameConstants.This + + inside(identifier.refOut.l) { case (param: MethodParameterIn) :: Nil => + param.code shouldBe NameConstants.This + param.name shouldBe NameConstants.This + } + } + } + } + + "call to a class instance function" should { + val cpg = code("""foo(); + |""".stripMargin) + + "have '$a' as argument 0 of the 'foo' call" in { + inside(cpg.call.name("foo").argument(0).l) { case (identifier: Identifier) :: Nil => + identifier.code shouldBe "$a" + identifier.name shouldBe "a" + identifier.argumentIndex shouldBe 0 + + inside(identifier.refOut.l) { case (local: Local) :: Nil => + local.code shouldBe "$a" + local.name shouldBe "a" + local.lineNumber shouldBe Some(5) + } + } + } + } + + "call to a static class function" should { + val cpg = code(""" + typeRef.code shouldBe "Foo" + typeRef.argumentIndex shouldBe 0 + + inside(typeRef.typ.l) { case (typ: Type) :: Nil => + typ.fullName shouldBe "Foo" + typ.name shouldBe "Foo" + typ.typeDeclFullName shouldBe "Foo" + } + } + } + } } diff --git a/joern-cli/frontends/php2cpg/src/test/scala/io/joern/php2cpg/querying/MethodTests.scala b/joern-cli/frontends/php2cpg/src/test/scala/io/joern/php2cpg/querying/MethodTests.scala index 111e115fc7fc..b3beee3c2f5d 100644 --- a/joern-cli/frontends/php2cpg/src/test/scala/io/joern/php2cpg/querying/MethodTests.scala +++ b/joern-cli/frontends/php2cpg/src/test/scala/io/joern/php2cpg/querying/MethodTests.scala @@ -1,10 +1,19 @@ package io.joern.php2cpg.querying import io.joern.php2cpg.Config +import io.joern.php2cpg.astcreation.AstCreator.NameConstants import io.joern.php2cpg.testfixtures.PhpCode2CpgFixture import io.joern.x2cpg.Defines import io.shiftleft.codepropertygraph.generated.{ModifierTypes, Operators} -import io.shiftleft.codepropertygraph.generated.nodes.{Call, ClosureBinding, Identifier, Literal, Local, MethodRef} +import io.shiftleft.codepropertygraph.generated.nodes.{ + Call, + ClosureBinding, + Identifier, + Literal, + Local, + Method, + MethodRef +} import io.shiftleft.semanticcpg.language.* import scala.util.Try @@ -301,4 +310,36 @@ class MethodTests extends PhpCode2CpgFixture { barDedupTwoTwo.fullName shouldBe "Foo.__construct.foo0.bar1" } } + + "static class functions" should { + val cpg = code("""" in { + inside(cpg.method.name("foo").l) { case (fooMethod: Method) :: Nil => + val List(fooParam) = fooMethod.parameter.order(0).l + fooParam.name shouldBe NameConstants.StaticReceiver + fooParam.code shouldBe NameConstants.StaticReceiver + } + } + } + + "non-static class functions" should { + val cpg = code("""" in { + inside(cpg.method.name("foo").l) { case (fooMethod: Method) :: Nil => + val List(fooParam) = fooMethod.parameter.order(0).l + fooParam.name shouldBe NameConstants.This + fooParam.code shouldBe NameConstants.This + } + } + } } diff --git a/joern-cli/frontends/php2cpg/src/test/scala/io/joern/php2cpg/querying/TypeDeclTests.scala b/joern-cli/frontends/php2cpg/src/test/scala/io/joern/php2cpg/querying/TypeDeclTests.scala index 336ff214839f..d769b9298840 100644 --- a/joern-cli/frontends/php2cpg/src/test/scala/io/joern/php2cpg/querying/TypeDeclTests.scala +++ b/joern-cli/frontends/php2cpg/src/test/scala/io/joern/php2cpg/querying/TypeDeclTests.scala @@ -114,7 +114,7 @@ class TypeDeclTests extends PhpCode2CpgFixture { inside(fooMethod.parameter.l) { case List(thisParam, xParam) => thisParam.name shouldBe "this" thisParam.code shouldBe "this" - thisParam.dynamicTypeHintFullName should contain("Foo") + thisParam.dynamicTypeHintFullName shouldBe empty thisParam.typeFullName shouldBe "Foo" thisParam.index shouldBe 0 @@ -614,7 +614,11 @@ class TypeDeclTests extends PhpCode2CpgFixture { "contain static methods" in { inside(cpg.typeDecl.name(s"Foo").method.name("bar").l) { case barMethod :: Nil => - barMethod.modifier.modifierType.sorted.l shouldBe List(ModifierTypes.PUBLIC, ModifierTypes.STATIC) + barMethod.modifier.modifierType.sorted.l shouldBe List( + ModifierTypes.PUBLIC, + ModifierTypes.STATIC, + ModifierTypes.VIRTUAL + ) } } diff --git a/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/internal/CallAstBuilder.scala b/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/internal/CallAstBuilder.scala index bb007d878715..5e0b1bed1b98 100644 --- a/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/internal/CallAstBuilder.scala +++ b/joern-cli/frontends/x2cpg/src/main/scala/io/joern/x2cpg/internal/CallAstBuilder.scala @@ -61,6 +61,29 @@ private[x2cpg] trait CallAstBuilder[Node, NodeProcessor] { .withReceiverEdges(callNode, receiverRoot) } + /** Create an abstract syntax tree for a static call, i.e. a call without a base or receiver. + * + * This is a simplified variant of [[callAst]] for calls that are statically dispatched and do not require a `this` + * instance or receiver object (e.g. static method calls, top-level function calls, scope-resolution calls). + * + * @param callNode + * the node that represents the entire call + * @param arguments + * arguments to the call + * @param startArgumentIndex + * optionally override the starting argument index (defaults to 1 when not provided) + */ + def staticCallAst(callNode: NewCall, arguments: Seq[Ast] = List(), startArgumentIndex: Option[Int] = None): Ast = { + if (startArgumentIndex.nonEmpty) + setArgumentIndices(arguments, startArgumentIndex.get) + else + setArgumentIndices(arguments) + + Ast(callNode) + .withChildren(arguments) + .withArgEdges(callNode, arguments.flatMap(_.root)) + } + /** Creates an AST for a field-access expression (`base.fieldName`). * * Emits a [[io.shiftleft.codepropertygraph.generated.Operators.fieldAccess]] call node with `base` as the first