Skip to content

Commit

Permalink
[php] Implemented TypeHintCallLinker & PathSep Supports String (#4430)
Browse files Browse the repository at this point in the history
* Implemented `PhpTypeHintCallLinker`
* Allowed for `pathSep` to be a string to accommodate PHP's `->` syntax

Resolves #4402
  • Loading branch information
DavidBakerEffendi authored Apr 10, 2024
1 parent 6ee14da commit 822b022
Show file tree
Hide file tree
Showing 13 changed files with 102 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ class JavaTypeHintCallLinker(cpg: Cpg) extends XTypeHintCallLinker(cpg) {
override protected def calls: Iterator[Call] = {
cpg.call
.nameNot("<operator>.*", "<operators>.*")
.filter(c =>
calleeNames(c).nonEmpty && c.callee.fullNameNot(Pattern.quote(Defines.UnresolvedNamespace) + ".*").isEmpty
)
.filter(c => calleeNames(c).nonEmpty && c.callee.forall(_.fullName.startsWith(Defines.UnresolvedNamespace)))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import io.shiftleft.semanticcpg.language.*

class JavaScriptTypeHintCallLinker(cpg: Cpg) extends XTypeHintCallLinker(cpg) {

override protected val pathSep = ':'
override protected val pathSep = ":"

override protected def calls: Iterator[Call] = cpg.call
.or(_.nameNot("<operator>.*", "<operators>.*"), _.name("<operator>.new"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ private class RecoverForJavaScriptFile(cpg: Cpg, cu: File, builder: DiffGraphBui

import io.joern.x2cpg.passes.frontend.XTypeRecovery.AllNodeTypesFromNodeExt

override protected val pathSep = ':'
override protected val pathSep = ":"

/** A heuristic method to determine if a call is a constructor or not.
*/
Expand Down Expand Up @@ -94,7 +94,7 @@ private class RecoverForJavaScriptFile(cpg: Cpg, cu: File, builder: DiffGraphBui
}
.flatMap {
case (t, ts) if Set(t) == ts => Set(t)
case (_, ts) => ts.map(_.replaceAll("\\.(?!js::program)", pathSep.toString))
case (_, ts) => ts.map(_.replaceAll("\\.(?!js::program)", pathSep))
}
p match {
case _: MethodParameterIn => symbolTable.put(p, resolvedHints)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import io.joern.php2cpg.passes.{
ClosureRefPass,
LocalCreationPass,
PhpTypeStubsParserPass,
PhpTypeRecoveryPassGenerator
PhpTypeRecoveryPassGenerator,
PhpTypeHintCallLinker
}
import io.joern.x2cpg.X2Cpg.withNewEmptyCpg
import io.joern.x2cpg.X2CpgFrontend
Expand Down Expand Up @@ -92,6 +93,6 @@ object Php2Cpg {
List(new PhpTypeStubsParserPass(cpg, setKnownTypesConfig)) ++ new PhpTypeRecoveryPassGenerator(
cpg,
typeRecoveryConfig
).generate()
).generate() :+ PhpTypeHintCallLinker(cpg)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.joern.php2cpg.passes

import io.joern.x2cpg.Defines
import io.joern.x2cpg.passes.frontend.XTypeHintCallLinker
import io.shiftleft.codepropertygraph.Cpg
import io.shiftleft.semanticcpg.language.*
import io.shiftleft.codepropertygraph.generated.nodes.Call

import java.util.regex.Pattern

class PhpTypeHintCallLinker(cpg: Cpg) extends XTypeHintCallLinker(cpg) {

override protected val pathSep = "->"

override protected def calls: Iterator[Call] = {
cpg.call
.nameNot("<operator>.*", "<operators>.*")
.filter(c => calleeNames(c).nonEmpty && c.callee.forall(_.fullName.startsWith(Defines.UnresolvedNamespace)))
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package io.joern.php2cpg.passes

import io.joern.x2cpg.Defines
import io.joern.x2cpg.passes.frontend._
import io.joern.x2cpg.passes.frontend.*
import io.shiftleft.codepropertygraph.Cpg
import io.shiftleft.codepropertygraph.generated.nodes._
import io.shiftleft.codepropertygraph.generated.{Operators, PropertyNames, DispatchTypes}
import io.shiftleft.semanticcpg.language._
import io.shiftleft.codepropertygraph.generated.nodes.*
import io.shiftleft.codepropertygraph.generated.{DispatchTypes, Operators, PropertyNames}
import io.shiftleft.semanticcpg.language.*
import io.shiftleft.semanticcpg.language.operatorextension.OpNodes
import io.shiftleft.semanticcpg.language.operatorextension.OpNodes.{Assignment, FieldAccess}
import overflowdb.BatchedUpdate.DiffGraphBuilder
Expand Down Expand Up @@ -49,6 +49,15 @@ private class RecoverForPhpFile(cpg: Cpg, cu: NamespaceBlock, builder: DiffGraph

protected val methodTypesTable = mutable.Map[Method, mutable.HashSet[String]]()

override val pathSep: String = "->"

override def hasTypes(node: AstNode): Boolean = {
node match {
case x: Call => !XTypeRecovery.unknownTypePattern.matches(x.methodFullName)
case _ => super.hasTypes(node)
}
}

override def isConstructor(c: Call): Boolean =
isConstructor(c.name) && c.code.endsWith(")")

Expand Down Expand Up @@ -134,7 +143,7 @@ private class RecoverForPhpFile(cpg: Cpg, cu: NamespaceBlock, builder: DiffGraph
case ::(head: Call, Nil) if head.argumentOut.headOption.exists(symbolTable.contains) =>
symbolTable
.get(head.argumentOut.head)
.map(t => Seq(t, head.name, XTypeRecovery.DummyReturnType).mkString(pathSep.toString))
.map(t => Seq(t, head.name, XTypeRecovery.DummyReturnType).mkString(pathSep))
case ::(identifier: Identifier, Nil) if symbolTable.contains(identifier) =>
symbolTable.get(identifier)
case ::(head: Call, Nil) =>
Expand All @@ -145,21 +154,26 @@ private class RecoverForPhpFile(cpg: Cpg, cu: NamespaceBlock, builder: DiffGraph
existingTypes.addAll(returnTypes)

/* Check whether method return is already known, and if so, remove dummy value */
val saveTypes = existingTypes.filterNot(typeName => {
if (typeName.startsWith(Defines.UnresolvedNamespace))
val saveTypes = existingTypes.filterNot { typeName =>
if (typeName.startsWith(Defines.UnresolvedNamespace)) {
true
else if (typeName.endsWith(s"${XTypeRecovery.DummyReturnType}"))
typeName.split(pathSep).headOption match {
case Some(methodName) => {
val methodReturns = methodReturnValues(Seq(methodName))
} else if (typeName.endsWith(XTypeRecovery.DummyReturnType)) {
typeName.split(pathSep).toList.reverse match {
case _ :: methodFullName :: Nil =>
val methodReturns = methodReturnValues(Seq(methodFullName))
.filterNot(_.endsWith(s"${XTypeRecovery.DummyReturnType}"))
!methodReturns.isEmpty
}
case None => false
methodReturns.nonEmpty
case _ :: methodName :: typeFullName =>
val methodFullName = Seq(s"${typeFullName.mkString(pathSep)}$pathSep$methodName")
val methodReturns = methodReturnValues(methodFullName)
.filterNot(_.endsWith(s"${XTypeRecovery.DummyReturnType}"))
methodReturns.nonEmpty
case _ => false
}
else
} else {
false
})
}
}
methodTypesTable.update(m, saveTypes)
builder.setNodeProperty(ret.method.methodReturn, PropertyNames.DYNAMIC_TYPE_HINT_FULL_NAME, saveTypes)
}
Expand Down Expand Up @@ -262,25 +276,24 @@ private class RecoverForPhpFile(cpg: Cpg, cu: NamespaceBlock, builder: DiffGraph

if (c.argument.exists(_.argumentIndex == 0)) {
c.argument(0) match {
case p: Identifier => {
case p: Identifier =>
val ts = (p.typeFullName +: p.dynamicTypeHintFullName)
.filterNot(_ == "ANY")
.distinct
.l
ts match {
case Seq() =>
case Seq(t) => {
case Nil =>
case t :: Nil =>
val newFullName = t + "->" + c.name
builder.setNodeProperty(c, PropertyNames.METHOD_FULL_NAME, newFullName)
builder.setNodeProperty(
c,
PropertyNames.TYPE_FULL_NAME,
s"${newFullName}$pathSep${XTypeRecovery.DummyReturnType}"
s"$newFullName$pathSep${XTypeRecovery.DummyReturnType}"
)
builder.setNodeProperty(c, PropertyNames.DYNAMIC_TYPE_HINT_FULL_NAME, Seq.empty)
}
case _ => { /* TODO: case where multiple possible types are identified */ }
}
}
case _ =>
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -422,4 +422,27 @@ class PhpTypeRecoveryPassTests extends PhpCode2CpgFixture() {
barMethod.methodReturn.dynamicTypeHintFullName shouldBe Seq("int")
}
}

"objects instantiated from an external dependency" should {
val cpg = code("""<?php
|use Curler\Client;
|
|$client = new Client();
|$response = $client->get('https://example2.com/data', $userId);
|echo $response->getBody();
|>
|""".stripMargin)

"resolve all types and calls for `$client`" in {
cpg.identifier("client").typeFullName.toSet shouldBe Set("Curler\\Client")
cpg.call("__construct").methodFullName.toSet shouldBe Set("Curler\\Client->__construct")
cpg.call("get").methodFullName.toSet shouldBe Set("Curler\\Client->get")
}

"resolve all types and calls for `$response`, where the call should have some dummy type" in {
cpg.identifier("response").typeFullName.toSet shouldBe Set("Curler\\Client->get-><returnValue>")
cpg.call("getBody").methodFullName.toSet shouldBe Set("Curler\\Client->get-><returnValue>->getBody")
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -98,18 +98,18 @@ class PythonImportResolverPass(cpg: Cpg) extends XImportResolverPass(cpg) {
* the possible callee names
*/
private def createPseudoImports(expEntity: String, alias: String): Set[EvaluatedImport] = {
val pathSep = "."
val isMaybeConstructor = expEntity.split("\\.").lastOption.exists(s => s.nonEmpty && s.charAt(0).isUpper)
val pathSep = '.'
val isMaybeConstructor = expEntity.split(pathSep).lastOption.exists(s => s.nonEmpty && s.charAt(0).isUpper)

def toUnresolvedImport(pseudoPath: String): Set[EvaluatedImport] = {
if (isMaybeConstructor) {
Set(UnknownMethod(Seq(pseudoPath, "__init__").mkString(pathSep), alias), UnknownTypeDecl(pseudoPath))
Set(UnknownMethod(Seq(pseudoPath, "__init__").mkString(pathSep.toString), alias), UnknownTypeDecl(pseudoPath))
} else {
Set(UnknownImport(pseudoPath))
}
}

expEntity.split("\\.").reverse.toList match
expEntity.split(pathSep).reverse.toList match
case name :: Nil => toUnresolvedImport(s"$name.py:<module>")
case name :: xs => toUnresolvedImport(s"${xs.reverse.mkString(JFile.separator)}.py:<module>$pathSep$name")
case Nil => Set.empty
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ private class RecoverForPythonFile(cpg: Cpg, cu: File, builder: DiffGraphBuilder
case t if t.matches(".*\\.<(member|returnValue|indexAccess)>(\\(.*\\))?") =>
super.createCallFromIdentifierTypeFullName(typeFullName, callName)
case t if isConstructor(tName) =>
Seq(t, callName).mkString(pathSep.toString)
Seq(t, callName).mkString(pathSep)
case _ => super.createCallFromIdentifierTypeFullName(typeFullName, callName)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import io.shiftleft.semanticcpg.language.*

class SwiftTypeHintCallLinker(cpg: Cpg) extends XTypeHintCallLinker(cpg) {

override protected val pathSep = ':'
override protected val pathSep = ":"

override protected def calls: Iterator[Call] = cpg.call
.or(_.nameNot("<operator>.*", "<operators>.*"), _.name("<operator>.new"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ private class RecoverForSwiftFile(cpg: Cpg, cu: File, builder: DiffGraphBuilder,

import io.joern.x2cpg.passes.frontend.XTypeRecovery.AllNodeTypesFromNodeExt

override protected val pathSep = ':'
override protected val pathSep = ":"

/** A heuristic method to determine if a call is a constructor or not.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ abstract class XTypeHintCallLinker(cpg: Cpg) extends CpgPass(cpg) {

implicit protected val resolver: NoResolve.type = NoResolve
private val fileNamePattern = Pattern.compile("^(.*(.py|.js|.rb)).*$")
protected val pathSep: Char = '.'
protected val pathSep: String = "."

protected def calls: Iterator[Call] = cpg.call
.nameNot("<operator>.*", "<operators>.*")
Expand Down Expand Up @@ -118,7 +118,7 @@ abstract class XTypeHintCallLinker(cpg: Cpg) extends CpgPass(cpg) {
}
val name =
if (methodName.contains(pathSep) && methodName.length > methodName.lastIndexOf(pathSep) + 1)
methodName.substring(methodName.lastIndexOf(pathSep) + 1)
methodName.substring(methodName.lastIndexOf(pathSep) + pathSep.length)
else methodName
createMethodStub(name, methodName, call.argumentOut.size, isExternal, builder)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import overflowdb.BatchedUpdate
import overflowdb.BatchedUpdate.DiffGraphBuilder
import scopt.OParser

import java.util.regex.{Matcher, Pattern}
import scala.annotation.tailrec
import scala.collection.mutable
import scala.util.{Failure, Success, Try}
Expand Down Expand Up @@ -196,7 +197,7 @@ object XTypeRecovery {

val unknownTypePattern: Regex = s"(i?)(UNKNOWN|ANY|${Defines.UnresolvedNamespace}).*".r

def dummyMemberType(prefix: String, memberName: String, sep: Char = '.'): String =
def dummyMemberType(prefix: String, memberName: String, sep: String = "."): String =
s"$prefix$sep$DummyMemberLoad($memberName)"

/** Scans the type for placeholder/dummy types.
Expand Down Expand Up @@ -289,7 +290,7 @@ abstract class RecoverForXCompilationUnit[CompilationUnitType <: AstNode](

/** The delimiter used to separate methods/functions in qualified names.
*/
protected val pathSep = '.'
protected val pathSep = "."

/** New node tracking set.
*/
Expand Down Expand Up @@ -497,7 +498,7 @@ abstract class RecoverForXCompilationUnit[CompilationUnitType <: AstNode](
val fullNames = returns.typeFullName ++ returnWithPossibleTypes.possibleTypes
fullNames.toSet match {
case xs if xs.nonEmpty => xs
case _ => symbolTable.get(x).map(t => Seq(t, XTypeRecovery.DummyReturnType).mkString(pathSep.toString))
case _ => symbolTable.get(x).map(t => Seq(t, XTypeRecovery.DummyReturnType).mkString(pathSep))
}
case x =>
symbolTable.get(x)
Expand Down Expand Up @@ -535,7 +536,7 @@ abstract class RecoverForXCompilationUnit[CompilationUnitType <: AstNode](
/** Returns the appropriate field parent scope.
*/
protected def getFieldParents(fa: FieldAccess): Set[String] = {
val fieldName = getFieldName(fa).split(pathSep).last
val fieldName = getFieldName(fa).split(Pattern.quote(pathSep)).last
Try(cpg.member.nameExact(fieldName).typeDecl.fullName.filterNot(_.contains("ANY")).toSet) match
case Failure(exception) =>
logger.warn("Unable to obtain name of member's parent type declaration", exception)
Expand Down Expand Up @@ -859,7 +860,7 @@ abstract class RecoverForXCompilationUnit[CompilationUnitType <: AstNode](
// TODO: This is more prone to giving dummy values as it does not do global look-ups
// but this is okay for now
val buf = mutable.ArrayBuffer.empty[String]
for (segment <- baseName.split(pathSep) ++ Array(fi.canonicalName)) {
for (segment <- baseName.split(Pattern.quote(pathSep)) ++ Array(fi.canonicalName)) {
val types =
if (buf.isEmpty) symbolTable.get(LocalVar(segment))
else buf.flatMap(t => symbolTable.get(LocalVar(s"$t$pathSep$segment"))).toSet
Expand Down Expand Up @@ -938,7 +939,7 @@ abstract class RecoverForXCompilationUnit[CompilationUnitType <: AstNode](
case ::(head: Call, Nil) if head.argumentOut.headOption.exists(symbolTable.contains) =>
symbolTable
.get(head.argumentOut.head)
.map(t => Seq(t, head.name, XTypeRecovery.DummyReturnType).mkString(pathSep.toString))
.map(t => Seq(t, head.name, XTypeRecovery.DummyReturnType).mkString(pathSep))
case ::(identifier: Identifier, Nil) if symbolTable.contains(identifier) =>
symbolTable.get(identifier)
case ::(head: Call, Nil) =>
Expand Down Expand Up @@ -1101,7 +1102,7 @@ abstract class RecoverForXCompilationUnit[CompilationUnitType <: AstNode](
columnNo: Option[Integer]
): NewMethodRef =
NewMethodRef()
.code(s"${baseName.map(_.appended(pathSep)).getOrElse("")}$funcName")
.code(s"${baseName.map(_ + pathSep).getOrElse("")}$funcName")
.methodFullName(methodFullName)
.lineNumber(lineNo)
.columnNumber(columnNo)
Expand Down

0 comments on commit 822b022

Please sign in to comment.