Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e4c3544

Browse files
committed
Python: Add support for from foo.bar import baz
This turned out to be fairly simple. Given an import such as ```python from foo.bar.baz import quux ``` we create an API-graph node for each valid dotted prefix of `foo.bar.baz`, i.e. `foo`, `foo.bar`, and `foo.bar.baz`. For these, we then insert nodes in the API graph, such that `foo` steps to `foo.bar` along an edge labeled `bar`, etc. Finally, we only allow undotted names to hang off of the API-graph root. Thus, `foo` will have a `moduleImport` edge off of the root, and a `getMember` edge for `bar` (which in turn has a `getMember` edge for `baz`). Relative imports are explicitly ignored. Finally, this commit also adds inline tests for a variety of ways of importing modules, including a copy of the "import-helper" tests (with a few modifications to allow a single annotation per line, as these get rather long quickly!).
1 parent cd7b013 commit e4c3544

16 files changed

Lines changed: 168 additions & 4 deletions

File tree

python/ql/src/semmle/python/ApiGraphs.qll

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,14 +230,52 @@ module API {
230230
/** The root of the API graph. */
231231
MkRoot() or
232232
/** An abstract representative for imports of the module called `name`. */
233-
MkModuleImport(string name) { imports(_, name) } or
233+
MkModuleImport(string name) {
234+
imports(_, name) or name = any(ImportExpr e | not e.isRelative()).getAnImportedModuleName()
235+
} or
234236
/** A use of an API member at the node `nd`. */
235237
MkUse(DataFlow::Node nd) { use(_, _, nd) }
236238

237239
class TUse = MkModuleImport or MkUse;
238240

241+
/**
242+
* Holds if the dotted module name `sub` refers to the `member` member of `base`.
243+
*
244+
* For instance, `prefix_member("foo.bar", "baz", "foo.bar.baz")` would hold.
245+
*/
246+
private predicate prefix_member(TApiNode base, string member, TApiNode sub) {
247+
exists(string base_str, string sub_str |
248+
base = MkModuleImport(base_str) and
249+
sub = MkModuleImport(sub_str)
250+
|
251+
base_str + "." + member = sub_str and
252+
not member.matches("%.%")
253+
)
254+
}
255+
239256
/** Holds if `imp` is an import of a module named `name` */
240-
private predicate imports(DataFlow::Node imp, string name) { imp = DataFlow::importNode(name) }
257+
private predicate imports(DataFlow::Node import_node, string name) {
258+
exists(Variable var, Import imp, Alias alias |
259+
alias = imp.getAName() and
260+
alias.getAsname() = var.getAStore() and
261+
(
262+
name = alias.getValue().(ImportMember).getImportedModuleName()
263+
or
264+
name = alias.getValue().(ImportExpr).getImportedModuleName() and
265+
not alias.getValue().(ImportExpr).isRelative()
266+
) and
267+
import_node.asExpr() = alias.getValue()
268+
)
269+
or
270+
exists(ImportExpr imp_expr |
271+
not imp_expr.isRelative() and
272+
imp_expr.getName() = name and
273+
import_node.asCfgNode().getNode() = imp_expr and
274+
// in `import foo.bar` we DON'T want to give a result for `importNode("foo.bar")`,
275+
// only for `importNode("foo")`. We exclude those cases with the following clause.
276+
not exists(Import imp | imp.getAName().getValue() = imp_expr)
277+
)
278+
}
241279

242280
/**
243281
* Holds if `ref` is a use of a node that should have an incoming edge from `base` labeled
@@ -248,9 +286,11 @@ module API {
248286
exists(DataFlow::LocalSourceNode src, DataFlow::LocalSourceNode pred |
249287
use(base, src) and pred = trackUseNode(src)
250288
|
289+
// Reading an attribute on a node that is a use of `base`:
251290
lbl = Label::memberFromRef(ref) and
252291
ref = pred.getAnAttributeRead()
253292
or
293+
// Calling a node that is a use of `base`
254294
lbl = Label::return() and
255295
ref = pred.getAnInvocation()
256296
)
@@ -263,7 +303,7 @@ module API {
263303
predicate use(TApiNode nd, DataFlow::Node ref) {
264304
exists(string name |
265305
nd = MkModuleImport(name) and
266-
ref = DataFlow::importNode(name)
306+
imports(ref, name)
267307
)
268308
or
269309
nd = MkUse(ref)
@@ -310,7 +350,15 @@ module API {
310350
pred = MkRoot() and
311351
lbl = Label::mod(m)
312352
|
313-
succ = MkModuleImport(m)
353+
succ = MkModuleImport(m) and
354+
// Only allow undotted names to count as base modules.
355+
not m.matches("%.%")
356+
)
357+
or
358+
/* Step from the dotted module name `foo.bar` to `foo.bar.baz` along an edge labeled `baz` */
359+
exists(string member |
360+
prefix_member(pred, member, succ) and
361+
lbl = Label::member(member)
314362
)
315363
or
316364
/* Every node that is a use of an API component is itself added to the API graph. */
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
foo = 42
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pass
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pass
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
semmle-extractor-options: --lang=3
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import a1 #$ use=moduleImport("a1")
2+
3+
x = a1.blah1 #$ use=moduleImport("a1").getMember("blah1")
4+
5+
import a2 as m2 #$ use=moduleImport("a2")
6+
7+
x2 = m2.blah2 #$ use=moduleImport("a2").getMember("blah2")
8+
9+
import a3.b3 as m3 #$ use=moduleImport("a3").getMember("b3")
10+
11+
x3 = m3.blah3 #$ use=moduleImport("a3").getMember("b3").getMember("blah3")
12+
13+
from a4.b4 import c4 as m4 #$ use=moduleImport("a4").getMember("b4").getMember("c4")
14+
15+
x4 = m4.blah4 #$ use=moduleImport("a4").getMember("b4").getMember("c4").getMember("blah4")
16+
17+
import a.b.c.d #$ use=moduleImport("a")
18+
19+
ab = a.b #$ use=moduleImport("a").getMember("b")
20+
21+
abc = ab.c #$ use=moduleImport("a").getMember("b").getMember("c")
22+
23+
abcd = abc.d #$ use=moduleImport("a").getMember("b").getMember("c").getMember("d")
24+
25+
x5 = abcd() #$ use=moduleImport("a").getMember("b").getMember("c").getMember("d").getReturn()
26+
27+
y5 = x5.method() #$ use=moduleImport("a").getMember("b").getMember("c").getMember("d").getReturn().getMember("method").getReturn()
28+
29+
30+
# Relative imports. These are ignored
31+
32+
from .foo import bar
33+
34+
from ..foobar import baz
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import mypkg #$ use=moduleImport("mypkg")
2+
print(mypkg.foo) #$ use=moduleImport("mypkg").getMember("foo") // 42
3+
try:
4+
print(mypkg.bar) #$ use=moduleImport("mypkg").getMember("bar")
5+
except AttributeError as e:
6+
print(e) # module 'mypkg' has no attribute 'bar'
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from mypkg import foo #$ use=moduleImport("mypkg").getMember("foo")
2+
from mypkg import bar #$ use=moduleImport("mypkg").getMember("bar")
3+
print(foo) #$ use=moduleImport("mypkg").getMember("foo")
4+
print(bar) #$ use=moduleImport("mypkg").getMember("bar")
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import mypkg.foo #$ use=moduleImport("mypkg")
2+
import mypkg.bar #$ use=moduleImport("mypkg")
3+
print(mypkg.foo) #$ use=moduleImport("mypkg").getMember("foo") // <module 'mypkg.foo' ...
4+
print(mypkg.bar) #$ use=moduleImport("mypkg").getMember("bar") // <module 'mypkg.bar' ...
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import mypkg.foo as _foo #$ use=moduleImport("mypkg").getMember("foo")
2+
import mypkg.bar as _bar #$ use=moduleImport("mypkg").getMember("bar")
3+
print(_foo) #$ use=moduleImport("mypkg").getMember("foo") // <module 'mypkg.bar' ...
4+
print(_bar) #$ use=moduleImport("mypkg").getMember("bar") // <module 'mypkg.bar' ...

0 commit comments

Comments
 (0)