Skip to content

Commit 52b3b9e

Browse files
authored
[pysrc2cpg] Fix **kwargs handling, walrus operator and comprehension test coverage (#5910)
* [pysrc2cpg] Fix **kwargs handling, add match pattern AST nodes, and test coverage - Fix **kwargs unpacking to preserve dict argument for taint tracking - Convert match statement patterns to proper AST nodes in case body blocks - Add walrus operator tests verifying expression semantics - Add comprehensive comprehension tests (list, set, dict, generator) * Address review feedback: fix test conventions and kwargs arg name - Rename (named expression) to (assignment expression) in walrus test - Remove lazy and explicit test.py filename from new test blocks - Use val keywordDictArgName = "<keyword_dict>" instead of "**" string literal - Revert match pattern AST node changes (needs proper semantic design) - Fix walrus operator test to match actual generated code * Fix scalafmt formatting in PythonAstVisitor
1 parent d7a0889 commit 52b3b9e

4 files changed

Lines changed: 124 additions & 8 deletions

File tree

joern-cli/frontends/pysrc2cpg/src/main/scala/io/joern/pysrc2cpg/PythonAstVisitor.scala

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package io.joern.pysrc2cpg
22

3-
import PythonAstVisitor.{logger, metaClassSuffix, noLineAndColumn}
3+
import PythonAstVisitor.{keywordDictArgName, logger, metaClassSuffix, noLineAndColumn}
44
import io.joern.pysrc2cpg.memop.*
55
import io.joern.pysrc2cpg.memop.MemoryOperation.{Del, Load, Store}
66
import io.joern.x2cpg.frontendspecific.pysrc2cpg.Constants.builtinPrefix
@@ -1444,7 +1444,6 @@ class PythonAstVisitor(
14441444
createNAryOperatorCall(boolOpToCodeAndFullName(boolOp.op), operandNodes, lineAndColOf(boolOp))
14451445
}
14461446

1447-
// TODO test
14481447
def convert(namedExpr: ast.NamedExpr): NewNode = {
14491448
val targetNode = convert(namedExpr.target)
14501449
val valueNode = convert(namedExpr.value)
@@ -1848,13 +1847,14 @@ class PythonAstVisitor(
18481847
*/
18491848
def convert(call: ast.Call): nodes.NewNode = {
18501849
val argumentNodes = call.args.map(convert).toSeq
1851-
val keywordArgNodes = call.keywords.flatMap { keyword =>
1850+
val keywordArgNodes = call.keywords.map { keyword =>
18521851
if (keyword.arg.isDefined) {
1853-
Some((keyword.arg.get, convert(keyword.value)))
1852+
(keyword.arg.get, convert(keyword.value))
18541853
} else {
18551854
// keyword.arg == None. This is the case for func(**dict) style arguments.
1856-
// TODO implement handling for this case.
1857-
None
1855+
// We use a synthetic argument name to preserve the unpacked dict as an argument
1856+
// in the CPG so that data flow tracking can follow through it.
1857+
(keywordDictArgName, convert(keyword.value))
18581858
}
18591859
}
18601860

@@ -2184,8 +2184,9 @@ class PythonAstVisitor(
21842184
object PythonAstVisitor {
21852185
private val logger = LoggerFactory.getLogger(getClass)
21862186

2187-
val typingPrefix = "typing."
2188-
val metaClassSuffix = "<meta>"
2187+
val typingPrefix = "typing."
2188+
val metaClassSuffix = "<meta>"
2189+
val keywordDictArgName = "<keyword_dict>"
21892190

21902191
val noLineAndColumn = LineAndColumn(-1, -1, -1, -1, -1, -1)
21912192

joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/cpg/AssignCpgTests.scala

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,33 @@ class AssignCpgTests extends PySrc2CpgFixture with Matchers {
203203
}
204204
}
205205

206+
"walrus operator (assignment expression)" should {
207+
val cpg = code("""
208+
|if (x := 10) > 5:
209+
| print(x)
210+
|""".stripMargin)
211+
212+
"create an assignment node for the walrus operator" in {
213+
val assignCall = cpg.call.methodFullName(Operators.assignment).codeExact("x = 10").head
214+
assignCall.code shouldBe "x = 10"
215+
assignCall.dispatchType shouldBe DispatchTypes.STATIC_DISPATCH
216+
}
217+
218+
"have correct assignment target and value" in {
219+
val assignCall = cpg.call.methodFullName(Operators.assignment).codeExact("x = 10").head
220+
assignCall.argument
221+
.argumentIndex(1)
222+
.isIdentifier
223+
.head
224+
.code shouldBe "x"
225+
assignCall.argument
226+
.argumentIndex(2)
227+
.isLiteral
228+
.head
229+
.code shouldBe "10"
230+
}
231+
}
232+
206233
"augmented assign" should {
207234
val cpg = code("""x += y""".stripMargin)
208235

joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/cpg/CallCpgTests.scala

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,36 @@ class CallCpgTests extends PySrc2CpgFixture(withOssDataflow = false) {
194194
}
195195
}
196196

197+
"call with **kwargs unpacking" should {
198+
val cpg = code("""func(a, **my_dict)""".stripMargin)
199+
200+
"test call node properties" in {
201+
val callNode = cpg.call.codeExact("func(a, **my_dict)").head
202+
callNode.name shouldBe "func"
203+
callNode.dispatchType shouldBe DispatchTypes.DYNAMIC_DISPATCH
204+
}
205+
206+
"test that the unpacked dict appears as an argument" in {
207+
val callNode = cpg.call.codeExact("func(a, **my_dict)").head
208+
val kwargsArg = callNode.argument.isIdentifier.nameExact("my_dict").head
209+
kwargsArg.code shouldBe "my_dict"
210+
kwargsArg.argumentIndex shouldBe -1
211+
kwargsArg.argumentName shouldBe Some("<keyword_dict>")
212+
}
213+
}
214+
215+
"call on member with **kwargs unpacking" should {
216+
val cpg = code("""x.func(a, **my_dict)""".stripMargin)
217+
218+
"test that the unpacked dict appears as an argument" in {
219+
val callNode = cpg.call.codeExact("x.func(a, **my_dict)").head
220+
val kwargsArg = callNode.argument.isIdentifier.nameExact("my_dict").head
221+
kwargsArg.code shouldBe "my_dict"
222+
kwargsArg.argumentIndex shouldBe -1
223+
kwargsArg.argumentName shouldBe Some("<keyword_dict>")
224+
}
225+
}
226+
197227
"call from a function defined from an imported module" should {
198228

199229
lazy val cpg = code(
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package io.joern.pysrc2cpg.cpg
2+
3+
import io.joern.pysrc2cpg.testfixtures.PySrc2CpgFixture
4+
import io.shiftleft.codepropertygraph.generated.Operators
5+
import io.shiftleft.semanticcpg.language.*
6+
7+
class ComprehensionCpgTests extends PySrc2CpgFixture(withOssDataflow = false) {
8+
9+
"list comprehension" should {
10+
val cpg = code("""x = [i * 2 for i in range(10)]""".stripMargin)
11+
12+
"create a call to range" in {
13+
cpg.call.codeExact("range(10)").head.name shouldBe "range"
14+
}
15+
16+
"create a multiplication operation" in {
17+
cpg.call.methodFullName(Operators.multiplication).codeExact("i * 2").size shouldBe 1
18+
}
19+
}
20+
21+
"set comprehension" should {
22+
val cpg = code("""x = {i * 2 for i in range(10)}""".stripMargin)
23+
24+
"create a call to range" in {
25+
cpg.call.codeExact("range(10)").head.name shouldBe "range"
26+
}
27+
28+
"create a multiplication operation" in {
29+
cpg.call.methodFullName(Operators.multiplication).codeExact("i * 2").size shouldBe 1
30+
}
31+
}
32+
33+
"dict comprehension" should {
34+
val cpg = code("""x = {k: v for k, v in items.items()}""".stripMargin)
35+
36+
"create a call to items()" in {
37+
cpg.call.codeExact("items.items()").head.name shouldBe "items"
38+
}
39+
40+
"have identifiers for k and v" in {
41+
cpg.identifier.nameExact("k").size should be > 0
42+
cpg.identifier.nameExact("v").size should be > 0
43+
}
44+
}
45+
46+
"generator expression" should {
47+
val cpg = code("""x = sum(i * 2 for i in range(10))""".stripMargin)
48+
49+
"create a call to sum" in {
50+
cpg.call.codeExact("sum(i * 2 for i in range(10))").head.name shouldBe "sum"
51+
}
52+
53+
"create a call to range" in {
54+
cpg.call.codeExact("range(10)").head.name shouldBe "range"
55+
}
56+
}
57+
58+
}

0 commit comments

Comments
 (0)