Skip to content

Commit b48167c

Browse files
[swiftsrc2cpg] De-sugar tuple patterns in if-case/guard-case conditions (#5920)
Extend tuple pattern de-sugaring from switch-case to if-case and guard-case conditions. Rename existing switch-specific helpers to context-agnostic names and promote them to protected visibility for reuse across AST creator traits. Supported patterns: - if case (1, 2) = expr (expression tuple, equality chain) - if case let (a, b) = expr (binding tuple, field access assignments) - if case (let a, 1) = expr (mixed binding + equality) - guard case let (a, b) = expr (bindings under enclosing scope) - if case let ((a, b), c) = expr (nested tuples) - if let (a, b) = optionalExpr (optional binding with tuple) - isTupleN condition check as the final expression in tuple condition blocks so that the condition has an explicit boolean test
1 parent 154fbb7 commit b48167c

6 files changed

Lines changed: 450 additions & 45 deletions

File tree

joern-cli/frontends/swiftsrc2cpg/src/main/scala/io/joern/swiftsrc2cpg/astcreation/AstForExprSyntaxCreator.scala

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -722,7 +722,7 @@ trait AstForExprSyntaxCreator(implicit withSchemaValidation: ValidationMode) {
722722
* identifier/field-identifier nodes so the resulting AST can be safely used as an argument without node-sharing
723723
* issues.
724724
*/
725-
private def createFieldAccessChain(baseName: String, fields: List[String], node: SwiftNode): Ast = {
725+
protected def createFieldAccessChain(baseName: String, fields: List[String], node: SwiftNode): Ast = {
726726
val baseAst = Ast(identifierNode(node, baseName))
727727
fields.foldLeft(baseAst) { (accAst, field) =>
728728
createFieldAccessCallAst(node, accAst, fieldIdentifierNode(node, field, field))
@@ -740,7 +740,7 @@ trait AstForExprSyntaxCreator(implicit withSchemaValidation: ValidationMode) {
740740
* Nested tuples like case ((1, 2), 3): are handled recursively: <subject>.0.0 == 1 && <subject>.0.1 == 2 &&
741741
* <subject>.1 == 3
742742
*/
743-
private def astForExpressionTuplePatternInSwitchContext(
743+
protected def astForExpressionTuplePattern(
744744
tupleExpr: TupleExprSyntax,
745745
subjectBase: String,
746746
subjectFieldPath: List[String],
@@ -753,7 +753,7 @@ trait AstForExprSyntaxCreator(implicit withSchemaValidation: ValidationMode) {
753753
val subjectAst = createFieldAccessChain(subjectBase, currentPath, node)
754754
element.expression match {
755755
case inner: TupleExprSyntax =>
756-
astForExpressionTuplePatternInSwitchContext(inner, subjectBase, currentPath, node)
756+
astForExpressionTuplePattern(inner, subjectBase, currentPath, node)
757757
case _ =>
758758
val rhsAst = astForNode(element)
759759
val eqCode = s"$subjectCode == ${code(element.expression)}"
@@ -768,7 +768,7 @@ trait AstForExprSyntaxCreator(implicit withSchemaValidation: ValidationMode) {
768768
}
769769
}
770770

771-
private def isBindingTupleExpr(tupleExpr: TupleExprSyntax): Boolean = {
771+
protected def isBindingTupleExpr(tupleExpr: TupleExprSyntax): Boolean = {
772772
tupleExpr.elements.children.exists { elem =>
773773
elem.expression.isInstanceOf[PatternExprSyntax]
774774
}
@@ -782,16 +782,16 @@ trait AstForExprSyntaxCreator(implicit withSchemaValidation: ValidationMode) {
782782
case ep: ExpressionPatternSyntax if ep.expression.isInstanceOf[TupleExprSyntax] =>
783783
val tupleExpr = ep.expression.asInstanceOf[TupleExprSyntax]
784784
if (isBindingTupleExpr(tupleExpr)) {
785-
astsForBindingTupleExprInSwitchContext(tupleExpr, tmpName, List.empty, ep)
785+
astsForBindingTupleExpr(tupleExpr, tmpName, List.empty, ep)
786786
} else {
787-
List(astForExpressionTuplePatternInSwitchContext(tupleExpr, tmpName, List.empty, ep))
787+
List(astForExpressionTuplePattern(tupleExpr, tmpName, List.empty, ep))
788788
}
789789
case vb: ValueBindingPatternSyntax =>
790790
vb.pattern match {
791791
case tp: TuplePatternSyntax =>
792-
astsForBindingTuplePatternInSwitchContext(tp, tmpName, List.empty, vb)
792+
astsForBindingTuplePattern(tp, tmpName, List.empty, vb)
793793
case ep: ExpressionPatternSyntax if ep.expression.isInstanceOf[TupleExprSyntax] =>
794-
astsForBindingTupleExprInSwitchContext(
794+
astsForBindingTupleExpr(
795795
ep.expression.asInstanceOf[TupleExprSyntax],
796796
tmpName,
797797
List.empty,
@@ -801,7 +801,7 @@ trait AstForExprSyntaxCreator(implicit withSchemaValidation: ValidationMode) {
801801
case _ => List(astForNode(item.pattern))
802802
}
803803
case tuple: TuplePatternSyntax =>
804-
astsForBindingTuplePatternInSwitchContext(tuple, tmpName, List.empty, tuple)
804+
astsForBindingTuplePattern(tuple, tmpName, List.empty, tuple)
805805
case _ =>
806806
List(astForNode(item.pattern))
807807
}
@@ -825,7 +825,7 @@ trait AstForExprSyntaxCreator(implicit withSchemaValidation: ValidationMode) {
825825
*/
826826

827827
/** Creates an instanceOf check for an IsTypePatternSyntax against a subject field access. */
828-
private def astForIsTypePatternInTupleContext(
828+
protected def astForIsTypePatternInTupleContext(
829829
isType: IsTypePatternSyntax,
830830
subjectAst: Ast,
831831
subjectCode: String,
@@ -842,7 +842,7 @@ trait AstForExprSyntaxCreator(implicit withSchemaValidation: ValidationMode) {
842842
}
843843

844844
/** Creates an equality check for an expression pattern against a subject field access. */
845-
private def astForExpressionPatternInTupleContext(
845+
protected def astForExpressionPatternInTupleContext(
846846
ep: ExpressionPatternSyntax,
847847
subjectAst: Ast,
848848
subjectCode: String,
@@ -855,7 +855,7 @@ trait AstForExprSyntaxCreator(implicit withSchemaValidation: ValidationMode) {
855855
}
856856

857857
/** Creates a variable binding assignment for a pattern element against a subject field access. */
858-
private def astForBindingInTupleContext(
858+
protected def astForBindingInTupleContext(
859859
varName: String,
860860
subjectAst: Ast,
861861
subjectCode: String,
@@ -869,7 +869,7 @@ trait AstForExprSyntaxCreator(implicit withSchemaValidation: ValidationMode) {
869869
List(createAssignmentCallAst(anchorNode, Ast(lhsNode), subjectAst, assignCode))
870870
}
871871

872-
private def astsForBindingTuplePatternInSwitchContext(
872+
protected def astsForBindingTuplePattern(
873873
tuplePat: TuplePatternSyntax,
874874
subjectBase: String,
875875
subjectFieldPath: List[String],
@@ -881,7 +881,7 @@ trait AstForExprSyntaxCreator(implicit withSchemaValidation: ValidationMode) {
881881
val subjectAst = createFieldAccessChain(subjectBase, currentPath, node)
882882
element.pattern match {
883883
case inner: TuplePatternSyntax =>
884-
astsForBindingTuplePatternInSwitchContext(inner, subjectBase, currentPath, node)
884+
astsForBindingTuplePattern(inner, subjectBase, currentPath, node)
885885
case isType: IsTypePatternSyntax =>
886886
astForIsTypePatternInTupleContext(isType, subjectAst, subjectCode, node)
887887
case ep: ExpressionPatternSyntax =>
@@ -891,7 +891,7 @@ trait AstForExprSyntaxCreator(implicit withSchemaValidation: ValidationMode) {
891891
case vb: ValueBindingPatternSyntax =>
892892
vb.pattern match {
893893
case inner: TuplePatternSyntax =>
894-
astsForBindingTuplePatternInSwitchContext(inner, subjectBase, currentPath, node)
894+
astsForBindingTuplePattern(inner, subjectBase, currentPath, node)
895895
case _ =>
896896
astForBindingInTupleContext(code(vb.pattern), subjectAst, subjectCode, tuplePat)
897897
}
@@ -902,7 +902,7 @@ trait AstForExprSyntaxCreator(implicit withSchemaValidation: ValidationMode) {
902902
}
903903

904904
/** Determines whether an expression inside a tuple represents a binding (let/var pattern). */
905-
private def isBindingExpression(expr: ExprSyntax): Boolean = expr match {
905+
protected def isBindingExpression(expr: ExprSyntax): Boolean = expr match {
906906
case p: PatternExprSyntax =>
907907
p.pattern match {
908908
case _: ValueBindingPatternSyntax => true
@@ -913,7 +913,7 @@ trait AstForExprSyntaxCreator(implicit withSchemaValidation: ValidationMode) {
913913
}
914914

915915
/** Dispatches a PatternSyntax inside a tuple context to the appropriate de-sugaring. */
916-
private def astsForPatternInTupleContext(
916+
protected def astsForPatternInTupleContext(
917917
pattern: PatternSyntax,
918918
subjectAst: Ast,
919919
subjectCode: String,
@@ -932,7 +932,7 @@ trait AstForExprSyntaxCreator(implicit withSchemaValidation: ValidationMode) {
932932
List(callAst(eqNode, List(subjectAst, rhsAst)))
933933
}
934934

935-
private def astsForBindingTupleExprInSwitchContext(
935+
protected def astsForBindingTupleExpr(
936936
tupleExpr: TupleExprSyntax,
937937
subjectBase: String,
938938
subjectFieldPath: List[String],
@@ -945,7 +945,7 @@ trait AstForExprSyntaxCreator(implicit withSchemaValidation: ValidationMode) {
945945
val subjectAst = createFieldAccessChain(subjectBase, currentPath, node)
946946
element.expression match {
947947
case inner: TupleExprSyntax =>
948-
astsForBindingTupleExprInSwitchContext(inner, subjectBase, currentPath, node, allBindings)
948+
astsForBindingTupleExpr(inner, subjectBase, currentPath, node, allBindings)
949949
case _ if allBindings || isBindingExpression(element.expression) =>
950950
val varName = extractBindingName(element.expression)
951951
astForBindingInTupleContext(varName, subjectAst, subjectCode, tupleExpr)
@@ -966,7 +966,7 @@ trait AstForExprSyntaxCreator(implicit withSchemaValidation: ValidationMode) {
966966
* - `DeclReferenceExprSyntax` (`a` in `case let (a, b):`)
967967
* - `PatternExprSyntax(ValueBindingPatternSyntax(IdentifierPatternSyntax))` (`var a` in `case (var a, var b):`)
968968
*/
969-
private def extractBindingName(expr: ExprSyntax): String = {
969+
protected def extractBindingName(expr: ExprSyntax): String = {
970970
expr match {
971971
case d: DeclReferenceExprSyntax => code(d)
972972
case p: PatternExprSyntax =>

joern-cli/frontends/swiftsrc2cpg/src/main/scala/io/joern/swiftsrc2cpg/astcreation/AstForPatternSyntaxCreator.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ trait AstForPatternSyntaxCreator(implicit withSchemaValidation: ValidationMode)
3535

3636
private def astForMissingPatternSyntax(@unused node: MissingPatternSyntax): Ast = Ast()
3737

38-
// TODO: implement astForTuplePatternSyntax for non-switch contexts (e.g. if-case expressions).
39-
// Switch-context handling is done via astsForBindingTuplePatternInSwitchContext in AstForExprSyntaxCreator.
38+
// Tuple patterns in switch-case and if-case/guard-case conditions are handled via
39+
// astsForBindingTuplePattern / astForExpressionTuplePattern in AstForExprSyntaxCreator.
40+
// This handler covers remaining contexts (e.g. bare pattern matching) which are not yet implemented.
4041
private def astForTuplePatternSyntax(node: TuplePatternSyntax): Ast = notHandledYet(node)
4142

4243
private def localForValueBindingPatternSyntax(

joern-cli/frontends/swiftsrc2cpg/src/main/scala/io/joern/swiftsrc2cpg/astcreation/AstForSyntaxCreator.scala

Lines changed: 163 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -263,14 +263,128 @@ trait AstForSyntaxCreator(implicit withSchemaValidation: ValidationMode) { this:
263263
private def astForLayoutRequirementSyntax(node: LayoutRequirementSyntax): Ast = notHandledYet(node)
264264

265265
private def astForMatchingPatternConditionSyntax(node: MatchingPatternConditionSyntax): Ast = {
266-
val lhsAst = astForNode(node.pattern)
267-
val rhsAst = astForNode(node.initializer.value)
266+
val initValue = node.initializer.value
267+
268+
// Detect tuple pattern variants
269+
val maybeTuplePattern: Option[TuplePatternSyntax] = node.pattern match {
270+
case tp: TuplePatternSyntax => Some(tp)
271+
case vb: ValueBindingPatternSyntax =>
272+
vb.pattern match {
273+
case tp: TuplePatternSyntax => Some(tp)
274+
case _ => None
275+
}
276+
case _ => None
277+
}
278+
279+
val maybeTupleExpr: Option[TupleExprSyntax] = node.pattern match {
280+
case ep: ExpressionPatternSyntax =>
281+
ep.expression match {
282+
case te: TupleExprSyntax => Some(te)
283+
case _ => None
284+
}
285+
case vb: ValueBindingPatternSyntax =>
286+
vb.pattern match {
287+
case ep: ExpressionPatternSyntax =>
288+
ep.expression match {
289+
case te: TupleExprSyntax => Some(te)
290+
case _ => None
291+
}
292+
case _ => None
293+
}
294+
case _ => None
295+
}
268296

269-
val tpe = Defines.Bool
270-
val op = Operators.equals
271-
val callNode_ = createStaticCallNode(node, code(node), op, op, tpe)
272-
val argAsts = List(lhsAst, rhsAst)
273-
callAst(callNode_, argAsts)
297+
(maybeTuplePattern, maybeTupleExpr) match {
298+
case (Some(tuplePat), _) =>
299+
astForTuplePatternInConditionContext(node, tuplePat, initValue)
300+
case (_, Some(tupleExpr)) =>
301+
astForTupleExprInConditionContext(node, tupleExpr, initValue)
302+
case _ =>
303+
// Original behavior for non-tuple patterns
304+
val lhsAst = astForNode(node.pattern)
305+
val rhsAst = astForNode(initValue)
306+
val tpe = Defines.Bool
307+
val op = Operators.equals
308+
val callNode_ = createStaticCallNode(node, code(node), op, op, tpe)
309+
val argAsts = List(lhsAst, rhsAst)
310+
callAst(callNode_, argAsts)
311+
}
312+
}
313+
314+
/** De-sugars a tuple in an if-case / guard-case condition.
315+
*
316+
* Creates a block: { <tmp> = initValue; ...desugarAsts... } where the de-sugaring is provided by `desugarFn`.
317+
*
318+
* Locals are added to `localAstParentStack.head`. For `if` conditions this is the IF control structure node. For
319+
* `guard` conditions this is the enclosing method/block scope (matching existing `guard let` behavior).
320+
*/
321+
private def astForTupleInConditionContext(
322+
node: SwiftNode,
323+
initValue: SwiftNode,
324+
arity: Int,
325+
desugarFn: String => List[Ast]
326+
): Ast = {
327+
val blockNode_ = blockNode(node)
328+
scope.pushNewBlockScope(blockNode_)
329+
localAstParentStack.push(blockNode_)
330+
331+
val tmpName = scopeLocalUniqueName("tmp")
332+
val tmpLocalNode = localNode(node, tmpName, tmpName, Defines.Any).order(0)
333+
diffGraph.addEdge(localAstParentStack.head, tmpLocalNode, EdgeTypes.AST)
334+
scope.addVariable(tmpName, tmpLocalNode, Defines.Any, VariableScopeManager.ScopeType.BlockScope)
335+
336+
val tmpIdentNode = identifierNode(node, tmpName, tmpName, Defines.Any)
337+
scope.addVariableReference(tmpName, tmpIdentNode, Defines.Any, EvaluationStrategies.BY_REFERENCE)
338+
val initAst = astForNode(initValue)
339+
val assignAst = createAssignmentCallAst(node, Ast(tmpIdentNode), initAst, s"$tmpName = ${code(initValue)}")
340+
341+
val desugarAsts = desugarFn(tmpName)
342+
343+
// Emit an isTupleN check as the final expression in the condition block
344+
val op = Defines.createIsTupleOperator(arity)
345+
val isTupleCode = s"$op($tmpName)"
346+
val isTupleNode = createStaticCallNode(node, isTupleCode, op, op, Defines.Bool)
347+
val tmpIdentForCheck = identifierNode(node, tmpName, tmpName, Defines.Any)
348+
scope.addVariableReference(tmpName, tmpIdentForCheck, Defines.Any, EvaluationStrategies.BY_REFERENCE)
349+
val isTupleAst = callAst(isTupleNode, List(Ast(tmpIdentForCheck)))
350+
351+
scope.popScope()
352+
localAstParentStack.pop()
353+
354+
blockAst(blockNode_, (assignAst +: desugarAsts) :+ isTupleAst)
355+
}
356+
357+
private def astForTuplePatternInConditionContext(
358+
node: SwiftNode,
359+
tuplePat: TuplePatternSyntax,
360+
initValue: SwiftNode
361+
): Ast = {
362+
val arity = tuplePat.elements.children.size
363+
astForTupleInConditionContext(
364+
node,
365+
initValue,
366+
arity,
367+
tmpName => astsForBindingTuplePattern(tuplePat, tmpName, List.empty, node)
368+
)
369+
}
370+
371+
private def astForTupleExprInConditionContext(
372+
node: SwiftNode,
373+
tupleExpr: TupleExprSyntax,
374+
initValue: SwiftNode
375+
): Ast = {
376+
val arity = tupleExpr.elements.children.size
377+
astForTupleInConditionContext(
378+
node,
379+
initValue,
380+
arity,
381+
tmpName =>
382+
if (isBindingTupleExpr(tupleExpr)) {
383+
astsForBindingTupleExpr(tupleExpr, tmpName, List.empty, node)
384+
} else {
385+
List(astForExpressionTuplePattern(tupleExpr, tmpName, List.empty, node))
386+
}
387+
)
274388
}
275389

276390
private def astForMemberBlockItemSyntax(node: MemberBlockItemSyntax): Ast = notHandledYet(node)
@@ -305,24 +419,50 @@ trait AstForSyntaxCreator(implicit withSchemaValidation: ValidationMode) { this:
305419
private def astForOptionalBindingConditionSyntax(node: OptionalBindingConditionSyntax): Ast = {
306420
val typeFullName = node.typeAnnotation.fold(Defines.Any)(t => AstCreatorHelper.cleanType(code(t.`type`)))
307421

308-
node.pattern match {
309-
case ident: IdentifierPatternSyntax =>
310-
val tpeFromTypeMap = fullnameProvider.typeFullname(ident)
311-
localForOptionalBindingConditionSyntax(node, code(ident.identifier), tpeFromTypeMap.getOrElse(typeFullName))
312-
case _ => // do nothing
422+
// Detect tuple pattern: Swift parser may produce TuplePatternSyntax or
423+
// ExpressionPatternSyntax wrapping TupleExprSyntax for `if let (a, b) = x`
424+
val maybeTupleExpr: Option[TupleExprSyntax] = node.pattern match {
425+
case tp: TuplePatternSyntax =>
426+
// Direct tuple pattern — use TuplePatternInConditionContext
427+
return node.initializer match {
428+
case Some(init) => astForTuplePatternInConditionContext(node, tp, init.value)
429+
case None => Ast()
430+
}
431+
case ep: ExpressionPatternSyntax =>
432+
ep.expression match {
433+
case te: TupleExprSyntax => Some(te)
434+
case _ => None
435+
}
436+
case _ => None
313437
}
314438

315-
val initAst = node.initializer.map(astForNode)
316-
if (initAst.isEmpty) {
317-
Ast()
318-
} else {
319-
val patternAst = astForNode(node.pattern)
320-
val tpe = fullnameProvider.typeFullname(node.pattern).getOrElse(typeFullName)
321-
registerType(tpe)
322-
patternAst.root
323-
.collect { case i: NewIdentifier => i }
324-
.foreach(_.typeFullName(tpe))
325-
createAssignmentCallAst(node, patternAst, initAst.head, code(node))
439+
maybeTupleExpr match {
440+
case Some(tupleExpr) =>
441+
node.initializer match {
442+
case Some(init) => astForTupleExprInConditionContext(node, tupleExpr, init.value)
443+
case None => Ast()
444+
}
445+
case None =>
446+
// Original behavior for non-tuple patterns
447+
node.pattern match {
448+
case ident: IdentifierPatternSyntax =>
449+
val tpeFromTypeMap = fullnameProvider.typeFullname(ident)
450+
localForOptionalBindingConditionSyntax(node, code(ident.identifier), tpeFromTypeMap.getOrElse(typeFullName))
451+
case _ => // do nothing
452+
}
453+
454+
val initAst = node.initializer.map(astForNode)
455+
if (initAst.isEmpty) {
456+
Ast()
457+
} else {
458+
val patternAst = astForNode(node.pattern)
459+
val tpe = fullnameProvider.typeFullname(node.pattern).getOrElse(typeFullName)
460+
registerType(tpe)
461+
patternAst.root
462+
.collect { case i: NewIdentifier => i }
463+
.foreach(_.typeFullName(tpe))
464+
createAssignmentCallAst(node, patternAst, initAst.head, code(node))
465+
}
326466
}
327467
}
328468

0 commit comments

Comments
 (0)