Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
436986a
[swiftsrc2cpg] Call and type decl member/access rework
max-leuthaeuser Oct 28, 2025
8b49740
more tests
max-leuthaeuser Nov 4, 2025
863ecd2
Merge branch 'heads/master' into max/callAndMemberRework
max-leuthaeuser Nov 4, 2025
b88715b
fix for review comment
max-leuthaeuser Nov 6, 2025
d63c849
fix for review comment
max-leuthaeuser Nov 6, 2025
ac30297
fix for review comment
max-leuthaeuser Nov 6, 2025
2e8bb33
Update joern-cli/frontends/swiftsrc2cpg/src/main/scala/io/joern/swift…
max-leuthaeuser Nov 7, 2025
1a9d88a
fix for closure calls to type decl members
max-leuthaeuser Nov 7, 2025
ae647d4
constructor receiver fix
max-leuthaeuser Nov 7, 2025
11e4760
fix scoping
max-leuthaeuser Nov 11, 2025
2f8b965
fix broken astForAttributeSyntax (has been like this forever)
max-leuthaeuser Nov 11, 2025
87a8f34
static function detection and self param for synthetic constructor me…
max-leuthaeuser Nov 12, 2025
8df951c
self for extensions
max-leuthaeuser Nov 12, 2025
a13a4c0
restore members for extensions
max-leuthaeuser Nov 12, 2025
b0fc7b1
sorting block elements to have extensions as last elements
max-leuthaeuser Nov 12, 2025
450d669
smaller fixes for post-processing passes (only affects joern)
max-leuthaeuser Nov 13, 2025
10b2b0f
fixes for self in extensions in separate files
max-leuthaeuser Nov 13, 2025
7796815
address review comments
max-leuthaeuser Nov 14, 2025
66615f8
add tests for calls to functions from extensions and protocols
max-leuthaeuser Nov 14, 2025
eeed3b1
fix for review comment
max-leuthaeuser Nov 17, 2025
83b4ba6
local refs
max-leuthaeuser Nov 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,22 @@ class AstCreator(

protected val logger: Logger = LoggerFactory.getLogger(classOf[AstCreator])

protected val scope = new SwiftVariableScopeManager()
protected val fullnameProvider = new FullnameProvider(typeMap)
protected val scope = new SwiftVariableScopeManager()
protected val fullnameProvider = new FullnameProvider(typeMap)
protected val methodAstParentStack = new Stack[NewNode]()
protected val typeRefIdStack = new Stack[NewTypeRef]
protected val localAstParentStack = new Stack[NewBlock]()

protected val methodAstParentStack = new Stack[NewNode]()
protected val typeRefIdStack = new Stack[NewTypeRef]
protected val dynamicInstanceTypeStack = new Stack[String]
protected val localAstParentStack = new Stack[NewBlock]()
protected val scopeLocalUniqueNames = mutable.HashMap.empty[String, Int]
case class InstanceTypeStackElement(name: String, members: mutable.HashSet[String])
protected val dynamicInstanceTypeStack = new Stack[InstanceTypeStackElement]

protected def addMemberToDynamicInstanceTypeStack(typeDeclFullName: String, memberName: String): Unit = {
dynamicInstanceTypeStack.find(_.name == typeDeclFullName).foreach { elem =>
elem.members.addOne(memberName)
}
}

protected val scopeLocalUniqueNames = mutable.HashMap.empty[String, Int]

protected lazy val definedSymbols: Map[String, String] = {
config.defines.map {
Expand Down Expand Up @@ -86,7 +94,7 @@ class AstCreator(
}

protected def astForNode(node: SwiftNode): Ast = node match {
case func: FunctionDeclLike => astForFunctionLike(func)
case func: FunctionDeclLike => astForFunctionLike(func, List.empty, None)
case swiftToken: SwiftToken => astForSwiftToken(swiftToken)
case syntax: Syntax => astForSyntax(syntax)
case exprSyntax: ExprSyntax => astForExprSyntax(exprSyntax)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,16 +164,23 @@ trait AstCreatorHelper(implicit withSchemaValidation: ValidationMode) { this: As

protected def astForIdentifier(node: SwiftNode): Ast = {
val identifierName = code(node)
val identNode = identifierNode(node, identifierName)
val variableOption = scope.lookupVariable(identifierName)
val tpe = variableOption match {
case Some((_, variableTypeName)) if variableTypeName != Defines.Any => variableTypeName
case None if identNode.typeFullName != Defines.Any => identNode.typeFullName
case _ => Defines.Any
dynamicInstanceTypeStack.headOption match {
case Some(InstanceTypeStackElement(tpe, members)) if members.contains(identifierName) =>
val selfNode = identifierNode(node, "self", "self", tpe)
scope.addVariableReference("self", selfNode, selfNode.typeFullName, EvaluationStrategies.BY_REFERENCE)
fieldAccessAst(node, node, Ast(selfNode), s"self.$identifierName", identifierName, tpe)
case _ =>
val identNode = identifierNode(node, identifierName)
val variableOption = scope.lookupVariable(identifierName)
val tpe = variableOption match {
case Some((_, variableTypeName)) if variableTypeName != Defines.Any => variableTypeName
case None if identNode.typeFullName != Defines.Any => identNode.typeFullName
case _ => Defines.Any
}
identNode.typeFullName = tpe
scope.addVariableReference(identifierName, identNode, tpe, EvaluationStrategies.BY_REFERENCE)
Ast(identNode)
}
identNode.typeFullName = tpe
scope.addVariableReference(identifierName, identNode, tpe, EvaluationStrategies.BY_REFERENCE)
Ast(identNode)
}

protected def registerType(typeFullName: String): Unit = {
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import io.joern.x2cpg.datastructures.Stack.*
import io.joern.x2cpg.frontendspecific.swiftsrc2cpg.Defines
import io.joern.x2cpg.{Ast, ValidationMode}
import io.shiftleft.codepropertygraph.generated.*
import io.shiftleft.codepropertygraph.generated.nodes.{NewCall, NewNode}
import io.shiftleft.codepropertygraph.generated.nodes.NewCall

import scala.annotation.unused

Expand Down Expand Up @@ -204,30 +204,16 @@ trait AstForExprSyntaxCreator(implicit withSchemaValidation: ValidationMode) {
callAst(callNode, argAsts)
}

private def handleCallNodeArgs(
callExpr: FunctionCallExprSyntax,
receiverAst: Ast,
baseNode: NewNode,
callName: String
): Ast = {
private def handleCallNodeArgs(callExpr: FunctionCallExprSyntax, baseAst: Ast, callName: String): Ast = {
val trailingClosureAsts = callExpr.trailingClosure.toList.map(astForNode)
val additionalTrailingClosuresAsts = callExpr.additionalTrailingClosures.children.map(c => astForNode(c.closure))

val args = callExpr.arguments.children.map(astForNode) ++ trailingClosureAsts ++ additionalTrailingClosuresAsts

val callExprCode = code(callExpr)
val callCode = callExprCode match {
case c if c.startsWith(".") && codeOf(baseNode) != "this" => s"${codeOf(baseNode)}$callExprCode"
case c if c.contains("#if ") =>
val recCode = codeOf(receiverAst.nodes.head)
if (recCode.endsWith(callName)) {
s"${codeOf(receiverAst.nodes.head)}(${code(callExpr.arguments)})"
} else {
s"${codeOf(receiverAst.nodes.head)}$callName(${code(callExpr.arguments)})"
}
case _ => callExprCode
}

val callCode = if (callExprCode.startsWith(".") || callExprCode.contains("#if ")) {
s"${codeOf(baseAst.root.get)}(${code(callExpr.arguments)})"
} else callExprCode
val callNode_ = callNode(
callExpr,
callCode,
Expand All @@ -239,7 +225,7 @@ trait AstForExprSyntaxCreator(implicit withSchemaValidation: ValidationMode) {
)
setFullNameInfoForCall(callExpr, callNode_)

callAst(callNode_, args, receiver = Option(receiverAst), base = Option(Ast(baseNode)))
callAst(callNode_, args, Option(baseAst))
}

private def astForFunctionCallExprSyntax(node: FunctionCallExprSyntax): Ast = {
Expand All @@ -248,55 +234,22 @@ trait AstForExprSyntaxCreator(implicit withSchemaValidation: ValidationMode) {
if (GlobalBuiltins.builtins.contains(calleeCode)) {
createBuiltinStaticCall(node, callee, calleeCode)
} else {
val (receiverAst, baseNode, callName) = callee match {
// TODO: extend the GsonTypeInfoReader to query for information whether
// the call is a call to a static function and generate a proper static call here
callee match {
case m: MemberAccessExprSyntax =>
val base = m.base
val member = m.declName
val memberCode = code(member)
base match {
case None =>
// referencing implicit this
val receiverAst = astForNode(callee)
val baseNodeId = identifierNode(m, "this")
scope.addVariableReference("this", baseNodeId, baseNodeId.typeFullName, EvaluationStrategies.BY_REFERENCE)
(receiverAst, baseNodeId, memberCode)
case Some(d: DeclReferenceExprSyntax) =>
val receiverAst = astForNode(callee)
val baseNodeId = identifierNode(d, code(d))
scope.addVariableReference(
code(d),
baseNodeId,
baseNodeId.typeFullName,
EvaluationStrategies.BY_REFERENCE
)
(receiverAst, baseNodeId, memberCode)
case Some(otherBase) =>
val tmpVarName = scopeLocalUniqueName("tmp")
val baseTmpNode = identifierNode(otherBase, tmpVarName)
scope.addVariableReference(
tmpVarName,
baseTmpNode,
baseTmpNode.typeFullName,
EvaluationStrategies.BY_REFERENCE
)
val baseAst = astForNode(otherBase)
val codeField = s"(${codeOf(baseTmpNode)} = ${codeOf(baseAst.nodes.head)})"
val tmpAssignmentAst =
createAssignmentCallAst(otherBase, Ast(baseTmpNode), baseAst, codeField)
val memberNode = fieldIdentifierNode(member, memberCode, memberCode)
val fieldAccessAst = createFieldAccessCallAst(callee, tmpAssignmentAst, memberNode)
val thisTmpNode = identifierNode(callee, tmpVarName)
(fieldAccessAst, thisTmpNode, memberCode)
}
val memberCode = code(m.declName)
handleCallNodeArgs(node, astForMemberAccessExprSyntax(m), memberCode)
case other if isRefToClosure(node, other) =>
return astForClosureCall(node)
case _ =>
val receiverAst = astForNode(callee)
val thisNode = identifierNode(callee, "this")
scope.addVariableReference(thisNode.name, thisNode, thisNode.typeFullName, EvaluationStrategies.BY_REFERENCE)
(receiverAst, thisNode, calleeCode)
astForClosureCall(node)
case declReferenceExprSyntax: DeclReferenceExprSyntax if code(declReferenceExprSyntax) != "self" =>
val selfTpe = typeHintForSelfExpression().headOption.getOrElse(Defines.Any)
val selfNode = identifierNode(declReferenceExprSyntax, "self", "self", selfTpe)
scope.addVariableReference(selfNode.name, selfNode, selfTpe, EvaluationStrategies.BY_REFERENCE)
handleCallNodeArgs(node, Ast(selfNode), calleeCode)
case other =>
handleCallNodeArgs(node, astForNode(other), calleeCode)
}
handleCallNodeArgs(node, receiverAst, baseNode, callName)
}
}

Expand All @@ -306,7 +259,7 @@ trait AstForExprSyntaxCreator(implicit withSchemaValidation: ValidationMode) {
val signature = fullnameProvider.typeFullnameRaw(expr.calledExpression).getOrElse(x2cpg.Defines.UnresolvedSignature)
val callName = Defines.ClosureApplyMethodName
val callMethodFullname = s"${Defines.Function}<$signature>.$callName:$signature"
val baseAst = astForNode(expr.calledExpression)
val baseAst = Ast(identifierNode(expr.calledExpression, code(expr.calledExpression)))

val trailingClosureAsts = expr.trailingClosure.toList.map(astForNode)
val additionalTrailingClosuresAsts = expr.additionalTrailingClosures.children.map(c => astForNode(c.closure))
Expand Down Expand Up @@ -422,13 +375,10 @@ trait AstForExprSyntaxCreator(implicit withSchemaValidation: ValidationMode) {
val member = node.declName
val baseAst = base match {
case None =>
// referencing implicit this
val baseNode = identifierNode(node, "this")
scope.addVariableReference("this", baseNode, Defines.Any, EvaluationStrategies.BY_REFERENCE)
Ast(baseNode)
case Some(d: DeclReferenceExprSyntax) if code(d) == "this" || code(d) == "self" =>
val baseNode = identifierNode(d, code(d))
scope.addVariableReference(code(d), baseNode, Defines.Any, EvaluationStrategies.BY_REFERENCE)
// referencing implicit self
val selfTpe = typeHintForSelfExpression().headOption.getOrElse(Defines.Any)
val baseNode = identifierNode(node, "self", "self", selfTpe)
scope.addVariableReference("self", baseNode, selfTpe, EvaluationStrategies.BY_REFERENCE)
Ast(baseNode)
case Some(otherBase) =>
astForNode(otherBase)
Expand All @@ -442,7 +392,6 @@ trait AstForExprSyntaxCreator(implicit withSchemaValidation: ValidationMode) {
val memberNode = fieldIdentifierNode(other, code(other), code(other))
createFieldAccessCallAst(node, baseAst, memberNode)
}

}

private def astForMissingExprSyntax(@unused node: MissingExprSyntax): Ast = Ast()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.joern.swiftsrc2cpg.astcreation
import io.joern.swiftsrc2cpg.parser.SwiftNodeSyntax.*
import io.joern.x2cpg
import io.joern.x2cpg.{Ast, ValidationMode}
import io.joern.x2cpg.datastructures.Stack.*
import io.joern.x2cpg.frontendspecific.swiftsrc2cpg.Defines
import io.shiftleft.codepropertygraph.generated.nodes.*
import io.shiftleft.codepropertygraph.generated.{DispatchTypes, ModifierTypes, Operators}
Expand Down Expand Up @@ -86,28 +87,30 @@ trait AstNodeBuilder(implicit withSchemaValidation: ValidationMode) { this: AstC
partAst: Ast,
additionalArgsAst: Seq[Ast] = Seq.empty
): Ast = {
val tpe = fullnameProvider.typeFullname(node).orElse(Some(Defines.Any))
val callNode_ = callNode(
node,
s"${codeOf(baseAst.nodes.head)}[${codeOf(partAst.nodes.head)}]",
Operators.indexAccess,
Operators.indexAccess,
DispatchTypes.STATIC_DISPATCH,
None,
Some(Defines.Any)
tpe
)
val arguments = List(baseAst, partAst) ++ additionalArgsAst
callAst(callNode_, arguments)
}

protected def createFieldAccessCallAst(node: SwiftNode, baseAst: Ast, partNode: NewNode): Ast = {
val tpe = fullnameProvider.typeFullname(node).orElse(Some(Defines.Any))
val callNode_ = callNode(
node,
s"${codeOf(baseAst.nodes.head)}.${codeOf(partNode)}",
Operators.fieldAccess,
Operators.fieldAccess,
DispatchTypes.STATIC_DISPATCH,
None,
Some(Defines.Any)
tpe
)
val arguments = List(baseAst, Ast(partNode))
callAst(callNode_, arguments)
Expand Down Expand Up @@ -135,10 +138,10 @@ trait AstNodeBuilder(implicit withSchemaValidation: ValidationMode) { this: AstC
callAst(callNode_, arguments)
}

private def typeHintForThisExpression(): Seq[String] = {
protected def typeHintForSelfExpression(): Seq[String] = {
dynamicInstanceTypeStack.headOption match {
case Some(tpe) => Seq(tpe)
case None => methodAstParentStack.collectFirst { case t: NewTypeDecl => t.fullName }.toSeq
case Some(InstanceTypeStackElement(tpe, _)) => Seq(tpe)
case None => methodAstParentStack.collectFirst { case t: NewTypeDecl => t.fullName }.toSeq
}
}

Expand All @@ -150,18 +153,19 @@ trait AstNodeBuilder(implicit withSchemaValidation: ValidationMode) { this: AstC
t
}
.getOrElse(name match {
case "this" | "self" | "Self" => typeHintForThisExpression().headOption.getOrElse(Defines.Any)
case "this" | "self" | "Self" => typeHintForSelfExpression().headOption.getOrElse(Defines.Any)
case _ => Defines.Any
})
identifierNode(node, name, name, tpe)
}

def staticInitMethodAstAndBlock(
node: SwiftNode,
initAsts: List[Ast],
inits: List[DeclSyntax],
fullName: String,
signature: Option[String],
returnType: String,
typeDecl: NewTypeDecl,
fileName: Option[String] = None
): AstAndMethod = {
val methodNode = NewMethod()
Expand All @@ -175,8 +179,14 @@ trait AstNodeBuilder(implicit withSchemaValidation: ValidationMode) { this: AstC
if (fileName.isDefined) {
methodNode.filename(fileName.get)
}

val blockNode = NewBlock()
localAstParentStack.push(blockNode)
val initAsts = inits.map(m => astForDeclMember(m, typeDecl))
localAstParentStack.pop()

val staticModifier = NewModifier().modifierType(ModifierTypes.STATIC)
val body = blockAst(NewBlock(), initAsts)
val body = blockAst(blockNode, initAsts)
val methodReturn = methodReturnNode(node, returnType)
AstAndMethod(methodAst(methodNode, Nil, body, methodReturn, List(staticModifier)), methodNode, body)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ object GsonTypeInfoReader {
val DeclRef = "decl_ref"
val CallExpr = "call_expr"
val ReturnStmt = "return_stmt"
val DotCallExpr = "dot_syntax_call_expr"
}

/** Safely retrieves a string property from a JsonObject.
Expand Down Expand Up @@ -69,7 +70,11 @@ object GsonTypeInfoReader {
val rangeObj = obj.get("range").getAsJsonObject
val start = rangeObj.get("start").getAsInt
val end = rangeObj.get("end").getAsInt
if (start == end) (start, end) else (start, end + 1)
val offset = astNodeKind(obj) match {
case NodeKinds.DotCallExpr => 3 // offsets are off by 2 from SwiftParser for simple dot calls
case _ => 1
}
if (start == end) (start, end) else (start, end + offset)
}

/** Safely extracts the source range from a JSON object.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ class ActorTests extends SwiftCompilerSrc2CpgSuite {

val List(myActor) = cpg.typeDecl.nameExact("MyActor2").l
myActor.fullName shouldBe "Sources/main.swift:<global>.MyActor2"
myActor.member.name.l shouldBe List("hello", "foo")
val List(constructor) = myActor.method.isConstructor.l
constructor.name shouldBe "init"
constructor.fullName shouldBe "Sources/main.swift:<global>.MyActor2.init:()->Sources/main.swift:<global>.MyActor2"
Expand All @@ -54,7 +53,6 @@ class ActorTests extends SwiftCompilerSrc2CpgSuite {

val List(myActorSwiftc) = compilerCpg.typeDecl.nameExact("MyActor2").l
myActorSwiftc.fullName shouldBe "SwiftTest.MyActor2"
myActorSwiftc.member.name.l shouldBe List("hello", "foo")
val List(constructorSwiftc) = myActorSwiftc.method.isConstructor.l
constructorSwiftc.name shouldBe "init"
constructorSwiftc.fullName shouldBe "SwiftTest.MyActor2.init:()->SwiftTest.MyActor2"
Expand Down
Loading
Loading