Skip to content

Commit 036e0f7

Browse files
committed
Python: Account for non-evaluation of annotations in cyclic imports.
Should fix #2426. Essentially, we disregard expressions used inside annotations, if these annotations occur in a file that has `from __future__ import annotations`, as this prevents the annotations from being evaluated.
1 parent 77c869f commit 036e0f7

File tree

7 files changed

+67
-1
lines changed

7 files changed

+67
-1
lines changed

python/ql/src/Imports/Cyclic.qll

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ predicate import_time_module_use(ModuleValue m, ModuleValue enclosing, Expr use,
6060
exists(Expr mod |
6161
use.getEnclosingModule() = enclosing.getScope() and
6262
not use.getScope+() instanceof Function and
63-
mod.pointsTo(m)
63+
mod.pointsTo(m) and
64+
not is_annotation_with_from_future_import_annotations(use)
6465
|
6566
// either 'M.foo'
6667
use.(Attribute).getObject() = mod and use.(Attribute).getName() = attr
@@ -70,6 +71,30 @@ predicate import_time_module_use(ModuleValue m, ModuleValue enclosing, Expr use,
7071
)
7172
}
7273

74+
/**
75+
* Holds if `use` appears inside an annotation.
76+
*/
77+
predicate is_used_in_annotation(Expr use) {
78+
exists(FunctionExpr f |
79+
f.getReturns().getASubExpression*() = use or
80+
f.getArgs().getAnAnnotation().getASubExpression*() = use
81+
)
82+
or
83+
exists(AnnAssign a | a.getAnnotation().getASubExpression*() = use)
84+
}
85+
86+
/**
87+
* Holds if `use` appears as a subexpression of an annotation, _and_ if the
88+
* postponed evaluation of annotations presented in PEP 563 is in effect.
89+
* See https://www.python.org/dev/peps/pep-0563/
90+
*/
91+
predicate is_annotation_with_from_future_import_annotations(Expr use) {
92+
exists(ImportMember i | i.getScope() = use.getEnclosingModule() |
93+
i.getModule().pointsTo().getName() = "__future__" and i.getName() = "annotations"
94+
) and
95+
is_used_in_annotation(use)
96+
}
97+
7398
/**
7499
* Whether importing module 'first' before importing module 'other' will fail at runtime, due to an
75100
* AttributeError at 'use' (in module 'other') caused by 'first.attr' not being defined as its definition can
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
| module3.py:8:23:8:33 | Attribute | 'Bar' may not be defined if module $@ is imported before module $@, as the $@ of Bar occurs after the cyclic $@ of module3. | module4.py:0:0:0:0 | Module module4 | module4 | module3.py:0:0:0:0 | Module module3 | module3 | module4.py:7:7:7:9 | ControlFlowNode for Bar | definition | module4.py:4:1:4:14 | Import | import |
2+
| module4.py:8:30:8:40 | Attribute | 'Foo' may not be defined if module $@ is imported before module $@, as the $@ of Foo occurs after the cyclic $@ of module4. | module3.py:0:0:0:0 | Module module3 | module3 | module4.py:0:0:0:0 | Module module4 | module4 | module3.py:7:7:7:9 | ControlFlowNode for Foo | definition | module3.py:4:1:4:14 | Import | import |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Imports/ModuleLevelCyclicImport.ql
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from __future__ import annotations
2+
3+
import dataclasses
4+
import typing
5+
6+
import module2
7+
8+
@dataclasses.dataclass()
9+
class Foo:
10+
bars: typing.List[module2.Bar]
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from __future__ import annotations
2+
3+
import dataclasses
4+
import typing
5+
6+
import module1
7+
8+
@dataclasses.dataclass()
9+
class Bar:
10+
def is_in_foo(self, foo: module1.Foo):
11+
return self in foo.bars
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import dataclasses
2+
import typing
3+
4+
import module4
5+
6+
@dataclasses.dataclass()
7+
class Foo:
8+
bars: typing.List[module4.Bar]
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import dataclasses
2+
import typing
3+
4+
import module3
5+
6+
@dataclasses.dataclass()
7+
class Bar:
8+
def is_in_foo(self, foo: module3.Foo):
9+
return self in foo.bars

0 commit comments

Comments
 (0)