Skip to content

Commit 02b3a1b

Browse files
committed
Python: At most one **kwargs ParameterNode per callable
Similar to the Ruby changes from github#11461 I feel the change to `DataFlowFunciton.getParameter` where we use `not exists(func.getArgByName(_))` is not very great, but I was not allowed to use `not exists(this.getParameter(any(ParameterPosition _).isKeyword(_)))` because of negative recursion.
1 parent f262dc6 commit 02b3a1b

File tree

4 files changed

+59
-18
lines changed

4 files changed

+59
-18
lines changed

python/ql/lib/semmle/python/dataflow/new/internal/DataFlowDispatch.qll

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,7 @@ abstract class DataFlowFunction extends DataFlowCallable, TFunction {
321321
or
322322
exists(string name | ppos.isKeyword(name) | result.getParameter() = func.getArgByName(name))
323323
or
324+
// `*args`
324325
exists(int index |
325326
(
326327
ppos.isStarArgs(index) and
@@ -343,9 +344,22 @@ abstract class DataFlowFunction extends DataFlowCallable, TFunction {
343344
not exists(func.getArg(_)) and index = 0
344345
)
345346
or
346-
ppos.isDictSplat() and result.getParameter() = func.getKwarg()
347-
or
348-
ppos.isDictSplat() and result = TSynthDictSplatParameterNode(this)
347+
// `**kwargs`
348+
// since dataflow library has restriction that we can only have ONE result per
349+
// parameter position, if there is both a synthetic **kwargs and a real **kwargs
350+
// parameter, we only give the result for the synthetic, and add local flow from the
351+
// synthetic to the real. It might seem more natural to do it in the other
352+
// direction, but since we have a clearStep on the real **kwargs parameter, we that
353+
// content-clearing would also affect the synthetic parameter, which we don't want.
354+
(
355+
not exists(func.getArgByName(_)) and
356+
ppos.isDictSplat() and
357+
result.getParameter() = func.getKwarg()
358+
or
359+
exists(func.getArgByName(_)) and
360+
ppos.isDictSplat() and
361+
result = TSynthDictSplatParameterNode(this)
362+
)
349363
}
350364
}
351365

@@ -1400,7 +1414,16 @@ class SummaryParameterNode extends ParameterNodeImpl, TSummaryParameterNode {
14001414
override Parameter getParameter() { none() }
14011415

14021416
override predicate isParameterOf(DataFlowCallable c, ParameterPosition ppos) {
1403-
sc = c.asLibraryCallable() and ppos = pos
1417+
sc = c.asLibraryCallable() and
1418+
ppos = pos and
1419+
// avoid overlap with `SynthDictSplatParameterNode`
1420+
not (
1421+
pos.isDictSplat() and
1422+
exists(ParameterPosition keywordPos |
1423+
FlowSummaryImpl::Private::summaryParameterNodeRange(sc, keywordPos) and
1424+
keywordPos.isKeyword(_)
1425+
)
1426+
)
14041427
}
14051428

14061429
override DataFlowCallable getEnclosingCallable() { result.asLibraryCallable() = sc }

python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPrivate.qll

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,11 @@ private predicate synthDictSplatArgumentNodeStoreStep(
224224
private predicate dictSplatParameterNodeClearStep(ParameterNode n, DictionaryElementContent c) {
225225
exists(DataFlowCallable callable, ParameterPosition dictSplatPos, ParameterPosition keywordPos |
226226
dictSplatPos.isDictSplat() and
227-
n = callable.getParameter(dictSplatPos) and
228-
not n instanceof SynthDictSplatParameterNode and
227+
(
228+
n.getParameter() = callable.(DataFlowFunction).getScope().getKwarg()
229+
or
230+
n = TSummaryParameterNode(callable.asLibraryCallable(), dictSplatPos)
231+
) and
229232
exists(callable.getParameter(keywordPos)) and
230233
keywordPos.isKeyword(c.getKey())
231234
)
@@ -276,6 +279,31 @@ class SynthDictSplatParameterNode extends ParameterNodeImpl, TSynthDictSplatPara
276279
override Parameter getParameter() { none() }
277280
}
278281

282+
/**
283+
* Flow step from the synthetic `**kwargs` parameter to the real `**kwargs` parameter.
284+
* Due to restriction in dataflow library, we can only give one of them as result for
285+
* `DataFlowCallable.getParameter`, so this is a workaround to ensure there is flow to
286+
* _both_ of them.
287+
*/
288+
private predicate dictSplatParameterNodeFlowStep(
289+
ParameterNodeImpl nodeFrom, ParameterNodeImpl nodeTo
290+
) {
291+
exists(DataFlowCallable callable |
292+
nodeFrom = TSynthDictSplatParameterNode(callable) and
293+
(
294+
nodeTo.getParameter() = callable.(DataFlowFunction).getScope().getKwarg()
295+
or
296+
exists(ParameterPosition pos |
297+
nodeTo = TSummaryParameterNode(callable.asLibraryCallable(), pos) and
298+
pos.isDictSplat()
299+
)
300+
)
301+
)
302+
}
303+
304+
/**
305+
* Reads from the synthetic **kwargs parameter to each keyword parameter.
306+
*/
279307
predicate synthDictSplatParameterNodeReadStep(
280308
SynthDictSplatParameterNode nodeFrom, DictionaryElementContent c, ParameterNode nodeTo
281309
) {
@@ -418,6 +446,8 @@ predicate simpleLocalFlowStep(Node nodeFrom, Node nodeTo) {
418446
simpleLocalFlowStepForTypetracking(nodeFrom, nodeTo)
419447
or
420448
summaryFlowSteps(nodeFrom, nodeTo)
449+
or
450+
dictSplatParameterNodeFlowStep(nodeFrom, nodeTo)
421451
}
422452

423453
/**

python/ql/test/experimental/dataflow/consistency/dataflow-consistency.expected

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,5 @@ argHasPostUpdate
2020
postWithInFlow
2121
viableImplInCallContextTooLarge
2222
uniqueParameterNodeAtPosition
23-
| test.py:239:1:239:42 | Function overflowCallee | ** | test.py:239:1:239:42 | SynthDictSplatParameterNode | Parameters with overlapping positions. |
24-
| test.py:239:1:239:42 | Function overflowCallee | ** | test.py:239:35:239:40 | ControlFlowNode for kwargs | Parameters with overlapping positions. |
2523
uniqueParameterNodePosition
2624
uniqueContentApprox

python/ql/test/experimental/dataflow/coverage/dataflow-consistency.expected

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,5 @@ argHasPostUpdate
2020
postWithInFlow
2121
viableImplInCallContextTooLarge
2222
uniqueParameterNodeAtPosition
23-
| argumentPassing.py:50:1:60:2 | Function argument_passing | ** | argumentPassing.py:50:1:60:2 | SynthDictSplatParameterNode | Parameters with overlapping positions. |
24-
| argumentPassing.py:50:1:60:2 | Function argument_passing | ** | argumentPassing.py:59:7:59:7 | ControlFlowNode for g | Parameters with overlapping positions. |
25-
| argumentPassing.py:185:1:185:23 | Function mixed | ** | argumentPassing.py:185:1:185:23 | SynthDictSplatParameterNode | Parameters with overlapping positions. |
26-
| argumentPassing.py:185:1:185:23 | Function mixed | ** | argumentPassing.py:185:16:185:21 | ControlFlowNode for kwargs | Parameters with overlapping positions. |
27-
| classes.py:441:5:441:41 | Function __prepare__ | ** | classes.py:441:5:441:41 | SynthDictSplatParameterNode | Parameters with overlapping positions. |
28-
| classes.py:441:5:441:41 | Function __prepare__ | ** | classes.py:441:36:441:39 | ControlFlowNode for kwds | Parameters with overlapping positions. |
29-
| test.py:407:1:407:28 | Function f_extra_keyword | ** | test.py:407:1:407:28 | SynthDictSplatParameterNode | Parameters with overlapping positions. |
30-
| test.py:407:1:407:28 | Function f_extra_keyword | ** | test.py:407:26:407:26 | ControlFlowNode for b | Parameters with overlapping positions. |
31-
| test.py:521:23:521:43 | Function lambda | ** | test.py:521:23:521:43 | SynthDictSplatParameterNode | Parameters with overlapping positions. |
32-
| test.py:521:23:521:43 | Function lambda | ** | test.py:521:35:521:35 | ControlFlowNode for b | Parameters with overlapping positions. |
3323
uniqueParameterNodePosition
3424
uniqueContentApprox

0 commit comments

Comments
 (0)