
io.joern.php2cpg.astcreation.AstCreator.scala Maven / Gradle / Ivy
package io.joern.php2cpg.astcreation
import io.joern.php2cpg.astcreation.AstCreator.{NameConstants, TypeConstants, operatorSymbols}
import io.joern.php2cpg.datastructures.{ArrayIndexTracker, Scope}
import io.joern.php2cpg.parser.Domain.PhpModifiers.containsAccessModifier
import io.joern.php2cpg.parser.Domain._
import io.joern.x2cpg.Ast.storeInDiffGraph
import io.joern.x2cpg.datastructures.Global
import io.joern.x2cpg.utils.AstPropertiesUtil.RootProperties
import io.joern.x2cpg.{Ast, AstCreatorBase, AstNodeBuilder}
import io.joern.x2cpg.Defines.{StaticInitMethodName, UnresolvedNamespace, UnresolvedSignature}
import io.joern.x2cpg.utils.NodeBuilders.{
newFieldIdentifierNode,
newIdentifierNode,
newMethodReturnNode,
newModifierNode,
newOperatorCallNode
}
import io.shiftleft.codepropertygraph.generated._
import io.shiftleft.codepropertygraph.generated.nodes.Call.PropertyDefaults
import io.shiftleft.codepropertygraph.generated.nodes.Local.{PropertyDefaults => LocalDefaults}
import io.shiftleft.codepropertygraph.generated.nodes._
import io.shiftleft.passes.IntervalKeyPool
import io.shiftleft.semanticcpg.language.types.structure.NamespaceTraversal
import org.slf4j.LoggerFactory
import overflowdb.BatchedUpdate
import io.joern.x2cpg.passes.frontend.MetaDataPass
class AstCreator(filename: String, phpAst: PhpFile, global: Global)
extends AstCreatorBase(filename)
with AstNodeBuilder[PhpNode, AstCreator] {
private val logger = LoggerFactory.getLogger(AstCreator.getClass)
private val scope = new Scope()
private val tmpKeyPool = new IntervalKeyPool(first = 0, last = Long.MaxValue)
private val globalNamespace = globalNamespaceBlock()
private def getNewTmpName(prefix: String = "tmp"): String = s"$prefix${tmpKeyPool.next.toString}"
override def createAst(): BatchedUpdate.DiffGraphBuilder = {
val ast = astForPhpFile(phpAst)
storeInDiffGraph(ast, diffGraph)
diffGraph
}
private def registerType(typ: String): String = {
if (typ != TypeConstants.Any) {
global.usedTypes.putIfAbsent(typ, true)
}
typ
}
private def flattenGlobalNamespaceStmt(stmt: PhpStmt): List[PhpStmt] = {
stmt match {
case namespace: PhpNamespaceStmt if namespace.name.isEmpty =>
namespace.stmts
case _ => stmt :: Nil
}
}
private def globalTypeDeclNode(file: PhpFile, globalNamespace: NewNamespaceBlock): NewTypeDecl = {
typeDeclNode(
file,
globalNamespace.name,
globalNamespace.fullName,
filename,
globalNamespace.code,
NodeTypes.NAMESPACE_BLOCK,
globalNamespace.fullName
)
}
private def globalMethodDeclStmt(file: PhpFile, bodyStmts: List[PhpStmt]): PhpMethodDecl = {
val modifiersList = List(ModifierTypes.VIRTUAL, ModifierTypes.PUBLIC, ModifierTypes.STATIC)
PhpMethodDecl(
name = PhpNameExpr(NamespaceTraversal.globalNamespaceName, file.attributes),
params = Nil,
modifiers = modifiersList,
returnType = None,
stmts = bodyStmts,
returnByRef = false,
namespacedName = None,
isClassMethod = false,
attributes = file.attributes
)
}
private def astForPhpFile(file: PhpFile): Ast = {
scope.pushNewScope(globalNamespace)
val (globalDeclStmts, globalMethodStmts) =
file.children.flatMap(flattenGlobalNamespaceStmt).partition(_.isInstanceOf[PhpConstStmt])
val globalMethodStmt = globalMethodDeclStmt(file, globalMethodStmts)
val globalTypeDeclStmt = PhpClassLikeStmt(
name = Some(PhpNameExpr(globalNamespace.name, file.attributes)),
modifiers = Nil,
extendsNames = Nil,
implementedInterfaces = Nil,
stmts = globalDeclStmts.appended(globalMethodStmt),
classLikeType = ClassLikeTypes.Class,
scalarType = None,
hasConstructor = false,
attributes = file.attributes
)
val globalTypeDeclAst = astForClassLikeStmt(globalTypeDeclStmt)
scope.popScope() // globalNamespace
Ast(globalNamespace).withChild(globalTypeDeclAst)
}
private def astForStmt(stmt: PhpStmt): Ast = {
stmt match {
case echoStmt: PhpEchoStmt => astForEchoStmt(echoStmt)
case methodDecl: PhpMethodDecl => astForMethodDecl(methodDecl)
case expr: PhpExpr => astForExpr(expr)
case breakStmt: PhpBreakStmt => astForBreakStmt(breakStmt)
case contStmt: PhpContinueStmt => astForContinueStmt(contStmt)
case whileStmt: PhpWhileStmt => astForWhileStmt(whileStmt)
case doStmt: PhpDoStmt => astForDoStmt(doStmt)
case forStmt: PhpForStmt => astForForStmt(forStmt)
case ifStmt: PhpIfStmt => astForIfStmt(ifStmt)
case switchStmt: PhpSwitchStmt => astForSwitchStmt(switchStmt)
case tryStmt: PhpTryStmt => astForTryStmt(tryStmt)
case returnStmt: PhpReturnStmt => astForReturnStmt(returnStmt)
case classLikeStmt: PhpClassLikeStmt => astForClassLikeStmt(classLikeStmt)
case gotoStmt: PhpGotoStmt => astForGotoStmt(gotoStmt)
case labelStmt: PhpLabelStmt => astForLabelStmt(labelStmt)
case namespace: PhpNamespaceStmt => astForNamespaceStmt(namespace)
case declareStmt: PhpDeclareStmt => astForDeclareStmt(declareStmt)
case _: NopStmt => Ast() // TODO This'll need to be updated when comments are added.
case haltStmt: PhpHaltCompilerStmt => astForHaltCompilerStmt(haltStmt)
case unsetStmt: PhpUnsetStmt => astForUnsetStmt(unsetStmt)
case globalStmt: PhpGlobalStmt => astForGlobalStmt(globalStmt)
case useStmt: PhpUseStmt => astForUseStmt(useStmt)
case groupUseStmt: PhpGroupUseStmt => astForGroupUseStmt(groupUseStmt)
case foreachStmt: PhpForeachStmt => astForForeachStmt(foreachStmt)
case traitUseStmt: PhpTraitUseStmt => astforTraitUseStmt(traitUseStmt)
case enumCase: PhpEnumCaseStmt => astForEnumCase(enumCase)
// TODO Figure out if this is breaking any assumptions that will cause issues later.
case staticStmt: PhpStaticStmt => Ast().withChildren(astsForStaticStmt(staticStmt))
case unhandled =>
logger.error(s"Unhandled stmt $unhandled in $filename")
???
}
}
private def astForEchoStmt(echoStmt: PhpEchoStmt): Ast = {
val args = echoStmt.exprs.map(astForExpr)
val code = s"echo ${args.map(_.rootCodeOrEmpty).mkString(",")}"
val callNode = newOperatorCallNode("echo", code, line = line(echoStmt))
callAst(callNode, args)
}
private def thisParamAstForMethod(originNode: PhpNode): Ast = {
val typeFullName = registerType(scope.getEnclosingTypeDeclTypeName.getOrElse(TypeConstants.Any))
val thisNode = parameterInNode(
originNode,
name = NameConstants.This,
code = NameConstants.This,
index = 0,
isVariadic = false,
evaluationStrategy = EvaluationStrategies.BY_SHARING,
typeFullName = typeFullName
).dynamicTypeHintFullName(typeFullName :: Nil)
// TODO Add dynamicTypeHintFullName to parameterInNode param list
scope.addToScope(NameConstants.This, thisNode)
Ast(thisNode)
}
private def thisIdentifier(lineNumber: Option[Integer]): NewIdentifier = {
val typ = scope.getEnclosingTypeDeclTypeName
newIdentifierNode(NameConstants.This, typ.getOrElse("ANY"), typ.toList, lineNumber)
.code(s"$$${NameConstants.This}")
}
private def setParamIndices(asts: Seq[Ast]): Seq[Ast] = {
asts.map(_.root).zipWithIndex.foreach {
case (Some(root: NewMethodParameterIn), idx) =>
root.index(idx + 1)
case (root, _) =>
logger.warn(s"Trying to set index for unsupported node $root")
}
asts
}
private def composeMethodFullName(methodName: String, isStatic: Boolean): String = {
if (methodName == NamespaceTraversal.globalNamespaceName) {
globalNamespace.fullName
} else {
val className = getTypeDeclPrefix
val methodDelimiter = if (isStatic) StaticMethodDelimiter else InstanceMethodDelimiter
val nameWithClass = List(className, Some(methodName)).flatten.mkString(methodDelimiter)
prependNamespacePrefix(nameWithClass)
}
}
private def astForMethodDecl(
decl: PhpMethodDecl,
bodyPrefixAsts: List[Ast] = Nil,
fullNameOverride: Option[String] = None,
isConstructor: Boolean = false
): Ast = {
val isStatic = decl.modifiers.contains(ModifierTypes.STATIC)
val thisParam = if (decl.isClassMethod && !isStatic) {
Option(thisParamAstForMethod(decl))
} else {
None
}
val methodName = decl.name.name
val fullName = fullNameOverride.getOrElse(composeMethodFullName(methodName, isStatic))
val signature = s"$UnresolvedSignature(${decl.params.size})"
val parameters = thisParam.toList ++ decl.params.zipWithIndex.map { case (param, idx) =>
astForParam(param, idx + 1)
}
val constructorModifier = Option.when(isConstructor)(ModifierTypes.CONSTRUCTOR)
val defaultAccessModifier = Option.unless(containsAccessModifier(decl.modifiers))(ModifierTypes.PUBLIC)
val allModifiers = constructorModifier ++: defaultAccessModifier ++: decl.modifiers
val modifiers = allModifiers.map(newModifierNode)
val modifierString = decl.modifiers match {
case Nil => ""
case mods => s"${mods.mkString(" ")} "
}
val methodCode = s"${modifierString}function $methodName(${parameters.map(_.rootCodeOrEmpty).mkString(",")})"
val method = methodNode(decl, methodName, methodCode, fullName, Some(signature), filename)
scope.pushNewScope(method)
val returnType = decl.returnType.map(_.name).getOrElse(TypeConstants.Any)
val methodBodyStmts = bodyPrefixAsts ++ decl.stmts.flatMap {
case staticStmt: PhpStaticStmt => astsForStaticStmt(staticStmt)
case stmt => astForStmt(stmt) :: Nil
}
val methodReturn = newMethodReturnNode(returnType, line = line(decl), column = None)
val methodBody = blockAst(blockNode(decl), methodBodyStmts)
scope.popScope()
methodAstWithAnnotations(method, parameters, methodBody, methodReturn, modifiers)
}
private def stmtBodyBlockAst(stmt: PhpStmtWithBody): Ast = {
val bodyBlock = blockNode(stmt)
val bodyStmtAsts = stmt.stmts.map(astForStmt)
Ast(bodyBlock).withChildren(bodyStmtAsts)
}
private def astForParam(param: PhpParam, index: Int): Ast = {
val evaluationStrategy =
if (param.byRef)
EvaluationStrategies.BY_REFERENCE
else
EvaluationStrategies.BY_VALUE
val typeFullName = registerType(param.paramType.map(_.name).getOrElse(TypeConstants.Any))
val byRefCodePrefix = if (param.byRef) "&" else ""
val code = s"$byRefCodePrefix$$${param.name}"
val paramNode = parameterInNode(param, param.name, code, index, param.isVariadic, evaluationStrategy, typeFullName)
scope.addToScope(param.name, paramNode)
Ast(paramNode)
}
private def astForExpr(expr: PhpExpr): Ast = {
expr match {
case funcCallExpr: PhpCallExpr => astForCall(funcCallExpr)
case variableExpr: PhpVariable => astForVariableExpr(variableExpr)
case nameExpr: PhpNameExpr => astForNameExpr(nameExpr)
case assignExpr: PhpAssignment => astForAssignment(assignExpr)
case scalarExpr: PhpScalar => astForScalar(scalarExpr)
case binaryOp: PhpBinaryOp => astForBinOp(binaryOp)
case unaryOp: PhpUnaryOp => astForUnaryOp(unaryOp)
case castExpr: PhpCast => astForCastExpr(castExpr)
case isSetExpr: PhpIsset => astForIsSetExpr(isSetExpr)
case printExpr: PhpPrint => astForPrintExpr(printExpr)
case ternaryOp: PhpTernaryOp => astForTernaryOp(ternaryOp)
case throwExpr: PhpThrowExpr => astForThrow(throwExpr)
case cloneExpr: PhpCloneExpr => astForClone(cloneExpr)
case emptyExpr: PhpEmptyExpr => astForEmpty(emptyExpr)
case evalExpr: PhpEvalExpr => astForEval(evalExpr)
case exitExpr: PhpExitExpr => astForExit(exitExpr)
case arrayExpr: PhpArrayExpr => astForArrayExpr(arrayExpr)
case listExpr: PhpListExpr => astForListExpr(listExpr)
case newExpr: PhpNewExpr => astForNewExpr(newExpr)
case matchExpr: PhpMatchExpr => astForMatchExpr(matchExpr)
case yieldExpr: PhpYieldExpr => astForYieldExpr(yieldExpr)
case closure: PhpClosureExpr => astForClosureExpr(closure)
case yieldFromExpr: PhpYieldFromExpr => astForYieldFromExpr(yieldFromExpr)
case classConstFetchExpr: PhpClassConstFetchExpr => astForClassConstFetchExpr(classConstFetchExpr)
case constFetchExpr: PhpConstFetchExpr => astForConstFetchExpr(constFetchExpr)
case arrayDimFetchExpr: PhpArrayDimFetchExpr => astForArrayDimFetchExpr(arrayDimFetchExpr)
case errorSuppressExpr: PhpErrorSuppressExpr => astForErrorSuppressExpr(errorSuppressExpr)
case instanceOfExpr: PhpInstanceOfExpr => astForInstanceOfExpr(instanceOfExpr)
case propertyFetchExpr: PhpPropertyFetchExpr => astForPropertyFetchExpr(propertyFetchExpr)
case includeExpr: PhpIncludeExpr => astForIncludeExpr(includeExpr)
case shellExecExpr: PhpShellExecExpr => astForShellExecExpr(shellExecExpr)
case null =>
logger.warn("expr was null")
???
case other => throw new NotImplementedError(s"unexpected expression '$other' of type ${other.getClass}")
}
}
private def intToLiteralAst(num: Int): Ast = {
Ast(NewLiteral().code(num.toString).typeFullName(registerType(TypeConstants.Int)))
}
private def astForBreakStmt(breakStmt: PhpBreakStmt): Ast = {
val code = breakStmt.num.map(num => s"break($num)").getOrElse("break")
val breakNode = controlStructureNode(breakStmt, ControlStructureTypes.BREAK, code)
val argument = breakStmt.num.map(intToLiteralAst)
controlStructureAst(breakNode, None, argument.toList)
}
private def astForContinueStmt(continueStmt: PhpContinueStmt): Ast = {
val code = continueStmt.num.map(num => s"continue($num)").getOrElse("continue")
val continueNode = controlStructureNode(continueStmt, ControlStructureTypes.CONTINUE, code)
val argument = continueStmt.num.map(intToLiteralAst)
controlStructureAst(continueNode, None, argument.toList)
}
private def astForWhileStmt(whileStmt: PhpWhileStmt): Ast = {
val condition = astForExpr(whileStmt.cond)
val lineNumber = line(whileStmt)
val code = s"while (${condition.rootCodeOrEmpty})"
val body = stmtBodyBlockAst(whileStmt)
whileAst(Option(condition), List(body), Option(code), lineNumber)
}
private def astForDoStmt(doStmt: PhpDoStmt): Ast = {
val condition = astForExpr(doStmt.cond)
val lineNumber = line(doStmt)
val code = s"do {...} while (${condition.rootCodeOrEmpty})"
val body = stmtBodyBlockAst(doStmt)
doWhileAst(Option(condition), List(body), Option(code), lineNumber)
}
private def astForForStmt(stmt: PhpForStmt): Ast = {
val lineNumber = line(stmt)
val initAsts = stmt.inits.map(astForExpr)
val conditionAsts = stmt.conditions.map(astForExpr)
val loopExprAsts = stmt.loopExprs.map(astForExpr)
val bodyAst = stmtBodyBlockAst(stmt)
val initCode = initAsts.map(_.rootCodeOrEmpty).mkString(",")
val conditionCode = conditionAsts.map(_.rootCodeOrEmpty).mkString(",")
val loopExprCode = loopExprAsts.map(_.rootCodeOrEmpty).mkString(",")
val forCode = s"for ($initCode;$conditionCode;$loopExprCode)"
val forNode = controlStructureNode(stmt, ControlStructureTypes.FOR, forCode)
forAst(forNode, Nil, initAsts, conditionAsts, loopExprAsts, bodyAst)
}
private def astForIfStmt(ifStmt: PhpIfStmt): Ast = {
val condition = astForExpr(ifStmt.cond)
val thenAst = stmtBodyBlockAst(ifStmt)
val elseAst = ifStmt.elseIfs match {
case Nil => ifStmt.elseStmt.map(els => stmtBodyBlockAst(els)).toList
case elseIf :: rest =>
val newIfStmt = PhpIfStmt(elseIf.cond, elseIf.stmts, rest, ifStmt.elseStmt, elseIf.attributes)
val wrappingBlock = blockNode(elseIf)
val wrappedAst = Ast(wrappingBlock).withChild(astForIfStmt(newIfStmt)) :: Nil
wrappedAst
}
val conditionCode = condition.rootCodeOrEmpty
val ifNode = controlStructureNode(ifStmt, ControlStructureTypes.IF, s"if ($conditionCode)")
controlStructureAst(ifNode, Option(condition), thenAst :: elseAst)
}
private def astForSwitchStmt(stmt: PhpSwitchStmt): Ast = {
val conditionAst = astForExpr(stmt.condition)
val switchNode =
controlStructureNode(stmt, ControlStructureTypes.SWITCH, s"switch (${conditionAst.rootCodeOrEmpty})")
val switchBodyBlock = blockNode(stmt)
val entryAsts = stmt.cases.flatMap(astsForSwitchCase)
val switchBody = Ast(switchBodyBlock).withChildren(entryAsts)
controlStructureAst(switchNode, Option(conditionAst), switchBody :: Nil)
}
private def astForTryStmt(stmt: PhpTryStmt): Ast = {
val tryBody = stmtBodyBlockAst(stmt)
val catches = stmt.catches.map(astForCatchStmt)
val finallyBody = stmt.finallyStmt.map(fin => stmtBodyBlockAst(fin))
val tryNode = controlStructureNode(stmt, ControlStructureTypes.TRY, "try { ... }")
tryCatchAst(tryNode, tryBody, catches, finallyBody)
}
private def astForReturnStmt(stmt: PhpReturnStmt): Ast = {
val maybeExprAst = stmt.expr.map(astForExpr)
val code = s"return ${maybeExprAst.map(_.rootCodeOrEmpty).getOrElse("")}"
val node = returnNode(stmt, code)
returnAst(node, maybeExprAst.toList)
}
private def astForClassLikeStmt(stmt: PhpClassLikeStmt): Ast = {
stmt.name match {
case None => astForAnonymousClass(stmt)
case Some(name) => astForNamedClass(stmt, name)
}
}
private def astForGotoStmt(stmt: PhpGotoStmt): Ast = {
val label = stmt.label.name
val code = s"goto $label"
val gotoNode = controlStructureNode(stmt, ControlStructureTypes.GOTO, code)
val jumpLabel = NewJumpLabel()
.name(label)
.code(label)
.lineNumber(line(stmt))
controlStructureAst(gotoNode, condition = None, children = Ast(jumpLabel) :: Nil)
}
private def astForLabelStmt(stmt: PhpLabelStmt): Ast = {
val label = stmt.label.name
val jumpTarget = NewJumpTarget()
.name(label)
.code(label)
.lineNumber(line(stmt))
Ast(jumpTarget)
}
private def astForNamespaceStmt(stmt: PhpNamespaceStmt): Ast = {
val name = stmt.name.map(_.name).getOrElse(NameConstants.Unknown)
val fullName = s"$filename:$name"
val namespaceBlock = NewNamespaceBlock()
.name(name)
.fullName(fullName)
scope.pushNewScope(namespaceBlock)
val bodyStmts = astsForClassLikeBody(stmt, stmt.stmts, createDefaultConstructor = false)
scope.popScope()
Ast(namespaceBlock).withChildren(bodyStmts)
}
private def astForDeclareStmt(stmt: PhpDeclareStmt): Ast = {
val declareAssignAsts = stmt.declares.map(astForDeclareItem)
val declareCode = s"${PhpOperators.declareFunc}(${declareAssignAsts.map(_.rootCodeOrEmpty).mkString(",")})"
val declareNode = newOperatorCallNode(PhpOperators.declareFunc, declareCode, line = line(stmt))
val declareAst = callAst(declareNode, declareAssignAsts)
stmt.stmts match {
case Some(stmtList) =>
val stmtAsts = stmtList.map(astForStmt)
Ast(blockNode(stmt))
.withChild(declareAst)
.withChildren(stmtAsts)
case None => declareAst
}
}
private def astForDeclareItem(item: PhpDeclareItem): Ast = {
val key = identifierNode(item, item.key.name, item.key.name, "ANY")
val value = astForExpr(item.value)
val code = s"${key.name}=${value.rootCodeOrEmpty}"
val declareAssignment = newOperatorCallNode(Operators.assignment, code, line = line(item))
callAst(declareAssignment, Ast(key) :: value :: Nil)
}
private def astForHaltCompilerStmt(stmt: PhpHaltCompilerStmt): Ast = {
val call = newOperatorCallNode(
NameConstants.HaltCompiler,
s"${NameConstants.HaltCompiler}()",
Some(TypeConstants.Void),
line(stmt),
column(stmt)
)
Ast(call)
}
private def astForUnsetStmt(stmt: PhpUnsetStmt): Ast = {
val name = PhpOperators.unset
val args = stmt.vars.map(astForExpr)
val code = s"$name(${args.map(_.rootCodeOrEmpty).mkString(", ")})"
val typeFullName = registerType(TypeConstants.Void)
val callNode = newOperatorCallNode(name, code, typeFullName = Option(typeFullName), line = line(stmt))
.methodFullName(PhpOperators.unset)
callAst(callNode, args)
}
private def astForGlobalStmt(stmt: PhpGlobalStmt): Ast = {
// This isn't an accurater representation of what `global` does, but with things like `global $$x` being possible,
// it's very difficult to figure out correct scopes for global variables.
val varsAsts = stmt.vars.map(astForExpr)
val code = s"${PhpOperators.global} ${varsAsts.map(_.rootCodeOrEmpty).mkString(", ")}"
val typeFullName = registerType(TypeConstants.Void)
val globalCallNode = newOperatorCallNode(PhpOperators.global, code, Option(typeFullName), line(stmt))
callAst(globalCallNode, varsAsts)
}
private def astForUseStmt(stmt: PhpUseStmt): Ast = {
// TODO Use useType + scope to get better name info
val imports = stmt.uses.map(astForUseUse(_))
wrapMultipleInBlock(imports, line(stmt))
}
private def astForGroupUseStmt(stmt: PhpGroupUseStmt): Ast = {
// TODO Use useType + scope to get better name info
val groupPrefix = s"${stmt.prefix.name}\\"
val imports = stmt.uses.map(astForUseUse(_, groupPrefix))
wrapMultipleInBlock(imports, line(stmt))
}
private def astForKeyValPair(key: PhpExpr, value: PhpExpr, lineNo: Option[Integer]): Ast = {
val keyAst = astForExpr(key)
val valueAst = astForExpr(value)
val code = s"${keyAst.rootCodeOrEmpty} => ${valueAst.rootCodeOrEmpty}"
val callNode = newOperatorCallNode(PhpOperators.doubleArrow, code, line = lineNo)
callAst(callNode, keyAst :: valueAst :: Nil)
}
private def astForForeachStmt(stmt: PhpForeachStmt): Ast = {
val iteratorAst = astForExpr(stmt.iterExpr)
val iterIdentifier = getTmpIdentifier(stmt, maybeTypeFullName = None, prefix = "iter_")
val assignItemTargetAst = stmt.keyVar match {
case Some(key) => astForKeyValPair(key, stmt.valueVar, line(stmt))
case None => astForExpr(stmt.valueVar)
}
// Initializer asts
// - Iterator assign
val iterValue = astForExpr(stmt.iterExpr)
val iteratorAssignAst = simpleAssignAst(Ast(iterIdentifier), iterValue, line(stmt))
// - Assigned item assign
val itemInitAst = getItemAssignAstForForeach(stmt, assignItemTargetAst, iterIdentifier.copy)
// Condition ast
val isNullName = PhpOperators.isNull
val valueAst = astForExpr(stmt.valueVar)
val isNullCode = s"$isNullName(${valueAst.rootCodeOrEmpty})"
val isNullCall = newOperatorCallNode(isNullName, isNullCode, Option(registerType(TypeConstants.Bool)), line(stmt))
.methodFullName(PhpOperators.isNull)
val notIsNull = newOperatorCallNode(Operators.logicalNot, s"!$isNullCode", line = line(stmt))
val isNullAst = callAst(isNullCall, valueAst :: Nil)
val conditionAst = callAst(notIsNull, isNullAst :: Nil)
// Update asts
val nextIterIdent = Ast(iterIdentifier.copy)
val nextSignature = "void()"
val nextCallCode = s"${nextIterIdent.rootCodeOrEmpty}->next()"
val nextCallNode = callNode(
stmt,
nextCallCode,
"next",
"Iterator.next",
DispatchTypes.DYNAMIC_DISPATCH,
Some(nextSignature),
Some(TypeConstants.Any)
)
val nextCallAst = callAst(nextCallNode, base = Option(nextIterIdent))
val itemUpdateAst = itemInitAst.root match {
case Some(initRoot: AstNodeNew) => itemInitAst.subTreeCopy(initRoot)
case _ =>
logger.warn(s"Could not copy foreach init ast in $filename")
Ast()
}
val bodyAst = stmtBodyBlockAst(stmt)
val ampPrefix = if (stmt.assignByRef) "&" else ""
val foreachCode = s"foreach (${iteratorAst.rootCodeOrEmpty} as $ampPrefix${assignItemTargetAst.rootCodeOrEmpty})"
val foreachNode = controlStructureNode(stmt, ControlStructureTypes.FOR, foreachCode)
Ast(foreachNode)
.withChild(wrapMultipleInBlock(iteratorAssignAst :: itemInitAst :: Nil, line(stmt)))
.withChild(conditionAst)
.withChild(wrapMultipleInBlock(nextCallAst :: itemUpdateAst :: Nil, line(stmt)))
.withChild(bodyAst)
.withConditionEdges(foreachNode, conditionAst.root.toList)
}
private def getItemAssignAstForForeach(
stmt: PhpForeachStmt,
assignItemTargetAst: Ast,
iteratorIdentifier: NewIdentifier
): Ast = {
val iteratorIdentifierAst = Ast(iteratorIdentifier)
val currentCallSignature = s"$UnresolvedSignature(0)"
val currentCallCode = s"${iteratorIdentifierAst.rootCodeOrEmpty}->current()"
val currentCallNode = callNode(
stmt,
currentCallCode,
"current",
"Iterator.current",
DispatchTypes.DYNAMIC_DISPATCH,
Some(currentCallSignature),
Some(TypeConstants.Any)
);
val currentCallAst = callAst(currentCallNode, base = Option(iteratorIdentifierAst))
val valueAst = if (stmt.assignByRef) {
val addressOfCode = s"&${currentCallAst.rootCodeOrEmpty}"
val addressOfCall = newOperatorCallNode(Operators.addressOf, addressOfCode, line = line(stmt))
callAst(addressOfCall, currentCallAst :: Nil)
} else {
currentCallAst
}
simpleAssignAst(assignItemTargetAst, valueAst, line(stmt))
}
private def simpleAssignAst(target: Ast, source: Ast, lineNo: Option[Integer]): Ast = {
val code = s"${target.rootCodeOrEmpty} = ${source.rootCodeOrEmpty}"
val callNode = newOperatorCallNode(Operators.assignment, code, line = lineNo)
callAst(callNode, target :: source :: Nil)
}
private def astforTraitUseStmt(stmt: PhpTraitUseStmt): Ast = {
// TODO Actually implement this
Ast()
}
private def astForUseUse(stmt: PhpUseUse, namePrefix: String = ""): Ast = {
val originalName = s"$namePrefix${stmt.originalName.name}"
val aliasCode = stmt.alias.map(alias => s" as ${alias.name}").getOrElse("")
val typeCode = stmt.useType match {
case PhpUseType.Function => s"function "
case PhpUseType.Constant => s"const "
case _ => ""
}
val code = s"use $typeCode$originalName$aliasCode"
val importNode = NewImport()
.importedEntity(originalName)
.importedAs(stmt.alias.map(_.name))
.isExplicit(true)
.code(code)
Ast(importNode)
}
private def astsForStaticStmt(stmt: PhpStaticStmt): List[Ast] = {
stmt.vars.flatMap { staticVarDecl =>
val variableAst = astForVariableExpr(staticVarDecl.variable)
val maybeValueAst = staticVarDecl.defaultValue.map(astForExpr)
val code = variableAst.rootCode.getOrElse(NameConstants.Unknown)
val name = variableAst.root match {
case Some(identifier: NewIdentifier) => identifier.name
case _ => code
}
val local = localNode(stmt, name, s"static $code", variableAst.rootType.getOrElse(TypeConstants.Any))
scope.addToScope(local.name, local)
variableAst.root.collect { case identifier: NewIdentifier =>
diffGraph.addEdge(identifier, local, EdgeTypes.REF)
}
val defaultAssignAst = maybeValueAst.map { valueAst =>
val valueCode = s"static $code = ${valueAst.rootCodeOrEmpty}"
val assignNode = newOperatorCallNode(Operators.assignment, valueCode, line = line(stmt))
callAst(assignNode, variableAst :: valueAst :: Nil)
}
Ast(local) :: defaultAssignAst.toList
}
}
private def astForAnonymousClass(stmt: PhpClassLikeStmt): Ast = {
// TODO
Ast()
}
def codeForClassStmt(stmt: PhpClassLikeStmt, name: PhpNameExpr): String = {
// TODO Extend for anonymous classes
val extendsString = stmt.extendsNames match {
case Nil => ""
case names => s" extends ${names.map(_.name).mkString(", ")}"
}
val implementsString =
if (stmt.implementedInterfaces.isEmpty)
""
else
s" implements ${stmt.implementedInterfaces.map(_.name).mkString(", ")}"
s"${stmt.classLikeType} ${name.name}$extendsString$implementsString"
}
private def astForNamedClass(stmt: PhpClassLikeStmt, name: PhpNameExpr): Ast = {
val inheritsFrom = (stmt.extendsNames ++ stmt.implementedInterfaces).map(_.name)
val code = codeForClassStmt(stmt, name)
val fullName =
if (name.name == NamespaceTraversal.globalNamespaceName)
globalNamespace.fullName
else {
prependNamespacePrefix(name.name)
}
val typeDecl = typeDeclNode(stmt, name.name, fullName, filename, code, inherits = inheritsFrom)
val createDefaultConstructor = stmt.hasConstructor
scope.pushNewScope(typeDecl)
val bodyStmts = astsForClassLikeBody(stmt, stmt.stmts, createDefaultConstructor)
val modifiers = stmt.modifiers.map(newModifierNode).map(Ast(_))
scope.popScope()
Ast(typeDecl).withChildren(modifiers).withChildren(bodyStmts)
}
private def astForStaticAndConstInits: Option[Ast] = {
scope.getConstAndStaticInits match {
case Nil => None
case inits =>
val signature = s"${TypeConstants.Void}()"
val fullName = composeMethodFullName(StaticInitMethodName, isStatic = true)
val ast = staticInitMethodAst(
inits,
fullName,
Option(signature),
registerType(TypeConstants.Void),
fileName = Some(filename)
)
Option(ast)
}
}
private def astsForClassLikeBody(
classLike: PhpStmt,
bodyStmts: List[PhpStmt],
createDefaultConstructor: Boolean
): List[Ast] = {
val classConsts = bodyStmts.collect { case cs: PhpConstStmt => cs }.flatMap(astsForConstStmt)
val properties = bodyStmts.collect { case cp: PhpPropertyStmt => cp }.flatMap(astsForPropertyStmt)
val explicitConstructorAst = bodyStmts.collectFirst {
case m: PhpMethodDecl if m.name.name == ConstructorMethodName => astForConstructor(m)
}
val constructorAst =
explicitConstructorAst.orElse(Option.when(createDefaultConstructor)(defaultConstructorAst(classLike)))
val otherBodyStmts = bodyStmts.flatMap {
case _: PhpConstStmt => None // Handled above
case _: PhpPropertyStmt => None // Handled above
case method: PhpMethodDecl if method.name.name == ConstructorMethodName => None // Handled above
// Not all statements are supported in class bodies, but since this is re-used for namespaces
// we allow that here.
case stmt => Some(astForStmt(stmt))
}
val clinitAst = astForStaticAndConstInits
val anonymousMethodAsts = scope.getAndClearAnonymousMethods
List(classConsts, properties, clinitAst, constructorAst, anonymousMethodAsts, otherBodyStmts).flatten
}
private def astForConstructor(constructorDecl: PhpMethodDecl): Ast = {
val fieldInits = scope.getFieldInits
astForMethodDecl(constructorDecl, fieldInits, isConstructor = true)
}
private def prependNamespacePrefix(name: String): String = {
scope.getEnclosingNamespaceNames.filterNot(_ == NamespaceTraversal.globalNamespaceName) match {
case Nil => name
case names => names.appended(name).mkString(NamespaceDelimiter)
}
}
private def getTypeDeclPrefix: Option[String] = {
scope.getEnclosingTypeDeclTypeName
.filterNot(_ == NamespaceTraversal.globalNamespaceName)
}
private def defaultConstructorAst(originNode: PhpNode): Ast = {
val fullName = composeMethodFullName(ConstructorMethodName, isStatic = false)
val signature = s"$UnresolvedSignature(0)"
val modifiers = List(ModifierTypes.VIRTUAL, ModifierTypes.PUBLIC, ModifierTypes.CONSTRUCTOR).map(newModifierNode)
val thisParam = thisParamAstForMethod(originNode)
val method = methodNode(originNode, ConstructorMethodName, fullName, fullName, Some(signature), filename)
val methodBody = blockAst(blockNode(originNode), scope.getFieldInits)
val methodReturn = newMethodReturnNode(TypeConstants.Any, line = None, column = None)
methodAstWithAnnotations(method, thisParam :: Nil, methodBody, methodReturn, modifiers)
}
private def astForMemberAssignment(memberNode: NewMember, valueExpr: PhpExpr, isField: Boolean): Ast = {
val targetAst = if (isField) {
val code = s"$$this->${memberNode.name}"
val fieldAccessNode = newOperatorCallNode(Operators.fieldAccess, code, line = memberNode.lineNumber)
val identifier = thisIdentifier(memberNode.lineNumber)
val thisParam = scope.lookupVariable(NameConstants.This)
val fieldIdentifier = newFieldIdentifierNode(memberNode.name, memberNode.lineNumber)
callAst(fieldAccessNode, List(identifier, fieldIdentifier).map(Ast(_))).withRefEdges(identifier, thisParam.toList)
} else {
val identifierCode = memberNode.code.replaceAll("const ", "").replaceAll("case ", "")
val typeFullName = Option(memberNode.typeFullName).map(registerType)
val identifier = newIdentifierNode(memberNode.name, typeFullName.getOrElse("ANY"))
.code(identifierCode)
Ast(identifier).withRefEdge(identifier, memberNode)
}
val value = astForExpr(valueExpr)
val assignmentCode = s"${targetAst.rootCodeOrEmpty} = ${value.rootCodeOrEmpty}"
val callNode = newOperatorCallNode(Operators.assignment, assignmentCode, line = memberNode.lineNumber)
callAst(callNode, List(targetAst, value))
}
private def astsForConstStmt(stmt: PhpConstStmt): List[Ast] = {
stmt.consts.map { constDecl =>
val finalModifier = Ast(newModifierNode(ModifierTypes.FINAL))
// `final const` is not allowed, so this is a safe way to represent constants in the CPG
val modifierAsts = finalModifier :: stmt.modifiers.map(newModifierNode).map(Ast(_))
val name = constDecl.name.name
val code = s"const $name"
val someValue = Option(constDecl.value)
astForConstOrFieldValue(stmt, name, code, someValue, scope.addConstOrStaticInitToScope, isField = false)
.withChildren(modifierAsts)
}
}
private def astForEnumCase(stmt: PhpEnumCaseStmt): Ast = {
val finalModifier = Ast(newModifierNode(ModifierTypes.FINAL))
val name = stmt.name.name
val code = s"case $name"
astForConstOrFieldValue(stmt, name, code, stmt.expr, scope.addConstOrStaticInitToScope, isField = false)
.withChild(finalModifier)
}
private def astsForPropertyStmt(stmt: PhpPropertyStmt): List[Ast] = {
stmt.variables.map { varDecl =>
val modifierAsts = stmt.modifiers.map(newModifierNode).map(Ast(_))
val name = varDecl.name.name
astForConstOrFieldValue(stmt, name, s"$$$name", varDecl.defaultValue, scope.addFieldInitToScope, isField = true)
.withChildren(modifierAsts)
}
}
private def astForConstOrFieldValue(
originNode: PhpNode,
name: String,
code: String,
value: Option[PhpExpr],
addToScope: Ast => Unit,
isField: Boolean
): Ast = {
val member = memberNode(originNode, name, code, TypeConstants.Any)
value match {
case Some(v) =>
val assignAst = astForMemberAssignment(member, v, isField)
addToScope(assignAst)
case None => // Nothing to do here
}
Ast(member)
}
private def astForCatchStmt(stmt: PhpCatchStmt): Ast = {
// TODO Add variable at some point. Current implementation is consistent with C++.
stmtBodyBlockAst(stmt)
}
private def astsForSwitchCase(caseStmt: PhpCaseStmt): List[Ast] = {
val maybeConditionAst = caseStmt.condition.map(astForExpr)
val jumpTarget = maybeConditionAst match {
case Some(conditionAst) => NewJumpTarget().name("case").code(s"case ${conditionAst.rootCodeOrEmpty}")
case None => NewJumpTarget().name("default").code("default")
}
jumpTarget.lineNumber(line(caseStmt))
val stmtAsts = caseStmt.stmts.map(astForStmt)
Ast(jumpTarget) :: stmtAsts
}
private def codeForMethodCall(call: PhpCallExpr, targetAst: Ast, name: String): String = {
val callOperator = if (call.isNullSafe) "?->" else "->"
s"${targetAst.rootCodeOrEmpty}$callOperator$name"
}
private def codeForStaticMethodCall(call: PhpCallExpr, name: String): String = {
val className =
call.target
.map(astForExpr)
.map(_.rootCode.getOrElse(UnresolvedNamespace))
.getOrElse(UnresolvedNamespace)
s"$className::$name"
}
private def astForCall(call: PhpCallExpr): Ast = {
val arguments = call.args.map(astForCallArg)
val targetAst = Option.unless(call.isStatic)(call.target.map(astForExpr)).flatten
val nameAst = Option.unless(call.methodName.isInstanceOf[PhpNameExpr])(astForExpr(call.methodName))
val name =
nameAst
.map(_.rootCodeOrEmpty)
.getOrElse(call.methodName match {
case nameExpr: PhpNameExpr => nameExpr.name
case other =>
logger.error(s"Found unexpected call target type: Crash for now to handle properly later: $other")
???
})
val argsCode = arguments.map(_.rootCodeOrEmpty).mkString(",")
val codePrefix =
if (!call.isStatic && targetAst.isDefined)
codeForMethodCall(call, targetAst.get, name)
else if (call.isStatic)
codeForStaticMethodCall(call, name)
else
name
val code = s"$codePrefix($argsCode)"
val dispatchType =
if (call.isStatic || call.target.isEmpty)
DispatchTypes.STATIC_DISPATCH
else
DispatchTypes.DYNAMIC_DISPATCH
val fullName = call.target match {
// Static method call with a known class name
case Some(nameExpr: PhpNameExpr) if call.isStatic =>
s"${nameExpr.name}${StaticMethodDelimiter}$name"
case None if PhpBuiltins.FuncNames.contains(name) =>
// No signature/namespace for MFN for builtin functions to ensure stable names as type info improves.
name
// Function call
case None if !PhpBuiltins.FuncNames.contains(name) =>
composeMethodFullName(name, call.isStatic)
// Other method calls. Need more type info for these.
case _ => PropertyDefaults.MethodFullName
}
// Use method signature for methods that can be linked to avoid varargs issue.
val signature = s"$UnresolvedSignature(${call.args.size})"
val callRoot = callNode(call, code, name, fullName, dispatchType, Some(signature), Some(TypeConstants.Any))
val receiverAst = (targetAst, nameAst) match {
case (Some(target), Some(n)) =>
val fieldAccess = newOperatorCallNode(Operators.fieldAccess, codePrefix, line = line(call))
Option(callAst(fieldAccess, target :: n :: Nil))
case (Some(target), None) => Option(target)
case (None, Some(n)) => Option(n)
case (None, None) => None
}
callAst(callRoot, arguments, base = receiverAst)
}
private def astForCallArg(arg: PhpArgument): Ast = {
arg match {
case PhpArg(expr, _, _, _, _) =>
astForExpr(expr)
case _: PhpVariadicPlaceholder =>
val identifier = identifierNode(arg, "...", "...", registerType(TypeConstants.VariadicPlaceholder))
Ast(identifier)
}
}
private def astForVariableExpr(variable: PhpVariable): Ast = {
// TODO Need to figure out variable variables. Maybe represent as some kind of call?
val valueAst = astForExpr(variable.value)
valueAst.root.collect { case root: ExpressionNew =>
root.code = s"$$${root.code}"
}
valueAst.root.collect { case root: NewIdentifier =>
root.lineNumber = line(variable)
}
valueAst
}
private def astForNameExpr(expr: PhpNameExpr): Ast = {
val identifier = identifierNode(expr, expr.name, expr.name, TypeConstants.Any)
scope.lookupVariable(identifier.name).foreach { declaringNode =>
diffGraph.addEdge(identifier, declaringNode, EdgeTypes.REF)
}
Ast(identifier)
}
/** This is used to rewrite the short form $xs[] = as array_push($xs, ) to avoid having to
* handle the empty array access operator as a special case in the dataflow engine.
*
* This representation is technically wrong in the case where the shorthand is used to initialise a new array (since
* PHP expects the first argument to array_push to be an existing array). This shouldn't affect dataflow, however.
*/
private def astForEmptyArrayDimAssign(assignment: PhpAssignment, arrayDimFetch: PhpArrayDimFetchExpr): Ast = {
val attrs = assignment.attributes
val arrayPushArgs = List(arrayDimFetch.variable, assignment.source).map(PhpArg(_))
val arrayPushCall = PhpCallExpr(
target = None,
methodName = PhpNameExpr("array_push", attrs),
args = arrayPushArgs,
isNullSafe = false,
isStatic = true,
attributes = attrs
)
val arrayPushAst = astForCall(arrayPushCall)
arrayPushAst.root.collect { case astRoot: NewCall =>
val args =
arrayPushAst.argEdges
.filter(_.src == astRoot)
.map(_.dst)
.collect { case arg: ExpressionNew => arg }
.sortBy(_.argumentIndex)
if (args.size != 2) {
val position = s"${line(assignment).getOrElse("")}:${filename}"
logger.warn(s"Expected 2 call args for emptyArrayDimAssign. Not resetting code: ${position}")
} else {
val codeOverride = s"${args.head.code}[] = ${args.last.code}"
astRoot.code(codeOverride)
}
}
arrayPushAst
}
private def astForAssignment(assignment: PhpAssignment): Ast = {
assignment.target match {
case arrayDimFetch: PhpArrayDimFetchExpr if arrayDimFetch.dimension.isEmpty =>
// Rewrite `$xs[] = ` as `array_push($xs, )` to simplify finding dataflows.
astForEmptyArrayDimAssign(assignment, arrayDimFetch)
case _ =>
val operatorName = assignment.assignOp
val targetAst = astForExpr(assignment.target)
val sourceAst = astForExpr(assignment.source)
// TODO Handle ref assigns properly (if needed).
val refSymbol = if (assignment.isRefAssign) "&" else ""
val symbol = operatorSymbols.getOrElse(assignment.assignOp, assignment.assignOp)
val code = s"${targetAst.rootCodeOrEmpty} $symbol $refSymbol${sourceAst.rootCodeOrEmpty}"
val callNode = newOperatorCallNode(operatorName, code, line = line(assignment))
callAst(callNode, List(targetAst, sourceAst))
}
}
private def astForScalar(scalar: PhpScalar): Ast = {
scalar match {
case PhpString(value, _) =>
Ast(NewLiteral().code(value).typeFullName(registerType(TypeConstants.String)).lineNumber(line(scalar)))
case PhpInt(value, _) =>
Ast(NewLiteral().code(value).typeFullName(registerType(TypeConstants.Int)).lineNumber(line(scalar)))
case PhpFloat(value, _) =>
Ast(NewLiteral().code(value).typeFullName(registerType(TypeConstants.Float)).lineNumber(line(scalar)))
case PhpEncapsed(parts, _) =>
val callNode =
newOperatorCallNode(PhpOperators.encaps, code = /* TODO */ PhpOperators.encaps, line = line(scalar))
val args = parts.map(astForExpr)
callAst(callNode, args)
case PhpEncapsedPart(value, _) =>
Ast(NewLiteral().code(value).typeFullName(registerType(TypeConstants.String)).lineNumber(line(scalar)))
case null =>
logger.warn("scalar was null")
???
}
}
private def astForBinOp(binOp: PhpBinaryOp): Ast = {
val leftAst = astForExpr(binOp.left)
val rightAst = astForExpr(binOp.right)
val symbol = operatorSymbols.getOrElse(binOp.operator, binOp.operator)
val code = s"${leftAst.rootCodeOrEmpty} $symbol ${rightAst.rootCodeOrEmpty}"
val callNode = newOperatorCallNode(binOp.operator, code, line = line(binOp))
callAst(callNode, List(leftAst, rightAst))
}
private def isPostfixOperator(operator: String): Boolean = {
Set(Operators.postDecrement, Operators.postIncrement).contains(operator)
}
private def astForUnaryOp(unaryOp: PhpUnaryOp): Ast = {
val exprAst = astForExpr(unaryOp.expr)
val symbol = operatorSymbols.getOrElse(unaryOp.operator, unaryOp.operator)
val code =
if (isPostfixOperator(unaryOp.operator))
s"${exprAst.rootCodeOrEmpty}$symbol"
else
s"$symbol${exprAst.rootCodeOrEmpty}"
val callNode = newOperatorCallNode(unaryOp.operator, code, line = line(unaryOp))
callAst(callNode, exprAst :: Nil)
}
private def astForCastExpr(castExpr: PhpCast): Ast = {
val typeFullName = registerType(castExpr.typ)
val typ = typeRefNode(castExpr, castExpr.typ, typeFullName)
val expr = astForExpr(castExpr.expr)
val codeStr = s"(${castExpr.typ}) ${expr.rootCodeOrEmpty}"
val callNode = newOperatorCallNode(name = Operators.cast, codeStr, Option(typeFullName), line(castExpr))
callAst(callNode, Ast(typ) :: expr :: Nil)
}
private def astForIsSetExpr(isSetExpr: PhpIsset): Ast = {
val name = PhpOperators.issetFunc
val args = isSetExpr.vars.map(astForExpr)
val code = s"$name(${args.map(_.rootCodeOrEmpty).mkString(",")})"
val typeFullName = registerType(TypeConstants.Bool)
val callNode =
newOperatorCallNode(name, code, typeFullName = Some(typeFullName), line = line(isSetExpr))
.methodFullName(PhpOperators.issetFunc)
callAst(callNode, args)
}
private def astForPrintExpr(printExpr: PhpPrint): Ast = {
val name = PhpOperators.printFunc
val arg = astForExpr(printExpr.expr)
val code = s"$name(${arg.rootCodeOrEmpty})"
val typeFullName = registerType(TypeConstants.Int)
val callNode =
newOperatorCallNode(name, code, typeFullName = Some(typeFullName), line = line(printExpr))
.methodFullName(PhpOperators.printFunc)
callAst(callNode, arg :: Nil)
}
private def astForTernaryOp(ternaryOp: PhpTernaryOp): Ast = {
val conditionAst = astForExpr(ternaryOp.condition)
val maybeThenAst = ternaryOp.thenExpr.map(astForExpr)
val elseAst = astForExpr(ternaryOp.elseExpr)
val operatorName = if (maybeThenAst.isDefined) Operators.conditional else PhpOperators.elvisOp
val code = maybeThenAst match {
case Some(thenAst) => s"${conditionAst.rootCodeOrEmpty} ? ${thenAst.rootCodeOrEmpty} : ${elseAst.rootCodeOrEmpty}"
case None => s"${conditionAst.rootCodeOrEmpty} ?: ${elseAst.rootCodeOrEmpty}"
}
val callNode = newOperatorCallNode(operatorName, code, line = line(ternaryOp))
val args = List(Option(conditionAst), maybeThenAst, Option(elseAst)).flatten
callAst(callNode, args)
}
private def astForThrow(expr: PhpThrowExpr): Ast = {
val thrownExpr = astForExpr(expr.expr)
val code = s"throw ${thrownExpr.rootCodeOrEmpty}"
val throwNode = controlStructureNode(expr, ControlStructureTypes.THROW, code)
Ast(throwNode).withChild(thrownExpr)
}
private def astForClone(expr: PhpCloneExpr): Ast = {
val name = PhpOperators.cloneFunc
val argAst = astForExpr(expr.expr)
val argType = argAst.rootType.map(registerType).orElse(Some(TypeConstants.Any))
val code = s"$name ${argAst.rootCodeOrEmpty}"
val callNode = newOperatorCallNode(name, code, argType, line(expr))
.methodFullName(PhpOperators.cloneFunc)
callAst(callNode, argAst :: Nil)
}
private def astForEmpty(expr: PhpEmptyExpr): Ast = {
val name = PhpOperators.emptyFunc
val argAst = astForExpr(expr.expr)
val code = s"$name(${argAst.rootCodeOrEmpty})"
val typeFullName = registerType(TypeConstants.Bool)
val callNode =
newOperatorCallNode(name, code, typeFullName = Some(typeFullName), line = line(expr))
.methodFullName(PhpOperators.emptyFunc)
callAst(callNode, argAst :: Nil)
}
private def astForEval(expr: PhpEvalExpr): Ast = {
val name = PhpOperators.evalFunc
val argAst = astForExpr(expr.expr)
val code = s"$name(${argAst.rootCodeOrEmpty})"
val typeFullName = registerType(TypeConstants.Bool)
val callNode =
newOperatorCallNode(name, code, typeFullName = Some(typeFullName), line = line(expr))
.methodFullName(PhpOperators.evalFunc)
callAst(callNode, argAst :: Nil)
}
private def astForExit(expr: PhpExitExpr): Ast = {
val name = PhpOperators.exitFunc
val args = expr.expr.map(astForExpr)
val code = s"$name(${args.map(_.rootCodeOrEmpty).getOrElse("")})"
val typeFullName = registerType(TypeConstants.Void)
val callNode = newOperatorCallNode(name, code, Some(typeFullName), line(expr))
.methodFullName(PhpOperators.exitFunc)
callAst(callNode, args.toList)
}
private def getTmpIdentifier(
originNode: PhpNode,
maybeTypeFullName: Option[String],
prefix: String = ""
): NewIdentifier = {
val name = s"$prefix${getNewTmpName()}"
val typeFullName = maybeTypeFullName.map(registerType).getOrElse(TypeConstants.Any)
identifierNode(originNode, name, s"$$$name", typeFullName)
}
private def astForArrayExpr(expr: PhpArrayExpr): Ast = {
val idxTracker = new ArrayIndexTracker
val typeFullName = registerType(TypeConstants.Array)
val tmpIdentifier = getTmpIdentifier(expr, Some(typeFullName))
val itemAssignments = expr.items.flatMap {
case Some(item) => Option(assignForArrayItem(item, tmpIdentifier.name, idxTracker))
case None =>
idxTracker.next // Skip an index
None
}
val arrayBlock = blockNode(expr)
Ast(arrayBlock)
.withChildren(itemAssignments)
.withChild(Ast(tmpIdentifier))
}
private def astForListExpr(expr: PhpListExpr): Ast = {
/* TODO: Handling list in a way that will actually work with dataflow tracking is somewhat more complicated than
* this and will likely need a fairly ugly lowering.
*
* In short, the case:
* list($a, $b) = $arr;
* can be lowered to:
* $a = $arr[0];
* $b = $arr[1];
*
* the case:
* list("id" => $a, "name" => $b) = $arr;
* can be lowered to:
* $a = $arr["id"];
* $b = $arr["name"];
*
* and the case:
* foreach ($arr as list($a, $b)) { ... }
* can be lowered as above for each $arr[i];
*
* The below is just a placeholder to prevent crashes while figuring out the cleanest way to
* implement the above lowering or to think of a better way to do it.
*/
val name = PhpOperators.listFunc
val args = expr.items.flatten.map { item => astForExpr(item.value) }
val listCode = s"$name(${args.map(_.rootCodeOrEmpty).mkString(",")})"
val listNode = newOperatorCallNode(name, listCode, line = line(expr))
.methodFullName(PhpOperators.listFunc)
callAst(listNode, args)
}
private def astForNewExpr(expr: PhpNewExpr): Ast = {
expr.className match {
case classLikeStmt: PhpClassLikeStmt =>
astForAnonymousClassInstantiation(expr, classLikeStmt)
case classNameExpr: PhpExpr =>
astForSimpleNewExpr(expr, classNameExpr)
case other =>
throw new NotImplementedError(s"unexpected expression '$other' of type ${other.getClass}")
}
}
private def astForMatchExpr(expr: PhpMatchExpr): Ast = {
val conditionAst = astForExpr(expr.condition)
val matchNode = controlStructureNode(expr, ControlStructureTypes.MATCH, s"match (${conditionAst.rootCodeOrEmpty})")
val matchBodyBlock = blockNode(expr)
val armsAsts = expr.matchArms.flatMap(astsForMatchArm)
val matchBody = Ast(matchBodyBlock).withChildren(armsAsts)
controlStructureAst(matchNode, Option(conditionAst), matchBody :: Nil)
}
private def astsForMatchArm(matchArm: PhpMatchArm): List[Ast] = {
// TODO Don't just throw away the condition asts here (also for switch cases)
val targets = matchArm.conditions.map { condition =>
val conditionAst = astForExpr(condition)
// In PHP cases aren't labeled with `case`, but this is used by the CFG creator to differentiate between
// case/default labels and other labels.
val code = s"case ${conditionAst.rootCode.getOrElse(NameConstants.Unknown)}"
NewJumpTarget().name(code).code(code).lineNumber(line(condition))
}
val defaultLabel = Option.when(matchArm.isDefault)(
NewJumpTarget().name(NameConstants.Default).code(NameConstants.Default).lineNumber(line(matchArm))
)
val targetAsts = (targets ++ defaultLabel.toList).map(Ast(_))
val bodyAst = astForExpr(matchArm.body)
targetAsts :+ bodyAst
}
private def astForYieldExpr(expr: PhpYieldExpr): Ast = {
val maybeKey = expr.key.map(astForExpr)
val maybeVal = expr.value.map(astForExpr)
val code = (maybeKey, maybeVal) match {
case (Some(key), Some(value)) =>
s"yield ${key.rootCodeOrEmpty} => ${value.rootCodeOrEmpty}"
case _ =>
s"yield ${maybeKey.map(_.rootCodeOrEmpty).getOrElse("")}${maybeVal.map(_.rootCodeOrEmpty).getOrElse("")}".trim
}
val yieldNode = controlStructureNode(expr, ControlStructureTypes.YIELD, code)
Ast(yieldNode)
.withChildren(maybeKey.toList)
.withChildren(maybeVal.toList)
}
private def astForClosureExpr(closureExpr: PhpClosureExpr): Ast = {
val methodName = scope.getScopedClosureName
val methodRef = methodRefNode(closureExpr, methodName, methodName, TypeConstants.Any)
val localsForUses = closureExpr.uses.flatMap { closureUse =>
val variableAst = astForExpr(closureUse.variable)
val codePref = if (closureUse.byRef) "&" else ""
variableAst.root match {
case Some(identifier: NewIdentifier) =>
// This is the expected case and is handled well
Some(localNode(closureExpr, identifier.name, codePref ++ identifier.code, TypeConstants.Any))
case Some(expr: ExpressionNew) =>
// Results here may be bad, but its' the best we're likely to do
Some(localNode(closureExpr, expr.code, codePref ++ expr.code, TypeConstants.Any))
case Some(other) =>
// This should never happen
logger.warn(s"Found ast '$other' for closure use in $filename")
None
case None =>
// This should never happen
logger.warn(s"Found empty ast for closure use in $filename")
None
}
}
// Add closure bindings to diffgraph
localsForUses.foreach { local =>
val closureBindingId = s"$filename:$methodName:${local.name}"
local.closureBindingId(closureBindingId)
scope.addToScope(local.name, local)
val closureBindingNode = NewClosureBinding()
.closureBindingId(closureBindingId)
.closureOriginalName(local.name)
.evaluationStrategy(EvaluationStrategies.BY_SHARING)
// The ref edge to the captured local is added in the ClosureRefPass
diffGraph.addNode(closureBindingNode)
diffGraph.addEdge(methodRef, closureBindingNode, EdgeTypes.CAPTURE)
}
// Create method for closure
val name = PhpNameExpr(methodName, closureExpr.attributes)
// TODO Check for static modifier
val modifiers = if (closureExpr.isStatic) ModifierTypes.STATIC :: Nil else Nil
val methodDecl = PhpMethodDecl(
name,
closureExpr.params,
modifiers,
closureExpr.returnType,
closureExpr.stmts,
closureExpr.returnByRef,
namespacedName = None,
isClassMethod = closureExpr.isStatic,
closureExpr.attributes
)
val methodAst = astForMethodDecl(methodDecl, localsForUses.map(Ast(_)), Option(methodName))
val usesCode = localsForUses match {
case Nil => ""
case locals => s" use(${locals.map(_.code).mkString(", ")})"
}
methodAst.root.collect { case method: NewMethod => method }.foreach { methodNode =>
methodNode.code(methodNode.code ++ usesCode)
}
// Add method to scope to be attached to typeDecl later
scope.addAnonymousMethod(methodAst)
Ast(methodRef)
}
private def astForYieldFromExpr(expr: PhpYieldFromExpr): Ast = {
// TODO This is currently only distinguishable from yield by the code field. Decide whether to treat YIELD_FROM
// separately or whether to lower this to a foreach with regular yields.
val exprAst = astForExpr(expr.expr)
val code = s"yield from ${exprAst.rootCodeOrEmpty}"
val yieldNode = controlStructureNode(expr, ControlStructureTypes.YIELD, code)
Ast(yieldNode)
.withChild(exprAst)
}
private def astForAnonymousClassInstantiation(expr: PhpNewExpr, classLikeStmt: PhpClassLikeStmt): Ast = {
// TODO Do this along with other anonymous class support
Ast()
}
private def astForSimpleNewExpr(expr: PhpNewExpr, classNameExpr: PhpExpr): Ast = {
val (maybeNameAst, className) = classNameExpr match {
case nameExpr: PhpNameExpr =>
(None, nameExpr.name)
case expr: PhpExpr =>
val ast = astForExpr(expr)
// The name doesn't make sense in this case, but the AST will be more useful
val name = ast.rootCode.getOrElse(NameConstants.Unknown)
(Option(ast), name)
}
val tmpIdentifier = getTmpIdentifier(expr, Option(className))
// Alloc assign
val allocCode = s"$className.()"
val allocNode = newOperatorCallNode(Operators.alloc, allocCode, Option(className), line(expr))
val allocAst = callAst(allocNode, base = maybeNameAst)
val allocAssignCode = s"${tmpIdentifier.code} = ${allocAst.rootCodeOrEmpty}"
val allocAssignNode = newOperatorCallNode(Operators.assignment, allocAssignCode, Option(className), line(expr))
val allocAssignAst = callAst(allocAssignNode, Ast(tmpIdentifier) :: allocAst :: Nil)
// Init node
val initArgs = expr.args.map(astForCallArg)
val initSignature = s"$UnresolvedSignature(${initArgs.size})"
val initFullName = s"$className$InstanceMethodDelimiter${ConstructorMethodName}"
val initCode = s"$initFullName(${initArgs.map(_.rootCodeOrEmpty).mkString(",")})"
val initCallNode = callNode(
expr,
initCode,
ConstructorMethodName,
initFullName,
DispatchTypes.DYNAMIC_DISPATCH,
Some(initSignature),
Some(TypeConstants.Any)
)
val initReceiver = Ast(tmpIdentifier.copy)
val initCallAst = callAst(initCallNode, initArgs, base = Option(initReceiver))
// Return identifier
val returnIdentifierAst = Ast(tmpIdentifier.copy)
Ast(blockNode(expr, "", TypeConstants.Any))
.withChild(allocAssignAst)
.withChild(initCallAst)
.withChild(returnIdentifierAst)
}
private def dimensionFromSimpleScalar(scalar: PhpSimpleScalar, idxTracker: ArrayIndexTracker): PhpExpr = {
val maybeIntValue = scalar match {
case string: PhpString =>
string.value
.drop(1)
.dropRight(1)
.toIntOption
case number => number.value.toIntOption
}
maybeIntValue match {
case Some(intValue) =>
idxTracker.updateValue(intValue)
PhpInt(intValue.toString, scalar.attributes)
case None =>
scalar
}
}
private def assignForArrayItem(item: PhpArrayItem, name: String, idxTracker: ArrayIndexTracker): Ast = {
// It's perhaps a bit clumsy to reconstruct PhpExpr nodes here, but reuse astForArrayDimExpr for consistency
val variable = PhpVariable(PhpNameExpr(name, item.attributes), item.attributes)
val dimension = item.key match {
case Some(key: PhpSimpleScalar) => dimensionFromSimpleScalar(key, idxTracker)
case Some(key) => key
case None => PhpInt(idxTracker.next, item.attributes)
}
val dimFetchNode = PhpArrayDimFetchExpr(variable, Option(dimension), item.attributes)
val dimFetchAst = astForArrayDimFetchExpr(dimFetchNode)
val valueAst = astForArrayItemValue(item)
val assignCode = s"${dimFetchAst.rootCodeOrEmpty} = ${valueAst.rootCodeOrEmpty}"
val assignNode = newOperatorCallNode(Operators.assignment, assignCode, line = line(item))
callAst(assignNode, dimFetchAst :: valueAst :: Nil)
}
private def astForArrayItemValue(item: PhpArrayItem): Ast = {
val exprAst = astForExpr(item.value)
val valueCode = exprAst.rootCodeOrEmpty
if (item.byRef) {
val parentCall = newOperatorCallNode(Operators.addressOf, s"&$valueCode", line = line(item))
callAst(parentCall, exprAst :: Nil)
} else if (item.unpack) {
val parentCall = newOperatorCallNode(PhpOperators.unpack, s"...$valueCode", line = line(item))
callAst(parentCall, exprAst :: Nil)
} else {
exprAst
}
}
private def astForArrayDimFetchExpr(expr: PhpArrayDimFetchExpr): Ast = {
val variableAst = astForExpr(expr.variable)
val variableCode = variableAst.rootCodeOrEmpty
expr.dimension match {
case Some(dimension) =>
val dimensionAst = astForExpr(dimension)
val code = s"$variableCode[${dimensionAst.rootCodeOrEmpty}]"
val accessNode = newOperatorCallNode(Operators.indexAccess, code, line = line(expr))
callAst(accessNode, variableAst :: dimensionAst :: Nil)
case None =>
val errorPosition = s"${variableCode}:${line(expr).getOrElse("")}:${filename}"
logger.error(s"ArrayDimFetchExpr without dimensions should be handled in assignment: ${errorPosition}")
Ast()
}
}
private def astForErrorSuppressExpr(expr: PhpErrorSuppressExpr): Ast = {
val childAst = astForExpr(expr.expr)
val code = s"@${childAst.rootCodeOrEmpty}"
val suppressNode = newOperatorCallNode(PhpOperators.errorSuppress, code, line = line(expr))
childAst.rootType.foreach(typ => suppressNode.typeFullName(registerType(typ)))
callAst(suppressNode, childAst :: Nil)
}
private def astForInstanceOfExpr(expr: PhpInstanceOfExpr): Ast = {
val exprAst = astForExpr(expr.expr)
val classAst = astForExpr(expr.className)
val code = s"${exprAst.rootCodeOrEmpty} instanceof ${classAst.rootCodeOrEmpty}"
val typeFullName = registerType(TypeConstants.Bool)
val instanceOfNode = newOperatorCallNode(Operators.instanceOf, code, Some(typeFullName), line(expr))
callAst(instanceOfNode, exprAst :: classAst :: Nil)
}
private def astForPropertyFetchExpr(expr: PhpPropertyFetchExpr): Ast = {
val objExprAst = astForExpr(expr.expr)
val fieldAst = expr.name match {
case name: PhpNameExpr => Ast(newFieldIdentifierNode(name.name, line(expr)))
case other => astForExpr(other)
}
val accessSymbol =
if (expr.isStatic)
"::"
else if (expr.isNullsafe)
"?->"
else
"->"
val code = s"${objExprAst.rootCodeOrEmpty}$accessSymbol${fieldAst.rootCodeOrEmpty}"
val fieldAccessNode = newOperatorCallNode(Operators.fieldAccess, code, line = line(expr))
callAst(fieldAccessNode, objExprAst :: fieldAst :: Nil)
}
private def astForIncludeExpr(expr: PhpIncludeExpr): Ast = {
val exprAst = astForExpr(expr.expr)
val code = s"${expr.includeType} ${exprAst.rootCodeOrEmpty}"
val callNode = newOperatorCallNode(expr.includeType, code, line = line(expr))
callAst(callNode, exprAst :: Nil)
}
private def astForShellExecExpr(expr: PhpShellExecExpr): Ast = {
val args = expr.parts.map(astForExpr)
val code = s"`${args.map(_.rootCodeOrEmpty).mkString("").replaceAll("\"", "")}`"
val callNode = newOperatorCallNode(PhpOperators.shellExec, code, line = line(expr))
.methodFullName(PhpOperators.shellExec)
callAst(callNode, args)
}
private def astForClassConstFetchExpr(expr: PhpClassConstFetchExpr): Ast = {
val target = astForExpr(expr.className)
val fieldIdentifierName = expr.constantName.map(_.name).getOrElse(NameConstants.Unknown)
val fieldIdentifier = newFieldIdentifierNode(fieldIdentifierName, line(expr))
val fieldAccessCode = s"${target.rootCodeOrEmpty}::${fieldIdentifier.code}"
val fieldAccessCall = newOperatorCallNode(Operators.fieldAccess, fieldAccessCode, line = line(expr))
callAst(fieldAccessCall, List(target, Ast(fieldIdentifier)))
}
private def astForConstFetchExpr(expr: PhpConstFetchExpr): Ast = {
val identifier =
identifierNode(expr, NamespaceTraversal.globalNamespaceName, NamespaceTraversal.globalNamespaceName, "ANY")
val fieldIdentifier = newFieldIdentifierNode(expr.name.name, line = line(expr))
val fieldAccessNode = newOperatorCallNode(Operators.fieldAccess, code = expr.name.name, line = line(expr))
val args = List(identifier, fieldIdentifier).map(Ast(_))
callAst(fieldAccessNode, args)
}
protected def line(phpNode: PhpNode): Option[Integer] = phpNode.attributes.lineNumber
protected def column(phpNode: PhpNode): Option[Integer] = None
protected def lineEnd(phpNode: PhpNode): Option[Integer] = None
protected def columnEnd(phpNode: PhpNode): Option[Integer] = None
}
object AstCreator {
object TypeConstants {
val String: String = "string"
val Int: String = "int"
val Float: String = "float"
val Bool: String = "bool"
val Void: String = "void"
val Any: String = "ANY"
val Array: String = "array"
val VariadicPlaceholder: String = "PhpVariadicPlaceholder"
}
object NameConstants {
val Default: String = "default"
val HaltCompiler: String = "__halt_compiler"
val This: String = "this"
val Unknown: String = "UNKNOWN"
val Closure: String = "__closure"
}
val operatorSymbols: Map[String, String] = Map(
Operators.and -> "&",
Operators.or -> "|",
Operators.xor -> "^",
Operators.logicalAnd -> "&&",
Operators.logicalOr -> "||",
PhpOperators.coalesceOp -> "??",
PhpOperators.concatOp -> ".",
Operators.division -> "/",
Operators.equals -> "==",
Operators.greaterEqualsThan -> ">=",
Operators.greaterThan -> ">",
PhpOperators.identicalOp -> "===",
PhpOperators.logicalXorOp -> "xor",
Operators.minus -> "-",
Operators.modulo -> "%",
Operators.multiplication -> "*",
Operators.notEquals -> "!=",
PhpOperators.notIdenticalOp -> "!==",
Operators.plus -> "+",
Operators.exponentiation -> "**",
Operators.shiftLeft -> "<<",
Operators.arithmeticShiftRight -> ">>",
Operators.lessEqualsThan -> "<=",
Operators.lessThan -> "<",
PhpOperators.spaceshipOp -> "<=>",
Operators.not -> "~",
Operators.logicalNot -> "!",
Operators.postDecrement -> "--",
Operators.postIncrement -> "++",
Operators.preDecrement -> "--",
Operators.preIncrement -> "++",
Operators.minus -> "-",
Operators.plus -> "+",
Operators.assignment -> "=",
Operators.assignmentAnd -> "&=",
Operators.assignmentOr -> "|=",
Operators.assignmentXor -> "^=",
PhpOperators.assignmentCoalesceOp -> "??=",
PhpOperators.assignmentConcatOp -> ".=",
Operators.assignmentDivision -> "/=",
Operators.assignmentMinus -> "-=",
Operators.assignmentModulo -> "%=",
Operators.assignmentMultiplication -> "*=",
Operators.assignmentPlus -> "+=",
Operators.assignmentExponentiation -> "**=",
Operators.assignmentShiftLeft -> "<<=",
Operators.assignmentArithmeticShiftRight -> ">>="
)
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy