Skip to content

Commit 038b032

Browse files
committed
get basic module exports to work in API-graphs
1 parent df9efbe commit 038b032

4 files changed

Lines changed: 65 additions & 20 deletions

File tree

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

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -380,13 +380,14 @@ module API {
380380
// The `builtins` module should always be implicitly available
381381
name = "builtins"
382382
} or
383+
MkModuleExport(Module mod) or
383384
/** A use of an API member at the node `nd`. */
384385
MkUse(DataFlow::Node nd) { use(_, _, nd) } or
385386
MkDef(DataFlow::Node nd) { rhs(_, _, nd) }
386387

387388
class TUse = MkModuleImport or MkUse;
388389

389-
class TDef = MkDef;
390+
class TDef = MkDef or MkModuleExport;
390391

391392
/**
392393
* Holds if the dotted module name `sub` refers to the `member` member of `base`.
@@ -440,21 +441,19 @@ module API {
440441
)
441442
}
442443

444+
// TODO: Compare with JS, check that I'm not missing stuff
443445
/**
444446
* Holds if `rhs` is the right-hand side of a definition of a node that should have an
445447
* incoming edge from `base` labeled `lbl` in the API graph.
446448
*/
447449
cached
448450
predicate rhs(TApiNode base, Label::ApiLabel lbl, DataFlow::Node rhs) {
449-
/*
450-
* exists(string m, string prop | // TODO: Figure out module exports in Python
451-
* base = MkModuleExport(m) and
452-
* lbl = Label::member(prop) and
453-
* exports(m, prop, rhs)
454-
* )
455-
* or
456-
*/
457-
451+
exists(Module mod, string prop |
452+
base = MkModuleExport(mod) and
453+
exports(mod, prop, rhs) and
454+
lbl = Label::member(prop)
455+
)
456+
or
458457
exists(DataFlow::Node def, DataFlow::LocalSourceNode pred |
459458
rhs(base, def) and pred = trackDefNode(def)
460459
|
@@ -557,13 +556,23 @@ module API {
557556
ref.asExpr() = fn.getInnerScope().getArg(i)
558557
)
559558
/*
560-
* or // TODO: Figure out self.
559+
* or // TODO: Figure out self. (and arg = -2, that might be a thing in python)
561560
* lbl = Label::receiver() and
562561
* ref = fn.getReceiver()
563562
*/
564563

565564
)
566565
or
566+
/*
567+
* or // TODO: Figure out classes.
568+
* exists(DataFlow::Node def, DataFlow::ClassNode cls, int i |
569+
* rhs(base, def) and cls = trackDefNode(def)
570+
* |
571+
* lbl = Label::parameter(i) and
572+
* ref = cls.getConstructor().getParameter(i)
573+
* )
574+
*/
575+
567576
// Built-ins, treated as members of the module `builtins`
568577
base = MkModuleImport("builtins") and
569578
lbl = Label::member(any(string name | ref = Builtins::likelyBuiltin(name)))
@@ -674,7 +683,8 @@ module API {
674683
*/
675684
cached
676685
predicate rhs(TApiNode nd, DataFlow::Node rhs) {
677-
// exists(string m | nd = MkModuleExport(m) | exports(m, rhs)) // TODO: Figure out module exported in Py.
686+
// TODO: There are no "default" exports in python, right? E.g. in `import foo`, `foo` cannot be a function.
687+
// exists(string m | nd = MkModuleExport(m) | exports(m, rhs))
678688
// or
679689
nd = MkDef(rhs)
680690
}
@@ -687,11 +697,13 @@ module API {
687697
/* There's an edge from the root node for each imported module. */
688698
exists(string m |
689699
pred = MkRoot() and
690-
lbl = Label::mod(m)
691-
|
692-
succ = MkModuleImport(m) and
700+
lbl = Label::mod(m) and
693701
// Only allow undotted names to count as base modules.
694702
not m.matches("%.%")
703+
|
704+
succ = MkModuleImport(m)
705+
or
706+
succ = MkModuleExport(any(Module mod | mod.getName() = m and mod.isPackage()))
695707
)
696708
or
697709
/* Step from the dotted module name `foo.bar` to `foo.bar.baz` along an edge labeled `baz` */
@@ -706,10 +718,18 @@ module API {
706718
succ = MkUse(ref)
707719
)
708720
or
721+
exists(Module parentMod, Module childMod, string edge |
722+
pred = MkModuleExport(parentMod) and
723+
succ = MkModuleExport(childMod) and
724+
parentMod.getSubModule(edge) = childMod and // TODO: __init__.py shows up here, handle those in some other way. See e.g. https://stackoverflow.com/questions/38927979/default-export-in-python-3
725+
lbl = Label::member(edge)
726+
)
727+
or
709728
exists(DataFlow::Node rhs |
710729
rhs(pred, lbl, rhs) and
711730
succ = MkDef(rhs)
712731
)
732+
// TODO: Compare with JS, check that I'm not missing stuff
713733
}
714734

715735
/**
@@ -737,12 +757,21 @@ module API {
737757
private import semmle.python.dataflow.new.internal.ImportStar
738758

739759
newtype TLabel =
740-
MkLabelModule(string mod) { exists(Impl::MkModuleImport(mod)) } or
760+
MkLabelModule(string mod) {
761+
(
762+
exists(Impl::MkModuleImport(mod))
763+
or
764+
exists(Module m | exists(Impl::MkModuleExport(m)) | mod = m.getName())
765+
) and
766+
not mod.matches("%.%") // only top level modules count as base modules
767+
} or
741768
MkLabelMember(string member) {
742769
member = any(DataFlow::AttrRef pr).getAttributeName() or
743770
exists(Builtins::likelyBuiltin(member)) or
744771
ImportStar::namePossiblyDefinedInImportStar(_, member, _) or
745-
Impl::prefix_member(_, member, _)
772+
Impl::prefix_member(_, member, _) or
773+
exists(any(Module mod).getSubModule(member)) or
774+
exports(_, member, _)
746775
} or
747776
MkLabelUnknownMember() or
748777
MkLabelParameter(int i) {
@@ -850,3 +879,14 @@ module API {
850879
LabelAwait await() { any() }
851880
}
852881
}
882+
883+
/** Holds if module `mod` exports `rhs` under the name `prop`. */
884+
private predicate exports(Module mod, string prop, DataFlow::Node rhs) {
885+
exists(Assign assign |
886+
assign = mod.getAStmt() and
887+
rhs.asExpr() = assign.getValue() and
888+
exists(Variable v | assign.defines(v) and prop = v.getId())
889+
)
890+
// TODO: Re-exports.
891+
// TODO: use this predicate with __init__.py?
892+
}

python/ql/test/library-tests/ApiGraphs/def.ql

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ class ApiDefTest extends InlineExpectationsTest {
1515
// from the inline tests.
1616
not n instanceof DataFlow::ModuleVariableNode and
1717
exists(l.getFile().getRelativePath()) and
18-
n.getLocation().getFile().getBaseName().matches("def%.py")
18+
exists(File f | f = n.getLocation().getFile() |
19+
f.getBaseName().matches("def%.py")
20+
or
21+
f.getAbsolutePath().matches("%/mypkg/%")
22+
)
1923
}
2024

2125
override predicate hasActualResult(Location location, string element, string tag, string value) {
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
foo = 42
1+
# TODO: Shortcurcuit the __init__ package.
2+
foo = 42 #$ def=moduleImport("mypkg").getMember("__init__").getMember("foo")
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
pass
1+
value = 3 #$ def=moduleImport("mypkg").getMember("foo").getMember("value")

0 commit comments

Comments
 (0)