Skip to content

Commit db78e3a

Browse files
authored
Merge pull request github#6274 from tausbn/python-api-graphs-import-star
Python: Support `import *` in API graphs
2 parents b45743b + 51c0cee commit db78e3a

File tree

4 files changed

+107
-13
lines changed

4 files changed

+107
-13
lines changed

python/ql/lib/semmle/python/ApiGraphs.qll

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -424,13 +424,8 @@ module API {
424424
* a value in the module `m`.
425425
*/
426426
private predicate possible_builtin_defined_in_module(string name, Module m) {
427-
exists(NameNode n |
428-
not exists(LocalVariable v | n.defines(v)) and
429-
n.isStore() and
430-
name = n.getId() and
431-
name = getBuiltInName() and
432-
m = n.getEnclosingModule()
433-
)
427+
global_name_defined_in_module(name, m) and
428+
name = getBuiltInName()
434429
}
435430

436431
/**
@@ -445,6 +440,51 @@ module API {
445440
m = n.getEnclosingModule()
446441
}
447442

443+
/**
444+
* Holds if `n` is an access of a variable called `name` (which is _not_ the name of a
445+
* built-in, and which is _not_ a global defined in the enclosing module) inside the scope `s`.
446+
*/
447+
private predicate name_possibly_defined_in_import_star(NameNode n, string name, Scope s) {
448+
n.isLoad() and
449+
name = n.getId() and
450+
// Not already defined in an enclosing scope.
451+
not exists(LocalVariable v |
452+
v.getId() = name and v.getScope() = n.getScope().getEnclosingScope*()
453+
) and
454+
not name = getBuiltInName() and
455+
s = n.getScope().getEnclosingScope*() and
456+
exists(potential_import_star_base(s)) and
457+
not global_name_defined_in_module(name, n.getEnclosingModule())
458+
}
459+
460+
/** Holds if a global variable called `name` is assigned a value in the module `m`. */
461+
private predicate global_name_defined_in_module(string name, Module m) {
462+
exists(NameNode n |
463+
not exists(LocalVariable v | n.defines(v)) and
464+
n.isStore() and
465+
name = n.getId() and
466+
m = n.getEnclosingModule()
467+
)
468+
}
469+
470+
/**
471+
* Gets the API graph node for all modules imported with `from ... import *` inside the scope `s`.
472+
*
473+
* For example, given
474+
*
475+
* `from foo.bar import *`
476+
*
477+
* this would be the API graph node with the path
478+
*
479+
* `moduleImport("foo").getMember("bar")`
480+
*/
481+
private TApiNode potential_import_star_base(Scope s) {
482+
exists(DataFlow::Node ref |
483+
ref.asCfgNode() = any(ImportStarNode n | n.getScope() = s).getModule() and
484+
use(result, ref)
485+
)
486+
}
487+
448488
/**
449489
* Holds if `ref` is a use of a node that should have an incoming edge from `base` labeled
450490
* `lbl` in the API graph.
@@ -487,6 +527,15 @@ module API {
487527
// Built-ins, treated as members of the module `builtins`
488528
base = MkModuleImport("builtins") and
489529
lbl = Label::member(any(string name | ref = likely_builtin(name)))
530+
or
531+
// Unknown variables that may belong to a module imported with `import *`
532+
exists(Scope s |
533+
base = potential_import_star_base(s) and
534+
lbl =
535+
Label::member(any(string name |
536+
name_possibly_defined_in_import_star(ref.asCfgNode(), name, s)
537+
))
538+
)
490539
}
491540

492541
/**

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

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,6 @@ def f():
7474
change_foo()
7575
sink(foo) #$ use=moduleImport("danger").getMember("SOURCE")
7676

77-
# Star imports
78-
79-
from unknown import * #$ use=moduleImport("unknown")
80-
81-
hello() #$ MISSING: use=moduleImport("unknown").getMember("hello").getReturn()
82-
8377

8478
# Subclasses
8579

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Star imports
2+
3+
from unknown import * #$ use=moduleImport("unknown")
4+
5+
# Currently missing, as we do not consider `hello` to be a `LocalSourceNode`, since it has flow
6+
# going into it from its corresponding `GlobalSsaVariable`.
7+
hello() #$ MISSING: use=moduleImport("unknown").getMember("hello").getReturn()
8+
9+
# We don't want our analysis to think that either `non_module_member` or `outer_bar` can
10+
# come from `from unknown import *`
11+
non_module_member
12+
13+
outer_bar = 5
14+
outer_bar
15+
16+
def foo():
17+
world() #$ use=moduleImport("unknown").getMember("world").getReturn()
18+
bar = 5
19+
bar
20+
non_module_member
21+
print(bar) #$ use=moduleImport("builtins").getMember("print").getReturn()
22+
23+
def quux():
24+
global non_module_member
25+
non_module_member = 5
26+
27+
def func1():
28+
var() #$ use=moduleImport("unknown").getMember("var").getReturn()
29+
def func2():
30+
var = "FOO"
31+
32+
def func3():
33+
var2 = print #$ use=moduleImport("builtins").getMember("print")
34+
def func4():
35+
var2() #$ MISSING: use=moduleImport("builtins").getMember("print").getReturn()
36+
func4()
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Star imports in local scope
2+
3+
hello2()
4+
5+
def foo():
6+
from unknown2 import * #$ use=moduleImport("unknown2")
7+
world2() #$ use=moduleImport("unknown2").getMember("world2").getReturn()
8+
bar2 = 5
9+
bar2
10+
non_module_member2
11+
print(bar2) #$ use=moduleImport("builtins").getMember("print").getReturn()
12+
13+
def quux2():
14+
global non_module_member2
15+
non_module_member2 = 5

0 commit comments

Comments
 (0)