Skip to content

Commit d46453c

Browse files
committed
Python: Support named imports as attribute reads
Required a small change in `DataFlow::importModule` to get the desired behaviour (cf. the type trackers defined in `moduleattr.ql`, but this should be harmless. The node that is added doesn't have any flow anywhere.
1 parent df447c0 commit d46453c

File tree

6 files changed

+98
-2
lines changed

6 files changed

+98
-2
lines changed

python/ql/src/experimental/dataflow/internal/Attributes.qll

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,3 +218,31 @@ private class GetAttrCallAsAttrRead extends AttrRead, CfgNode {
218218
result = this.getAttributeNameExpr().asExpr().(StrConst).getText()
219219
}
220220
}
221+
222+
/**
223+
* A convenience class for embedding `ImportMemberNode` into `DataFlowCfgNode`, as the former is not
224+
* obviously a subtype of the latter.
225+
*/
226+
private class DataFlowImportMemberNode extends ImportMemberNode, DataFlowCfgNode { }
227+
228+
/**
229+
* Represents a named import as an attribute read. That is,
230+
* ```python
231+
* from module import attr as attr_ref
232+
* ```
233+
* is treated as if it is a read of the attribute `module.attr`, even if `module` is not imported directly.
234+
*/
235+
private class ModuleAttributeImportAsAttrRead extends AttrRead, CfgNode {
236+
override DataFlowImportMemberNode node;
237+
238+
override Node getObject() { result.asCfgNode() = node.getModule(_) }
239+
240+
override ExprNode getAttributeNameExpr() {
241+
// The name of an imported attribute doesn't exist as a `Node` in the control flow graph, as it
242+
// can only ever be an identifier, and is therefore represented directly as a string.
243+
// Use `getAttributeName` to access the name of the attribute.
244+
none()
245+
}
246+
247+
override string getAttributeName() { exists(node.getModule(result)) }
248+
}

python/ql/src/experimental/dataflow/internal/DataFlowUtil.qll

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ predicate localFlow(Node source, Node sink) { localFlowStep*(source, sink) }
3636
*
3737
* Also see `DataFlow::importMember`
3838
*/
39-
EssaNode importModule(string name) {
39+
Node importModule(string name) {
4040
exists(Variable var, Import imp, Alias alias |
4141
alias = imp.getAName() and
4242
alias.getAsname() = var.getAStore() and
@@ -45,8 +45,14 @@ EssaNode importModule(string name) {
4545
or
4646
name = alias.getValue().(ImportExpr).getImportedModuleName()
4747
) and
48-
result.getVar().(AssignmentDefinition).getSourceVariable() = var
48+
result.(EssaNode).getVar().(AssignmentDefinition).getSourceVariable() = var
4949
)
50+
or
51+
// In `from module import attr`, we want to consider `module` to be an expression that refers to a
52+
// module of that name, as this allows us to refer to attributes of this module, even if it's
53+
// never imported directly. Note that there crucially isn't any _flow_ from `module` to references
54+
// to that same identifier.
55+
result.asCfgNode().getNode() = any(ImportExpr i | i.getAnImportedModuleName() = name)
5056
}
5157

5258
/**

python/ql/test/experimental/dataflow/import-helper/ImportHelper.expected

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,36 @@
11
importModule
2+
| test1.py:1:8:1:12 | ControlFlowNode for ImportExpr | mypkg |
23
| test1.py:1:8:1:12 | GSSA Variable mypkg | mypkg |
4+
| test2.py:1:6:1:10 | ControlFlowNode for ImportExpr | mypkg |
5+
| test2.py:1:6:1:10 | ControlFlowNode for ImportExpr | mypkg |
36
| test2.py:1:19:1:21 | GSSA Variable foo | mypkg.foo |
47
| test2.py:1:24:1:26 | GSSA Variable bar | mypkg.bar |
8+
| test3.py:1:8:1:16 | ControlFlowNode for ImportExpr | mypkg |
9+
| test3.py:1:8:1:16 | ControlFlowNode for ImportExpr | mypkg.foo |
10+
| test3.py:2:8:2:16 | ControlFlowNode for ImportExpr | mypkg |
11+
| test3.py:2:8:2:16 | ControlFlowNode for ImportExpr | mypkg.bar |
512
| test3.py:2:8:2:16 | GSSA Variable mypkg | mypkg |
13+
| test4.py:1:8:1:16 | ControlFlowNode for ImportExpr | mypkg |
14+
| test4.py:1:8:1:16 | ControlFlowNode for ImportExpr | mypkg.foo |
615
| test4.py:1:21:1:24 | GSSA Variable _foo | mypkg.foo |
16+
| test4.py:2:8:2:16 | ControlFlowNode for ImportExpr | mypkg |
17+
| test4.py:2:8:2:16 | ControlFlowNode for ImportExpr | mypkg.bar |
718
| test4.py:2:21:2:24 | GSSA Variable _bar | mypkg.bar |
19+
| test5.py:1:8:1:12 | ControlFlowNode for ImportExpr | mypkg |
820
| test5.py:1:8:1:12 | GSSA Variable mypkg | mypkg |
21+
| test5.py:9:6:9:10 | ControlFlowNode for ImportExpr | mypkg |
922
| test5.py:9:26:9:29 | GSSA Variable _bar | mypkg.bar |
23+
| test6.py:1:8:1:12 | ControlFlowNode for ImportExpr | mypkg |
1024
| test6.py:1:8:1:12 | GSSA Variable mypkg | mypkg |
25+
| test6.py:5:8:5:16 | ControlFlowNode for ImportExpr | mypkg |
26+
| test6.py:5:8:5:16 | ControlFlowNode for ImportExpr | mypkg.foo |
1127
| test6.py:5:8:5:16 | GSSA Variable mypkg | mypkg |
28+
| test7.py:1:6:1:10 | ControlFlowNode for ImportExpr | mypkg |
1229
| test7.py:1:19:1:21 | GSSA Variable foo | mypkg.foo |
30+
| test7.py:5:8:5:16 | ControlFlowNode for ImportExpr | mypkg |
31+
| test7.py:5:8:5:16 | ControlFlowNode for ImportExpr | mypkg.foo |
1332
| test7.py:5:8:5:16 | GSSA Variable mypkg | mypkg |
33+
| test7.py:9:6:9:10 | ControlFlowNode for ImportExpr | mypkg |
1434
| test7.py:9:19:9:21 | GSSA Variable foo | mypkg.foo |
1535
importMember
1636
| test2.py:1:19:1:21 | GSSA Variable foo | mypkg | foo |
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from module import attr as attr_ref
2+
3+
x = attr_ref
4+
5+
def fun():
6+
y = attr_ref
7+
8+
# The following should _not_ be a reference to the above module, since we don't actually import it.
9+
z = module
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module_tracker
2+
| import_as_attr.py:1:6:1:11 | ControlFlowNode for ImportExpr |
3+
module_attr_tracker
4+
| import_as_attr.py:0:0:0:0 | ModuleVariableNode for Global Variable attr_ref in Module import_as_attr |
5+
| import_as_attr.py:1:20:1:35 | ControlFlowNode for ImportMember |
6+
| import_as_attr.py:1:28:1:35 | GSSA Variable attr_ref |
7+
| import_as_attr.py:3:1:3:1 | GSSA Variable x |
8+
| import_as_attr.py:3:5:3:12 | ControlFlowNode for attr_ref |
9+
| import_as_attr.py:6:5:6:5 | SSA variable y |
10+
| import_as_attr.py:6:9:6:16 | ControlFlowNode for attr_ref |
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import python
2+
import experimental.dataflow.DataFlow
3+
import experimental.dataflow.TypeTracker
4+
5+
DataFlow::Node module_tracker(TypeTracker t) {
6+
t.start() and
7+
result = DataFlow::importModule("module")
8+
or
9+
exists(TypeTracker t2 | result = module_tracker(t2).track(t2, t))
10+
}
11+
12+
query DataFlow::Node module_tracker() { result = module_tracker(DataFlow::TypeTracker::end()) }
13+
14+
DataFlow::Node module_attr_tracker(TypeTracker t) {
15+
t.startInAttr("attr") and
16+
result = module_tracker()
17+
or
18+
exists(TypeTracker t2 | result = module_attr_tracker(t2).track(t2, t))
19+
}
20+
21+
query DataFlow::Node module_attr_tracker() {
22+
result = module_attr_tracker(DataFlow::TypeTracker::end())
23+
}

0 commit comments

Comments
 (0)