From 160dd99fa22e79a1beff7510cb0a270b54e81f85 Mon Sep 17 00:00:00 2001 From: allsmog Date: Thu, 2 Apr 2026 10:52:27 -0700 Subject: [PATCH] [pysrc2cpg] Lower match pattern bindings into proper AST assignments Replaces the string-only pattern representation in match/case blocks with proper AST nodes that encode destructuring semantics, enabling data flow tracking through pattern-bound variables. For each match pattern type, generate assignment nodes using the same index access and assignment primitives as tuple unpacking: - MatchSequence [a, b]: a = subject[0], b = subject[1] - MatchAs (catch-all): x = subject - MatchAs (alias): recurse + whole = subject - MatchMapping: name = subject[key] - MatchClass: positional index + keyword field access - MatchOr: process first alternative (all bind same names) - MatchStar: rest = subject (simplified flow) - Complex subjects: temp variable to avoid re-evaluation JumpTarget nodes are preserved for CfgCreator compatibility. Nested patterns use temp variables like tuple unpacking does. --- .../io/joern/pysrc2cpg/PythonAstVisitor.scala | 226 ++++++++++++++++-- .../joern/pysrc2cpg/cpg/MatchCpgTests.scala | 159 ++++++++---- .../pysrc2cpg/dataflow/DataFlowTests.scala | 24 ++ 3 files changed, 355 insertions(+), 54 deletions(-) diff --git a/joern-cli/frontends/pysrc2cpg/src/main/scala/io/joern/pysrc2cpg/PythonAstVisitor.scala b/joern-cli/frontends/pysrc2cpg/src/main/scala/io/joern/pysrc2cpg/PythonAstVisitor.scala index 42f06340ef79..0a80c0b8e6a6 100644 --- a/joern-cli/frontends/pysrc2cpg/src/main/scala/io/joern/pysrc2cpg/PythonAstVisitor.scala +++ b/joern-cli/frontends/pysrc2cpg/src/main/scala/io/joern/pysrc2cpg/PythonAstVisitor.scala @@ -5,7 +5,7 @@ import io.joern.pysrc2cpg.memop.* import io.joern.pysrc2cpg.memop.MemoryOperation.{Del, Load, Store} import io.joern.x2cpg.frontendspecific.pysrc2cpg.Constants.builtinPrefix import io.joern.pythonparser.{AstPrinter, ast} -import io.joern.pythonparser.ast.{Arguments, MatchAs, iast, iexpr, istmt} +import io.joern.pythonparser.ast.{Arguments, MatchAs, MatchClass, MatchMapping, MatchOr, MatchSequence, MatchSingleton, MatchStar, MatchValue, iast, iexpr, ipattern, istmt} import io.joern.x2cpg.frontendspecific.pysrc2cpg.Constants import io.joern.x2cpg.{AstCreatorBase, ValidationMode} import io.shiftleft.codepropertygraph.generated.* @@ -1243,37 +1243,235 @@ class PythonAstVisitor( createBlock(blockStmts, lineAndCol) } - // TODO add case pattern and guard statements to not just as string in the JUMP_TARGET to the CPG - // but rather as proper AST constructs. def convert(matchStmt: ast.Match): NewNode = { + val lineAndCol = lineAndColOf(matchStmt) val controlStructureNode = - nodeBuilder.controlStructureNode("match ... : ...", ControlStructureTypes.MATCH, lineAndColOf(matchStmt)) + nodeBuilder.controlStructureNode("match ... : ...", ControlStructureTypes.MATCH, lineAndCol) - val matchSubject = convert(matchStmt.subject) + // For simple Name subjects, reference the name directly in pattern assignments. + // For complex expressions, create a temp variable so it's evaluated once and referenceable. + val (subjectRefName, matchSubjectNode, prefixNodes) = matchStmt.subject match { + case ast.Name(id, _) => + (id, convert(matchStmt.subject), Seq.empty[nodes.NewNode]) + case _ => + val tmpName = getUnusedName() + val subjectExpr = convert(matchStmt.subject) + val tmpAssign = createAssignmentToIdentifier(tmpName, subjectExpr, lineAndCol) + val tmpRef = createIdentifierNode(tmpName, Load, lineAndCol) + (tmpName, tmpRef, Seq(tmpAssign)) + } val caseBlocks = matchStmt.cases.flatMap { caseStmt => val jumpTargetCode = caseStmt.pattern match { case MatchAs(None, _, _) if caseStmt.guard.isEmpty => - // TODO For the moment we have to use "default" because otherwise the CfgCreator does not detect - // the jump target as the default case. + // Use "default" because the CfgCreator checks for this to detect the default case. "default" case pattern => val printer = new AstPrinter("") "case " + printer.print(pattern) + caseStmt.guard.map(g => " if " + printer.print(g)).getOrElse("") } - val jumpTarget = nodeBuilder.jumpNode(jumpTargetCode) - val bodyNodes = caseStmt.body.map(convert) - jumpTarget :: createBlock(bodyNodes, lineAndColOf(caseStmt.pattern)) :: Nil + val jumpTarget = nodeBuilder.jumpNode(jumpTargetCode) + val patternAssignments = lowerMatchPatternBindings(caseStmt.pattern, subjectRefName, lineAndColOf(caseStmt.pattern)) + val bodyNodes = caseStmt.body.map(convert) + jumpTarget :: createBlock(patternAssignments ++ bodyNodes, lineAndColOf(caseStmt.pattern)) :: Nil } - val switchBodyBlock = createBlock(caseBlocks, lineAndColOf(matchStmt)) + val switchBodyBlock = createBlock(caseBlocks, lineAndCol) - edgeBuilder.conditionEdge(matchSubject, controlStructureNode) - addAstChildNodes(controlStructureNode, 1, matchSubject) + edgeBuilder.conditionEdge(matchSubjectNode, controlStructureNode) + addAstChildNodes(controlStructureNode, 1, matchSubjectNode) addAstChildNodes(controlStructureNode, 2, switchBodyBlock) - controlStructureNode + if (prefixNodes.nonEmpty) { + createBlock(prefixNodes :+ controlStructureNode, lineAndCol) + } else { + controlStructureNode + } + } + + /** Lower match pattern bindings into assignment nodes that extract values from the subject. + * + * For example, `case [a, b]:` matching against subject `x` produces: `a = x[0]`, `b = x[1]` + * + * This reuses the same index access and assignment primitives as tuple unpacking. + */ + private def lowerMatchPatternBindings( + pattern: ipattern, + subjectRefName: String, + lineAndCol: LineAndColumn + ): Seq[nodes.NewNode] = { + pattern match { + case MatchAs(None, Some(name), _) => + // Catch-all with binding: `case x:` → `x = subject` + val subjectRef = createIdentifierNode(subjectRefName, Load, lineAndCol) + Seq(createAssignmentToIdentifier(name, subjectRef, lineAndCol)) + + case MatchAs(Some(inner), Some(name), _) => + // Pattern with alias: `case [a, b] as whole:` → recurse into inner + `whole = subject` + val innerBindings = lowerMatchPatternBindings(inner, subjectRefName, lineAndCol) + val subjectRef = createIdentifierNode(subjectRefName, Load, lineAndCol) + innerBindings :+ createAssignmentToIdentifier(name, subjectRef, lineAndCol) + + case MatchAs(_, None, _) => + // Wildcard `case _:` or unnamed pattern — no bindings + Seq.empty + + case MatchSequence(patterns, _) => + patterns.zipWithIndex.flatMap { case (elemPattern, index) => + lowerSequenceElementBinding(elemPattern, subjectRefName, index, lineAndCol) + }.toSeq + + case MatchStar(Some(name), _) => + // Star capture: `*rest` — bind to subject (simplified) + val subjectRef = createIdentifierNode(subjectRefName, Load, lineAndCol) + Seq(createAssignmentToIdentifier(name, subjectRef, lineAndCol)) + + case MatchMapping(keys, patterns, rest, _) => + val keyBindings = keys.zip(patterns).flatMap { case (key, valuePattern) => + lowerMappingElementBinding(valuePattern, subjectRefName, key, lineAndCol) + }.toSeq + val restBinding = rest.map { restName => + val subjectRef = createIdentifierNode(subjectRefName, Load, lineAndCol) + createAssignmentToIdentifier(restName, subjectRef, lineAndCol) + }.toSeq + keyBindings ++ restBinding + + case MatchClass(_, patterns, kwdAttrs, kwdPatterns, _) => + val positionalBindings = patterns.zipWithIndex.flatMap { case (elemPattern, index) => + lowerSequenceElementBinding(elemPattern, subjectRefName, index, lineAndCol) + }.toSeq + val keywordBindings = kwdAttrs.zip(kwdPatterns).flatMap { case (attrName, attrPattern) => + lowerAttributeBinding(attrPattern, subjectRefName, attrName, lineAndCol) + }.toSeq + positionalBindings ++ keywordBindings + + case MatchOr(patterns, _) => + // All alternatives must bind the same names in Python. Process the first. + patterns.headOption.map(lowerMatchPatternBindings(_, subjectRefName, lineAndCol)).getOrElse(Seq.empty) + + case _: MatchValue | _: MatchSingleton => + // Literal/singleton patterns have no variable bindings + Seq.empty + + case _ => Seq.empty + } + } + + /** Lower a sequence element pattern at a given index. For named bindings, creates `name = subject[index]`. For nested + * patterns like `[a, [b, c]]`, creates a temp variable for the nested subject. + */ + private def lowerSequenceElementBinding( + elemPattern: ipattern, + subjectRefName: String, + index: Int, + lineAndCol: LineAndColumn + ): Seq[nodes.NewNode] = { + elemPattern match { + case MatchAs(None, Some(name), _) => + // Direct binding: `a` in `case [a, b]:` → `a = subject[index]` + val subjectRef = createIdentifierNode(subjectRefName, Load, lineAndCol) + val indexNode = nodeBuilder.intLiteralNode(index.toString, lineAndCol) + val indexAccess = createIndexAccess(subjectRef, indexNode, lineAndCol) + Seq(createAssignmentToIdentifier(name, indexAccess, lineAndCol)) + + case MatchStar(Some(name), _) => + // Star capture in sequence: `*rest` in `case [a, *rest]:` → `rest = subject[index]` + val subjectRef = createIdentifierNode(subjectRefName, Load, lineAndCol) + val indexNode = nodeBuilder.intLiteralNode(index.toString, lineAndCol) + val indexAccess = createIndexAccess(subjectRef, indexNode, lineAndCol) + Seq(createAssignmentToIdentifier(name, indexAccess, lineAndCol)) + + case nested @ (_: MatchSequence | _: MatchMapping | _: MatchClass | _: MatchOr) => + // Nested pattern: create a temp variable for `subject[index]`, then recurse + val tmpName = getUnusedName() + val subjectRef = createIdentifierNode(subjectRefName, Load, lineAndCol) + val indexNode = nodeBuilder.intLiteralNode(index.toString, lineAndCol) + val indexAccess = createIndexAccess(subjectRef, indexNode, lineAndCol) + val tmpAssign = createAssignmentToIdentifier(tmpName, indexAccess, lineAndCol) + tmpAssign +: lowerMatchPatternBindings(nested, tmpName, lineAndCol) + + case MatchAs(Some(inner), Some(name), _) => + // Aliased nested pattern: `[a, b] as x` at position + val subjectRef = createIdentifierNode(subjectRefName, Load, lineAndCol) + val indexNode = nodeBuilder.intLiteralNode(index.toString, lineAndCol) + val indexAccess = createIndexAccess(subjectRef, indexNode, lineAndCol) + val tmpName = getUnusedName() + val tmpAssign = createAssignmentToIdentifier(tmpName, indexAccess, lineAndCol) + val nameAssign = createAssignmentToIdentifier(name, createIdentifierNode(tmpName, Load, lineAndCol), lineAndCol) + (tmpAssign +: lowerMatchPatternBindings(inner, tmpName, lineAndCol)) :+ nameAssign + + case _ => + // MatchValue, MatchSingleton, MatchAs(_, None, _) — no bindings + Seq.empty + } + } + + /** Lower a mapping pattern element: `name = subject[key]` */ + private def lowerMappingElementBinding( + valuePattern: ipattern, + subjectRefName: String, + key: iexpr, + lineAndCol: LineAndColumn + ): Seq[nodes.NewNode] = { + valuePattern match { + case MatchAs(None, Some(name), _) => + val subjectRef = createIdentifierNode(subjectRefName, Load, lineAndCol) + val keyNode = convert(key) + val indexAccess = createIndexAccess(subjectRef, keyNode, lineAndCol) + Seq(createAssignmentToIdentifier(name, indexAccess, lineAndCol)) + + case nested => + // For nested patterns, create temp for subject[key] and recurse + val bindings = extractPatternBindingNames(nested) + if (bindings.nonEmpty) { + val tmpName = getUnusedName() + val subjectRef = createIdentifierNode(subjectRefName, Load, lineAndCol) + val keyNode = convert(key) + val indexAccess = createIndexAccess(subjectRef, keyNode, lineAndCol) + val tmpAssign = createAssignmentToIdentifier(tmpName, indexAccess, lineAndCol) + tmpAssign +: lowerMatchPatternBindings(nested, tmpName, lineAndCol) + } else Seq.empty + } + } + + /** Lower a class attribute pattern: `name = subject.attr` */ + private def lowerAttributeBinding( + attrPattern: ipattern, + subjectRefName: String, + attrName: String, + lineAndCol: LineAndColumn + ): Seq[nodes.NewNode] = { + attrPattern match { + case MatchAs(None, Some(name), _) => + val subjectRef = createIdentifierNode(subjectRefName, Load, lineAndCol) + val fieldAccess = createFieldAccess(subjectRef, attrName, lineAndCol) + Seq(createAssignmentToIdentifier(name, fieldAccess, lineAndCol)) + + case nested => + val bindings = extractPatternBindingNames(nested) + if (bindings.nonEmpty) { + val tmpName = getUnusedName() + val subjectRef = createIdentifierNode(subjectRefName, Load, lineAndCol) + val fieldAccess = createFieldAccess(subjectRef, attrName, lineAndCol) + val tmpAssign = createAssignmentToIdentifier(tmpName, fieldAccess, lineAndCol) + tmpAssign +: lowerMatchPatternBindings(nested, tmpName, lineAndCol) + } else Seq.empty + } + } + + /** Check if a pattern contains any variable bindings (used to avoid creating unnecessary temps). */ + private def extractPatternBindingNames(pattern: ipattern): Seq[String] = { + pattern match { + case MatchAs(inner, name, _) => name.toSeq ++ inner.toSeq.flatMap(extractPatternBindingNames) + case MatchSequence(patterns, _) => patterns.flatMap(extractPatternBindingNames).toSeq + case MatchStar(name, _) => name.toSeq + case MatchMapping(_, patterns, rest, _) => patterns.flatMap(extractPatternBindingNames).toSeq ++ rest.toSeq + case MatchClass(_, patterns, _, kwdPatterns, _) => + patterns.flatMap(extractPatternBindingNames).toSeq ++ kwdPatterns.flatMap(extractPatternBindingNames).toSeq + case MatchOr(patterns, _) => patterns.headOption.toSeq.flatMap(extractPatternBindingNames) + case _ => Seq.empty + } } def convert(raise: ast.Raise): NewNode = { diff --git a/joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/cpg/MatchCpgTests.scala b/joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/cpg/MatchCpgTests.scala index c6e80149cacf..6596a304562c 100644 --- a/joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/cpg/MatchCpgTests.scala +++ b/joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/cpg/MatchCpgTests.scala @@ -1,14 +1,14 @@ package io.joern.pysrc2cpg.cpg import io.joern.pysrc2cpg.testfixtures.PySrc2CpgFixture -import io.shiftleft.codepropertygraph.generated.{NodeTypes, nodes} +import io.shiftleft.codepropertygraph.generated.{NodeTypes, Operators} import io.shiftleft.semanticcpg.language.* class MatchCpgTests extends PySrc2CpgFixture() { "match statement with guards" should { val cpg = code(""" - |def someFunc(): - | match [1, 2]: + |def someFunc(command): + | match command: | case [a, b] if 3 == 4: | print(1) | case _ if 5 == 6: @@ -20,9 +20,8 @@ class MatchCpgTests extends PySrc2CpgFixture() { "have correct AST" in { val matchStmt = cpg.controlStructure.head val condition = matchStmt.astChildren.order(1).head - condition.label shouldBe NodeTypes.CALL - condition.code shouldBe "[1, 2]" - condition.lineNumber shouldBe Some(3) + condition.label shouldBe NodeTypes.IDENTIFIER + condition.code shouldBe "command" val matchBodyBlock = matchStmt.astChildren.order(2).head matchBodyBlock.label shouldBe NodeTypes.BLOCK @@ -40,13 +39,11 @@ class MatchCpgTests extends PySrc2CpgFixture() { jumpTargetFirstCase.code shouldBe "case [a, b] if 3 == 4" blockFirstCase.label shouldBe NodeTypes.BLOCK - blockFirstCase.code shouldBe "print(1)" jumpTargetSecondCase.label shouldBe NodeTypes.JUMP_TARGET jumpTargetSecondCase.code shouldBe "case _ if 5 == 6" blockSecondCase.label shouldBe NodeTypes.BLOCK - blockSecondCase.code shouldBe "print(2)" jumpTargetThirdCase.label shouldBe NodeTypes.JUMP_TARGET jumpTargetThirdCase.code shouldBe "default" @@ -60,8 +57,8 @@ class MatchCpgTests extends PySrc2CpgFixture() { "match statement" should { val cpg = code(""" - |def someFunc(): - | match [1, 2]: + |def someFunc(command): + | match command: | case [a, b]: | print(1) | case _: @@ -71,9 +68,8 @@ class MatchCpgTests extends PySrc2CpgFixture() { "have correct AST" in { val matchStmt = cpg.controlStructure.head val condition = matchStmt.astChildren.order(1).head - condition.label shouldBe NodeTypes.CALL - condition.code shouldBe "[1, 2]" - condition.lineNumber shouldBe Some(3) + condition.label shouldBe NodeTypes.IDENTIFIER + condition.code shouldBe "command" val matchBodyBlock = matchStmt.astChildren.order(2).head matchBodyBlock.label shouldBe NodeTypes.BLOCK @@ -84,7 +80,6 @@ class MatchCpgTests extends PySrc2CpgFixture() { jumpTargetFirstCase.code shouldBe "case [a, b]" blockFirstCase.label shouldBe NodeTypes.BLOCK - blockFirstCase.code shouldBe "print(1)" jumpTargetSecondCase.label shouldBe NodeTypes.JUMP_TARGET jumpTargetSecondCase.code shouldBe "default" @@ -96,40 +91,124 @@ class MatchCpgTests extends PySrc2CpgFixture() { } "have correct CFG" in { - val methodNode = cpg.method.nameExact("someFunc").head - methodNode.cfgOut.l match { - case List(firstConditionExpr) => - firstConditionExpr.code shouldBe "1" - case other => fail(s"Expected 1 CFG successor, but got ${other.size}: $other") + // The condition identifier should have CFG edges to the jump targets + val conditionId = cpg.identifier.nameExact("command").lineNumber(3).head + conditionId.cfgOut.l match { + case List(jumpTarget1, jumpTarget2) => + Set(jumpTarget1.code, jumpTarget2.code) shouldBe Set("case [a, b]", "default") + case other => fail(s"Expected 2 CFG successors, but got ${other.size}: $other") } + } + } - val conditionCall = cpg.call.codeExact("[1, 2]").head - conditionCall.cfgOut.l match { - case List(jumpTargetFirstCase, jumpTargetSecondCase) => - jumpTargetFirstCase.label shouldBe NodeTypes.JUMP_TARGET - jumpTargetFirstCase.code shouldBe "case [a, b]" + "match statement with sequence pattern bindings" should { + val cpg = code(""" + |def someFunc(command): + | match command: + | case [a, b]: + | print(a, b) + |""".stripMargin) - jumpTargetSecondCase.label shouldBe NodeTypes.JUMP_TARGET - jumpTargetSecondCase.code shouldBe "default" - case other => fail(s"Expected 2 CFG successors, but got ${other.size}: $other") - } + "create assignment nodes for pattern variables" in { + val matchStmt = cpg.controlStructure.head + val caseBlock = matchStmt.astChildren.order(2).head.astChildren.l(1) + caseBlock.label shouldBe NodeTypes.BLOCK - val print1Block = cpg.block.codeExact("print(1)").head - print1Block.cfgOut.l match { - case List(methodReturn) => - methodReturn.label shouldBe NodeTypes.METHOD_RETURN - case other => fail(s"Expected 1 CFG successor, but got ${other.size}: $other") - } + // Pattern assignments should be in the case block + val assignments = cpg.call.methodFullName(Operators.assignment).l + .filter(_.code.matches("a = command\\[0\\]|b = command\\[1\\]")) + assignments.size shouldBe 2 + } - val print2Block = cpg.block.codeExact("print(2)").head - print2Block.cfgOut.l match { - case List(methodReturn) => - methodReturn.label shouldBe NodeTypes.METHOD_RETURN - case other => fail(s"Expected 1 CFG successor, but got ${other.size}: $other") - } + "use index access for sequence element extraction" in { + val indexAccesses = cpg.call.methodFullName(Operators.indexAccess).l + .filter(_.code.matches("command\\[0\\]|command\\[1\\]")) + indexAccesses.size shouldBe 2 + } + } + + "match statement with named binding (catch-all)" should { + val cpg = code(""" + |def someFunc(command): + | match command: + | case x: + | print(x) + |""".stripMargin) + + "create assignment for catch-all binding" in { + val assignments = cpg.call.methodFullName(Operators.assignment).codeExact("x = command").l + assignments.size shouldBe 1 + } + } + + "match statement with wildcard" should { + val cpg = code(""" + |def someFunc(command): + | match command: + | case _: + | print("default") + |""".stripMargin) + + "not create pattern assignments for wildcard" in { + // The only assignment-like thing should be the method body, not pattern bindings + val patternAssignments = cpg.call.methodFullName(Operators.assignment).l + .filter(_.code.contains("command")) + patternAssignments.size shouldBe 0 + } + } + + "match statement with literal pattern" should { + val cpg = code(""" + |def someFunc(command): + | match command: + | case 42: + | print("found") + |""".stripMargin) + + "not create pattern assignments for literal" in { + val patternAssignments = cpg.call.methodFullName(Operators.assignment).l + .filter(_.code.contains("command")) + patternAssignments.size shouldBe 0 + } + } + "match statement with complex subject" should { + val cpg = code(""" + |def someFunc(): + | match get_data(): + | case [a, b]: + | print(a, b) + |""".stripMargin) + + "create temp variable for complex subject" in { + // Complex expression subjects get a temp variable + val tmpAssignments = cpg.call.methodFullName(Operators.assignment).l + .filter(_.code.matches("tmp\\d+ = get_data\\(\\)")) + tmpAssignments.size shouldBe 1 } + "create pattern assignments referencing temp" in { + val assignments = cpg.call.methodFullName(Operators.assignment).l + .filter(_.code.matches("[ab] = tmp\\d+\\[\\d+\\]")) + assignments.size shouldBe 2 + } } + "match statement with alias pattern" should { + val cpg = code(""" + |def someFunc(command): + | match command: + | case [a, b] as whole: + | print(whole) + |""".stripMargin) + + "create assignments for both inner bindings and alias" in { + val aAssign = cpg.call.methodFullName(Operators.assignment).l + .filter(_.code.matches("a = command\\[0\\]")) + aAssign.size shouldBe 1 + + val wholeAssign = cpg.call.methodFullName(Operators.assignment).codeExact("whole = command").l + wholeAssign.size shouldBe 1 + } + } } diff --git a/joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/dataflow/DataFlowTests.scala b/joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/dataflow/DataFlowTests.scala index 26ab208eb5f4..57ce15df263e 100644 --- a/joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/dataflow/DataFlowTests.scala +++ b/joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/dataflow/DataFlowTests.scala @@ -859,6 +859,30 @@ class InternalMethodCustomSemanticsDataFlowTest val flows = sink.reachableByFlows(source) flows shouldBe empty } + + "data flow through match pattern sequence binding" in { + val cpg = code(""" + |def process(user_input): + | match user_input: + | case [action, target]: + | print(action) + |""".stripMargin) + val source = cpg.method("process").parameter.nameExact("user_input") + val sink = cpg.call("print").argument(1) + sink.reachableByFlows(source).size shouldBe 1 + } + + "data flow through match pattern catch-all binding" in { + val cpg = code(""" + |def process(user_input): + | match user_input: + | case x: + | print(x) + |""".stripMargin) + val source = cpg.method("process").parameter.nameExact("user_input") + val sink = cpg.call("print").argument(1) + sink.reachableByFlows(source).size shouldBe 1 + } } class DefaultSemanticsDataFlowTest1 extends PySrc2CpgFixture(withOssDataflow = true, semantics = DefaultSemantics()) {