Skip to content

Commit d032bf5

Browse files
authored
Merge pull request github#13685 from RasmusWL/captured-variables-default-param-value
Python: Model parameter with default value as `DefinitionNode`
2 parents 69b98c7 + 98ed5cf commit d032bf5

File tree

9 files changed

+60
-19
lines changed

9 files changed

+60
-19
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Parameters with a default value are now considered a `DefinitionNode`. This improvement was motivated by allowing type-tracking and API graphs to follow flow from such a default value to a use by a captured variable.

python/ql/lib/semmle/python/Flow.qll

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -640,12 +640,23 @@ class DefinitionNode extends ControlFlowNode {
640640
exists(Assign a | list_or_tuple_nested_element(a.getATarget()).getAFlowNode() = this)
641641
or
642642
exists(For for | for.getTarget().getAFlowNode() = this)
643+
or
644+
exists(Parameter param | this = param.asName().getAFlowNode() and exists(param.getDefault()))
643645
}
644646

645647
/** flow node corresponding to the value assigned for the definition corresponding to this flow node */
646648
ControlFlowNode getValue() {
647649
result = assigned_value(this.getNode()).getAFlowNode() and
648-
(result.getBasicBlock().dominates(this.getBasicBlock()) or result.isImport())
650+
(
651+
result.getBasicBlock().dominates(this.getBasicBlock())
652+
or
653+
result.isImport()
654+
or
655+
// since the default value for a parameter is evaluated in the same basic block as
656+
// the function definition, but the parameter belongs to the basic block of the function,
657+
// there is no dominance relationship between the two.
658+
exists(Parameter param | this = param.asName().getAFlowNode())
659+
)
649660
}
650661
}
651662

@@ -795,6 +806,8 @@ private AstNode assigned_value(Expr lhs) {
795806
or
796807
/* for lhs in seq: => `result` is the `for` node, representing the `iter(next(seq))` operation. */
797808
result.(For).getTarget() = lhs
809+
or
810+
exists(Parameter param | lhs = param.asName() and result = param.getDefault())
798811
}
799812

800813
predicate nested_sequence_assign(

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

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,15 @@ predicate runtimeJumpStep(Node nodeFrom, Node nodeTo) {
473473
or
474474
// Setting the possible values of the variable at the end of import time
475475
nodeFrom = nodeTo.(ModuleVariableNode).getADefiningWrite()
476+
or
477+
// a parameter with a default value, since the parameter will be in the scope of the
478+
// function, while the default value itself will be in the scope that _defines_ the
479+
// function.
480+
exists(ParameterDefinition param |
481+
// note: we go to the _control-flow node_ of the parameter, and not the ESSA node of the parameter, since for type-tracking, the ESSA node is not a LocalSourceNode, so we would get in trouble.
482+
nodeFrom.asCfgNode() = param.getDefault() and
483+
nodeTo.asCfgNode() = param.getDefiningNode()
484+
)
476485
}
477486

478487
/**
@@ -574,9 +583,6 @@ predicate jumpStepSharedWithTypeTracker(Node nodeFrom, Node nodeTo) {
574583
r.getAttributeName(), nodeFrom) and
575584
nodeTo = r
576585
)
577-
or
578-
// Default value for parameter flows to that parameter
579-
defaultValueFlowStep(nodeFrom, nodeTo)
580586
}
581587

582588
/**
@@ -797,19 +803,6 @@ predicate attributeStoreStep(Node nodeFrom, AttributeContent c, PostUpdateNode n
797803
)
798804
}
799805

800-
predicate defaultValueFlowStep(CfgNode nodeFrom, CfgNode nodeTo) {
801-
exists(Function f, Parameter p, ParameterDefinition def |
802-
// `getArgByName` supports, unlike `getAnArg`, keyword-only parameters
803-
p = f.getArgByName(_) and
804-
nodeFrom.asExpr() = p.getDefault() and
805-
// The following expresses
806-
// nodeTo.(ParameterNode).getParameter() = p
807-
// without non-monotonic recursion
808-
def.getParameter() = p and
809-
nodeTo.getNode() = def.getDefiningNode()
810-
)
811-
}
812-
813806
/**
814807
* Holds if data can flow from `nodeFrom` to `nodeTo` via a read of content `c`.
815808
*/

python/ql/lib/semmle/python/essa/SsaDefinitions.qll

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ module SsaSource {
2020
/** Holds if `v` is defined by assignment at `defn` and given `value`. */
2121
cached
2222
predicate assignment_definition(Variable v, ControlFlowNode defn, ControlFlowNode value) {
23-
defn.(NameNode).defines(v) and defn.(DefinitionNode).getValue() = value
23+
defn.(NameNode).defines(v) and
24+
defn.(DefinitionNode).getValue() = value and
25+
// since parameter will be considered a DefinitionNode, if it has a default value,
26+
// we need to exclude it here since it is already covered by parameter_definition
27+
// (and points-to was unhappy that it was included in both)
28+
not parameter_definition(v, defn)
2429
}
2530

2631
/** Holds if `v` is defined by assignment of the captured exception. */

python/ql/test/experimental/dataflow/typetracking/test.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,19 @@ def foo():
6565
also_x = foo() # $ tracked
6666
print(also_x) # $ tracked
6767

68+
69+
def from_parameter_default():
70+
x_alias = tracked # $tracked
71+
def outer(x=tracked): # $tracked
72+
print(x) # $tracked
73+
def inner():
74+
print(x) # $ tracked
75+
print(x_alias) # $tracked
76+
return x # $tracked
77+
also_x = outer() # $tracked
78+
print(also_x) # $tracked
79+
80+
6881
# ------------------------------------------------------------------------------
6982
# Function decorator
7083
# ------------------------------------------------------------------------------
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
| test.py:3:5:3:9 | ControlFlowNode for fail5 | test.py:3:1:3:13 | ControlFlowNode for FunctionExpr |
2+
| test.py:4:5:4:8 | ControlFlowNode for Tuple | test.py:4:12:4:12 | ControlFlowNode for t |
3+
| test.py:7:5:7:26 | ControlFlowNode for default_value_in_param | test.py:7:1:7:33 | ControlFlowNode for FunctionExpr |
4+
| test.py:7:28:7:28 | ControlFlowNode for x | test.py:7:30:7:31 | ControlFlowNode for IntegerLiteral |
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import python
2+
3+
from DefinitionNode d
4+
select d, d.getValue()
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
| 3 | 5 | ControlFlowNode for fail5 |
22
| 4 | 5 | ControlFlowNode for Tuple |
33
| 4 | 5 | ControlFlowNode for x |
4-
| 4 | 8 | ControlFlowNode for y |
4+
| 4 | 8 | ControlFlowNode for y |
5+
| 7 | 5 | ControlFlowNode for default_value_in_param |
6+
| 7 | 28 | ControlFlowNode for x |

python/ql/test/library-tests/variables/definitions/test.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@
33
def fail5(t):
44
x, y = t
55
return x
6+
7+
def default_value_in_param(x=42):
8+
print(x)

0 commit comments

Comments
 (0)