Skip to content

Commit 3f45d86

Browse files
authored
Merge pull request github#2047 from taus-semmle/python-modernise-and-fix-cyclic-import-fp
Python: modernise and fix cyclic import false positive.
2 parents fbb7747 + c5c84a1 commit 3f45d86

File tree

6 files changed

+65
-52
lines changed

6 files changed

+65
-52
lines changed

python/ql/src/Imports/Cyclic.qll

Lines changed: 38 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,50 @@
11
import python
22

3-
predicate is_import_time(Stmt s) {
4-
not s.getScope+() instanceof Function
5-
}
3+
predicate is_import_time(Stmt s) { not s.getScope+() instanceof Function }
64

7-
PythonModuleObject module_imported_by(PythonModuleObject m) {
5+
ModuleValue module_imported_by(ModuleValue m) {
86
exists(Stmt imp |
97
result = stmt_imports(imp) and
10-
imp.getEnclosingModule() = m.getModule() and
8+
imp.getEnclosingModule() = m.getScope() and
119
// Import must reach exit to be part of a cycle
1210
imp.getAnEntryNode().getBasicBlock().reachesExit()
1311
)
1412
}
1513

1614
/** Is there a circular import of 'm1' beginning with 'm2'? */
17-
predicate circular_import(PythonModuleObject m1, PythonModuleObject m2) {
15+
predicate circular_import(ModuleValue m1, ModuleValue m2) {
1816
m1 != m2 and
19-
m2 = module_imported_by(m1) and m1 = module_imported_by+(m2)
17+
m2 = module_imported_by(m1) and
18+
m1 = module_imported_by+(m2)
2019
}
2120

22-
ModuleObject stmt_imports(ImportingStmt s) {
23-
exists(string name |
24-
result.importedAs(name) and not name = "__main__" |
25-
name = s.getAnImportedModuleName()
21+
ModuleValue stmt_imports(ImportingStmt s) {
22+
exists(string name | result.importedAs(name) and not name = "__main__" |
23+
name = s.getAnImportedModuleName() and
24+
s.getASubExpression().pointsTo(result)
2625
)
2726
}
2827

29-
predicate import_time_imported_module(PythonModuleObject m1, PythonModuleObject m2, Stmt imp) {
30-
imp.getEnclosingModule() = m1.getModule() and
28+
predicate import_time_imported_module(ModuleValue m1, ModuleValue m2, Stmt imp) {
29+
imp.getEnclosingModule() = m1.getScope() and
3130
is_import_time(imp) and
3231
m2 = stmt_imports(imp)
3332
}
3433

3534
/** Is there a cyclic import of 'm1' beginning with an import 'm2' at 'imp' where all the imports are top-level? */
36-
predicate import_time_circular_import(PythonModuleObject m1, PythonModuleObject m2, Stmt imp) {
35+
predicate import_time_circular_import(ModuleValue m1, ModuleValue m2, Stmt imp) {
3736
m1 != m2 and
38-
import_time_imported_module(m1, m2, imp) and
37+
import_time_imported_module(m1, m2, imp) and
3938
import_time_transitive_import(m2, _, m1)
4039
}
4140

42-
predicate import_time_transitive_import(PythonModuleObject base, Stmt imp, PythonModuleObject last) {
41+
predicate import_time_transitive_import(ModuleValue base, Stmt imp, ModuleValue last) {
4342
last != base and
4443
(
4544
import_time_imported_module(base, last, imp)
4645
or
47-
exists(PythonModuleObject mid |
48-
import_time_transitive_import(base, imp, mid) and
46+
exists(ModuleValue mid |
47+
import_time_transitive_import(base, imp, mid) and
4948
import_time_imported_module(mid, last, _)
5049
)
5150
) and
@@ -56,12 +55,12 @@ predicate import_time_transitive_import(PythonModuleObject base, Stmt imp, Pytho
5655
/**
5756
* Returns import-time usages of module 'm' in module 'enclosing'
5857
*/
59-
predicate import_time_module_use(PythonModuleObject m, PythonModuleObject enclosing, Expr use, string attr) {
60-
exists(Expr mod |
61-
use.getEnclosingModule() = enclosing.getModule() and
62-
not use.getScope+() instanceof Function
63-
and mod.refersTo(m)
64-
|
58+
predicate import_time_module_use(ModuleValue m, ModuleValue enclosing, Expr use, string attr) {
59+
exists(Expr mod |
60+
use.getEnclosingModule() = enclosing.getScope() and
61+
not use.getScope+() instanceof Function and
62+
mod.pointsTo(m)
63+
|
6564
// either 'M.foo'
6665
use.(Attribute).getObject() = mod and use.(Attribute).getName() = attr
6766
or
@@ -70,20 +69,24 @@ predicate import_time_module_use(PythonModuleObject m, PythonModuleObject enclos
7069
)
7170
}
7271

73-
/** Whether importing module 'first' before importing module 'other' will fail at runtime, due to an
74-
AttributeError at 'use' (in module 'other') caused by 'first.attr' not being defined as its definition can
75-
occur after the import 'other' in 'first'.
76-
*/
77-
predicate failing_import_due_to_cycle(PythonModuleObject first, PythonModuleObject other, Stmt imp,
78-
ControlFlowNode defn, Expr use, string attr) {
72+
/**
73+
* Whether importing module 'first' before importing module 'other' will fail at runtime, due to an
74+
* AttributeError at 'use' (in module 'other') caused by 'first.attr' not being defined as its definition can
75+
* occur after the import 'other' in 'first'.
76+
*/
77+
predicate failing_import_due_to_cycle(
78+
ModuleValue first, ModuleValue other, Stmt imp, ControlFlowNode defn, Expr use, string attr
79+
) {
7980
import_time_imported_module(other, first, _) and
8081
import_time_transitive_import(first, imp, other) and
8182
import_time_module_use(first, other, use, attr) and
82-
exists(ImportTimeScope n, SsaVariable v |
83+
exists(ImportTimeScope n, SsaVariable v |
8384
defn = v.getDefinition() and
84-
n = first.getModule() and v.getVariable().getScope() = n and v.getId() = attr |
85+
n = first.getScope() and
86+
v.getVariable().getScope() = n and
87+
v.getId() = attr
88+
|
8589
not defn.strictlyDominates(imp.getAnEntryNode())
86-
)
87-
and not exists(If i | i.isNameEqMain() and i.contains(use))
90+
) and
91+
not exists(If i | i.isNameEqMain() and i.contains(use))
8892
}
89-

python/ql/src/Imports/CyclicImport.ql

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,13 @@
1414
import python
1515
import Cyclic
1616

17-
from PythonModuleObject m1, PythonModuleObject m2, Stmt imp
18-
where
19-
imp.getEnclosingModule() = m1.getModule()
20-
and stmt_imports(imp) = m2
21-
and circular_import(m1, m2)
22-
and m1 != m2
23-
// this query finds all cyclic imports that are *not* flagged by ModuleLevelCyclicImport
24-
and not failing_import_due_to_cycle(m2, m1, _, _, _, _)
25-
and not exists(If i | i.isNameEqMain() and i.contains(imp))
17+
from ModuleValue m1, ModuleValue m2, Stmt imp
18+
where
19+
imp.getEnclosingModule() = m1.getScope() and
20+
stmt_imports(imp) = m2 and
21+
circular_import(m1, m2) and
22+
m1 != m2 and
23+
// this query finds all cyclic imports that are *not* flagged by ModuleLevelCyclicImport
24+
not failing_import_due_to_cycle(m2, m1, _, _, _, _) and
25+
not exists(If i | i.isNameEqMain() and i.contains(imp))
2626
select imp, "Import of module $@ begins an import cycle.", m2, m2.getName()
27-

python/ql/src/Imports/ModuleLevelCyclicImport.ql

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,10 @@ import Cyclic
2121
// 3. 'foo' is defined in M after the import in M which completes the cycle.
2222
// then if we import the 'used' module, we will reach the cyclic import, start importing the 'using'
2323
// module, hit the 'use', and then crash due to the imported symbol not having been defined yet
24-
25-
from PythonModuleObject m1, Stmt imp, PythonModuleObject m2, string attr, Expr use, ControlFlowNode defn
24+
from ModuleValue m1, Stmt imp, ModuleValue m2, string attr, Expr use, ControlFlowNode defn
2625
where failing_import_due_to_cycle(m1, m2, imp, defn, use, attr)
27-
select use, "'" + attr + "' may not be defined if module $@ is imported before module $@, " +
28-
"as the $@ of " + attr + " occurs after the cyclic $@ of " + m2.getName() + ".",
29-
m1, m1.getName(), m2, m2.getName(), defn, "definition", imp, "import"
30-
31-
26+
select use,
27+
"'" + attr + "' may not be defined if module $@ is imported before module $@, as the $@ of " +
28+
attr + " occurs after the cyclic $@ of " + m2.getName() + ".",
29+
// Arguments for the placeholders in the above message:
30+
m1, m1.getName(), m2, m2.getName(), defn, "definition", imp, "import"

python/ql/src/semmle/python/objects/ObjectAPI.qll

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ class ModuleValue extends Value {
133133
result = this.(PythonModuleObjectInternal).getSourceModule().getFile()
134134
}
135135

136+
/** Whether this module is imported by 'import name'. For example on a linux system,
137+
* the module 'posixpath' is imported as 'os.path' or as 'posixpath' */
138+
predicate importedAs(string name) {
139+
PointsToInternal::module_imported_as(this, name)
140+
}
141+
136142
}
137143

138144
module Module {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
from typing import TYPE_CHECKING
12

23
if False:
34
import module1
45

6+
if TYPE_CHECKING:
7+
import module1
8+
59
x = 1
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
TYPE_CHECKING = False
2+

0 commit comments

Comments
 (0)