From abd592c15cedea82d2352ac2a78cbf339f573d9b Mon Sep 17 00:00:00 2001 From: Tebogo Selahle Date: Mon, 26 Jan 2026 14:53:11 +0200 Subject: [PATCH 1/8] feat: support scope resolution operators --- .../php2cpg/astcreation/AstCreator.scala | 25 +- .../astcreation/AstCreatorHelper.scala | 15 +- .../AstForExpressionsCreator.scala | 49 +++- .../astcreation/AstForFunctionsCreator.scala | 40 ++- .../scala/io/joern/php2cpg/utils/Scope.scala | 6 + .../io/joern/php2cpg/querying/CallTests.scala | 255 +++++++++++++++++- 6 files changed, 364 insertions(+), 26 deletions(-) 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..b4039ea8c15c 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" @@ -278,9 +281,17 @@ trait AstCreatorHelper(disableFileContent: Boolean)(implicit withSchemaValidatio } 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..befcfa393f69 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,10 @@ 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 => + astForDynamicCall(call, name, arguments, maybeTarget, isLateStaticBindingCall = true) + case _ if call.isStatic => astForStaticCall(call, name, arguments) + case maybeTarget => astForDynamicCall(call, name, arguments, maybeTarget) } } @@ -93,12 +95,35 @@ 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 match { + case Some(t: PhpNameExpr) => + scope.surroundingMethodReceiver match { + case Some(recv) => + val target = t.copy(name = recv) + Some(astForNameExpr(target, Some(NameConstants.Static))) + case _ => + logger.warn(s"Expected method surround call $call to have parameter 0 as `static` or `") + None + } + case t => + logger.warn(s"Expected a PhpNameExpr target for call on static receiver but got $t.") + None + } + } 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 @@ -127,6 +152,10 @@ trait AstForExpressionsCreator(implicit withSchemaValidation: ValidationMode) { } private def astForStaticCall(call: PhpCallExpr, name: String, arguments: Seq[Ast]): Ast = { + val isParentCall = call.target match { + case Some(expr: PhpNameExpr) => expr.name == NameConstants.Parent + case _ => false + } val argsCode = getArgsCode(call, arguments) val codePrefix = codeForStaticMethodCall(call, name) val code = s"$codePrefix($argsCode)" @@ -140,6 +169,8 @@ trait AstForExpressionsCreator(implicit withSchemaValidation: ValidationMode) { } val staticReceiver = call.target.collect { + case nameExpr: PhpNameExpr if isParentCall => + getInheritedTypeFullName case nameExpr: PhpNameExpr if nameExpr.name == NameConstants.Self => getTypeDeclPrefix case nameExpr: PhpNameExpr => @@ -697,8 +728,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..17d239674b5c 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 @@ -173,6 +173,19 @@ trait AstForFunctionsCreator(implicit withSchemaValidation: ValidationMode) { th } scope.useFunctionDecl(methodName, fullName) + val thisParam = if (!isAnonymousMethod && decl.isClassMethod && !isStatic) { + Option(thisParamAstForMethod(decl)) + } else { + None + } + val staticReceiver = Option.when(decl.isClassMethod && isStatic) { + staticReceiverAstForMethod(decl) + } + + val parameters = thisParam.toList ++ staticReceiver.toList ++ decl.params.zipWithIndex.map { case (param, idx) => + astForParam(param, idx + 1) + } + val returnType = decl.returnType.map(_.name).getOrElse(Defines.Any) val fieldInitAsts = scope.getFieldInits.map { fieldInit => @@ -260,6 +273,25 @@ trait AstForFunctionsCreator(implicit withSchemaValidation: ValidationMode) { th 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 + ).dynamicTypeHintFullName(typeFullName :: Nil) + // TODO Add dynamicTypeHintFullName to parameterInNode param list + + 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 +341,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..1c1167fc8508 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(v => Set(NameConstants.StaticReceiver, NameConstants.This).contains(v)) + 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..76059b022448 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,11 @@ 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, Method} 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 { @@ -417,4 +417,255 @@ class CallTests extends PhpCode2CpgFixture { construct.typeFullName shouldBe Defines.Any construct.dynamicTypeHintFullName shouldBe Seq.empty } + + "late static binding call in static method" should { + val cpg = code(""" + call.methodFullName shouldBe "Foo.bar" + call.code shouldBe "static::bar()" + call.dispatchType shouldBe DispatchTypes.DYNAMIC_DISPATCH + + inside(call.receiver.isIdentifier.l) { case (identifier: Identifier) :: Nil => + identifier.name shouldBe NameConstants.StaticReceiver + identifier.code shouldBe NameConstants.Static + } + } + } + + "contain as argument 0 of static functions" in { + inside(cpg.method.isStatic.l) { case (fooMethod: Method) :: (barMethod: Method) :: _ :: Nil => + fooMethod.name shouldBe "foo" + fooMethod.parameter.headOption.map(_.name) shouldBe Some(NameConstants.StaticReceiver) + + barMethod.name shouldBe "bar" + barMethod.parameter.headOption.map(_.name) shouldBe Some(NameConstants.StaticReceiver) + } + } + } + + "late static binding call in 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 "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 receivers" in { + inside(cpg.call("bar").l) { case (call: Call) :: Nil => + call.staticReceiver shouldBe Some("Foo") + } + } + } + + "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 receivers" in { + inside(cpg.call("bar").l) { case (call: Call) :: Nil => + call.staticReceiver shouldBe Some("Foo") + } + } + } + + "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 receivers" in { + inside(cpg.call("bar").l) { case (call: Call) :: Nil => + call.staticReceiver shouldBe Some("Foo") + } + } + } + + "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 receivers" in { + inside(cpg.call("bar").l) { case (call: Call) :: Nil => + call.staticReceiver shouldBe Some("Base") + } + } + } + + "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 receivers" in { + inside(cpg.call("bar").l) { case (call: Call) :: Nil => + call.staticReceiver shouldBe Some("Base") + } + } + } + + "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 receivers" in { + inside(cpg.call("bar").l) { case (call: Call) :: Nil => + call.staticReceiver shouldBe Some("Base") + } + } + } + + "parent call to a non-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 receivers" in { + inside(cpg.call("bar").l) { case (call: Call) :: Nil => + call.staticReceiver shouldBe Some("Base") + } + } + } } From ff65fb16a022b33f51d017a79eed6a6a21c0e6e6 Mon Sep 17 00:00:00 2001 From: Tebogo Selahle Date: Mon, 26 Jan 2026 16:23:06 +0200 Subject: [PATCH 2/8] chore: cleanup --- .../astcreation/AstForExpressionsCreator.scala | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) 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 befcfa393f69..e504e8ee74f8 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 @@ -85,6 +85,7 @@ trait AstForExpressionsCreator(implicit withSchemaValidation: ValidationMode) { case None if isCallOnVariable(call) => astForDynamicCall(call, name, arguments, None) case None => astForStaticCall(call, name, arguments) 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) @@ -102,16 +103,12 @@ trait AstForExpressionsCreator(implicit withSchemaValidation: ValidationMode) { val targetAst = if (isLateStaticBindingCall) { maybeTarget match { case Some(t: PhpNameExpr) => - scope.surroundingMethodReceiver match { - case Some(recv) => - val target = t.copy(name = recv) - Some(astForNameExpr(target, Some(NameConstants.Static))) - case _ => - logger.warn(s"Expected method surround call $call to have parameter 0 as `static` or `") - None + scope.surroundingMethodReceiver.map { recv => + val target = t.copy(name = recv) + astForNameExpr(target, Some(NameConstants.Static)) } case t => - logger.warn(s"Expected a PhpNameExpr target for call on static receiver but got $t.") + logger.warn(s"Expected a PhpNameExpr target but got $t.") None } } else { From 047b52750f17c8935c9b4a9d60e8a37eb4811949 Mon Sep 17 00:00:00 2001 From: Tebogo Selahle Date: Tue, 3 Feb 2026 17:55:10 +0200 Subject: [PATCH 3/8] fix: add arg0 to class function calls --- .../astcreation/AstCreatorHelper.scala | 2 + .../AstForExpressionsCreator.scala | 36 ++- .../astcreation/AstForFunctionsCreator.scala | 6 +- .../io/joern/php2cpg/querying/CallTests.scala | 233 ++++++++++++++---- .../joern/php2cpg/querying/MethodTests.scala | 43 +++- 5 files changed, 263 insertions(+), 57 deletions(-) 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 b4039ea8c15c..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 @@ -280,6 +280,8 @@ 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) 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 e504e8ee74f8..bb0d22565d69 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 @@ -101,16 +101,7 @@ trait AstForExpressionsCreator(implicit withSchemaValidation: ValidationMode) { ): Ast = { val argsCode = getArgsCode(call, arguments) val targetAst = if (isLateStaticBindingCall) { - maybeTarget match { - case Some(t: PhpNameExpr) => - scope.surroundingMethodReceiver.map { recv => - val target = t.copy(name = recv) - astForNameExpr(target, Some(NameConstants.Static)) - } - case t => - logger.warn(s"Expected a PhpNameExpr target but got $t.") - None - } + maybeTarget.flatMap(astForClassScopeResolutionTarget) } else { maybeTarget.map(astForExpr) } @@ -148,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 t => + logger.warn(s"Expected a PhpNameExpr target but got $t.") + None + } + } + private def astForStaticCall(call: PhpCallExpr, name: String, arguments: Seq[Ast]): Ast = { val isParentCall = call.target match { case Some(expr: PhpNameExpr) => expr.name == NameConstants.Parent @@ -165,6 +169,14 @@ trait AstForExpressionsCreator(implicit withSchemaValidation: ValidationMode) { case _ => getMfn(call, name) } + val targetArgument = call.target.collect { + case nameExpr: PhpNameExpr if nameExpr.name == NameConstants.Parent || nameExpr.name == NameConstants.Self => + 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 isParentCall => getInheritedTypeFullName @@ -176,7 +188,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, List(targetArgument, arguments).flatten, startArgumentIndex = startArgumentIndex) } protected def simpleAssignAst(origin: PhpNode, target: Ast, source: Ast): Ast = { 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 17d239674b5c..664e7bcef687 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 @@ -267,7 +267,7 @@ 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) @@ -284,7 +284,7 @@ trait AstForFunctionsCreator(implicit withSchemaValidation: ValidationMode) { th isVariadic = false, evaluationStrategy = EvaluationStrategies.BY_SHARING, typeFullName = typeFullName - ).dynamicTypeHintFullName(typeFullName :: Nil) + ) // TODO Add dynamicTypeHintFullName to parameterInNode param list scope.addToScope(NameConstants.StaticReceiver, node) 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 76059b022448..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 @@ -4,7 +4,15 @@ import io.joern.php2cpg.astcreation.AstCreator.NameConstants 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, Method} +import io.shiftleft.codepropertygraph.generated.nodes.{ + Call, + Identifier, + Literal, + Local, + MethodParameterIn, + Type, + TypeRef +} import io.shiftleft.codepropertygraph.generated.{DispatchTypes, Operators} import io.shiftleft.semanticcpg.language.* @@ -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" @@ -418,14 +442,14 @@ class CallTests extends PhpCode2CpgFixture { construct.dynamicTypeHintFullName shouldBe Seq.empty } - "late static binding call in static method" should { + "late static binding call in static method to a static method" should { val cpg = code("""' 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(call.receiver.isIdentifier.l) { case (identifier: Identifier) :: Nil => - identifier.name shouldBe NameConstants.StaticReceiver - identifier.code shouldBe NameConstants.Static + 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 + } + } - "contain as argument 0 of static functions" in { - inside(cpg.method.isStatic.l) { case (fooMethod: Method) :: (barMethod: Method) :: _ :: Nil => - fooMethod.name shouldBe "foo" - fooMethod.parameter.headOption.map(_.name) shouldBe Some(NameConstants.StaticReceiver) + "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 - barMethod.name shouldBe "bar" - barMethod.parameter.headOption.map(_.name) shouldBe Some(NameConstants.StaticReceiver) + 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" should { + "late static binding call in a non-static method to a non-static method" should { val cpg = code(""" - identifier.name shouldBe "this" + 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 + } } } } @@ -498,11 +557,23 @@ class CallTests extends PhpCode2CpgFixture { } } - "have the correct receivers" in { + "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 { @@ -524,11 +595,23 @@ class CallTests extends PhpCode2CpgFixture { } } - "have the correct receivers" in { + "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 { @@ -550,11 +633,23 @@ class CallTests extends PhpCode2CpgFixture { } } - "have the correct receivers" in { + "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 { @@ -578,11 +673,23 @@ class CallTests extends PhpCode2CpgFixture { } } - "have the correct receivers" in { + "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 { @@ -606,11 +713,23 @@ class CallTests extends PhpCode2CpgFixture { } } - "have the correct receivers" in { + "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 { @@ -634,37 +753,67 @@ class CallTests extends PhpCode2CpgFixture { } } - "have the correct receivers" in { + "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 static function" should { + "call to a class instance function" should { val cpg = code("""foo(); |""".stripMargin) - "be a statically dispatched call to the base class" in { - inside(cpg.call("bar").l) { case (call: Call) :: Nil => - call.methodFullName shouldBe "Base.bar" - call.code shouldBe "parent::bar()" - call.dispatchType shouldBe DispatchTypes.STATIC_DISPATCH + "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) + } } } + } - "have the correct receivers" in { - inside(cpg.call("bar").l) { case (call: Call) :: Nil => - call.staticReceiver shouldBe Some("Base") + "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 + } + } + } } From bd328744bef5ab752253e136bbedf29d9a1f4e05 Mon Sep 17 00:00:00 2001 From: Tebogo Selahle Date: Wed, 4 Feb 2026 12:43:06 +0200 Subject: [PATCH 4/8] fix: whoops, update tests --- .../astcreation/AstForExpressionsCreator.scala | 10 +++------- .../io/joern/php2cpg/querying/TypeDeclTests.scala | 11 ++++++++--- 2 files changed, 11 insertions(+), 10 deletions(-) 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 bb0d22565d69..675dbfdd5177 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 @@ -153,10 +153,6 @@ trait AstForExpressionsCreator(implicit withSchemaValidation: ValidationMode) { } private def astForStaticCall(call: PhpCallExpr, name: String, arguments: Seq[Ast]): Ast = { - val isParentCall = call.target match { - case Some(expr: PhpNameExpr) => expr.name == NameConstants.Parent - case _ => false - } val argsCode = getArgsCode(call, arguments) val codePrefix = codeForStaticMethodCall(call, name) val code = s"$codePrefix($argsCode)" @@ -170,7 +166,7 @@ trait AstForExpressionsCreator(implicit withSchemaValidation: ValidationMode) { } val targetArgument = call.target.collect { - case nameExpr: PhpNameExpr if nameExpr.name == NameConstants.Parent || nameExpr.name == NameConstants.Self => + 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) @@ -178,7 +174,7 @@ trait AstForExpressionsCreator(implicit withSchemaValidation: ValidationMode) { }.flatten val staticReceiver = call.target.collect { - case nameExpr: PhpNameExpr if isParentCall => + case nameExpr: PhpNameExpr if nameExpr.name == NameConstants.Parent => getInheritedTypeFullName case nameExpr: PhpNameExpr if nameExpr.name == NameConstants.Self => getTypeDeclPrefix @@ -190,7 +186,7 @@ trait AstForExpressionsCreator(implicit withSchemaValidation: ValidationMode) { val allArgs = List(targetArgument, arguments).flatten val startArgumentIndex = Option.when(targetArgument.isDefined)(0) - staticCallAst(callRoot, List(targetArgument, arguments).flatten, startArgumentIndex = startArgumentIndex) + staticCallAst(callRoot, allArgs, startArgumentIndex = startArgumentIndex) } protected def simpleAssignAst(origin: PhpNode, target: Ast, source: Ast): Ast = { 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..dbe54b527619 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 @@ -613,8 +613,13 @@ 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) + inside(cpg.typeDecl.name(s"Foo").method.name("bar").l) { + case barMethod :: Nil => + barMethod.modifier.modifierType.sorted.l shouldBe List( + ModifierTypes.PUBLIC, + ModifierTypes.STATIC, + ModifierTypes.VIRTUAL + ) } } From 07c7af1e05e76f4447f885e27d068ad9d46d343c Mon Sep 17 00:00:00 2001 From: Tebogo Selahle Date: Fri, 6 Feb 2026 13:37:03 +0200 Subject: [PATCH 5/8] chore: remove dynamicTypeHintFullName comment --- .../io/joern/php2cpg/astcreation/AstForFunctionsCreator.scala | 2 -- 1 file changed, 2 deletions(-) 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 664e7bcef687..96a2b3e35097 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 @@ -268,7 +268,6 @@ trait AstForFunctionsCreator(implicit withSchemaValidation: ValidationMode) { th evaluationStrategy = EvaluationStrategies.BY_SHARING, typeFullName = typeFullName ) - // TODO Add dynamicTypeHintFullName to parameterInNode param list Ast(thisNode) } @@ -285,7 +284,6 @@ trait AstForFunctionsCreator(implicit withSchemaValidation: ValidationMode) { th evaluationStrategy = EvaluationStrategies.BY_SHARING, typeFullName = typeFullName ) - // TODO Add dynamicTypeHintFullName to parameterInNode param list scope.addToScope(NameConstants.StaticReceiver, node) From b17415b6969448a8b4d9d57de1d2bfa09f7bd1de Mon Sep 17 00:00:00 2001 From: Tebogo Selahle Date: Mon, 13 Jul 2026 15:58:18 +0200 Subject: [PATCH 6/8] refactor: scalafmt --- .../io/joern/php2cpg/querying/TypeDeclTests.scala | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) 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 dbe54b527619..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 @@ -613,13 +613,12 @@ 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, - ModifierTypes.VIRTUAL - ) + inside(cpg.typeDecl.name(s"Foo").method.name("bar").l) { case barMethod :: Nil => + barMethod.modifier.modifierType.sorted.l shouldBe List( + ModifierTypes.PUBLIC, + ModifierTypes.STATIC, + ModifierTypes.VIRTUAL + ) } } From 579827c49ecc28735d5c1d4d1ba4f11f3dc7b156 Mon Sep 17 00:00:00 2001 From: Tebogo Selahle Date: Mon, 13 Jul 2026 15:58:42 +0200 Subject: [PATCH 7/8] fix: rebase issues fix --- .../astcreation/AstForFunctionsCreator.scala | 19 ++++----------- .../joern/x2cpg/internal/CallAstBuilder.scala | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+), 14 deletions(-) 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 96a2b3e35097..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 @@ -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 { @@ -173,19 +177,6 @@ trait AstForFunctionsCreator(implicit withSchemaValidation: ValidationMode) { th } scope.useFunctionDecl(methodName, fullName) - val thisParam = if (!isAnonymousMethod && decl.isClassMethod && !isStatic) { - Option(thisParamAstForMethod(decl)) - } else { - None - } - val staticReceiver = Option.when(decl.isClassMethod && isStatic) { - staticReceiverAstForMethod(decl) - } - - val parameters = thisParam.toList ++ staticReceiver.toList ++ decl.params.zipWithIndex.map { case (param, idx) => - astForParam(param, idx + 1) - } - val returnType = decl.returnType.map(_.name).getOrElse(Defines.Any) val fieldInitAsts = scope.getFieldInits.map { fieldInit => 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 From f932590b15877d887d713a23b122b52c61bd5cce Mon Sep 17 00:00:00 2001 From: Tebogo Selahle Date: Mon, 13 Jul 2026 16:03:35 +0200 Subject: [PATCH 8/8] fix: lint issues --- .../joern/php2cpg/astcreation/AstForExpressionsCreator.scala | 4 ++-- .../php2cpg/src/main/scala/io/joern/php2cpg/utils/Scope.scala | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 675dbfdd5177..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 @@ -146,8 +146,8 @@ trait AstForExpressionsCreator(implicit withSchemaValidation: ValidationMode) { val target = t.copy(name = recv) astForNameExpr(target, code = Some(recv)) } - case t => - logger.warn(s"Expected a PhpNameExpr target but got $t.") + case expr => + logger.warn(s"Expected a PhpNameExpr target but got $expr.") 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 1c1167fc8508..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 @@ -252,7 +252,7 @@ class Scope(summary: Map[String, Seq[SymbolSummary]] = Map.empty) stack .collectFirst { case scopeEl @ ScopeElement(_: MethodScope, vars) => vars.headOption.map(_.head) } .flatten - .filter(v => Set(NameConstants.StaticReceiver, NameConstants.This).contains(v)) + .filter(el => Set(NameConstants.StaticReceiver, NameConstants.This).contains(el)) def getConstAndStaticInits: List[PhpInit] = { getInits(constAndStaticInits)