Merge pull request #2431 from tausbn/python-cyclic-import-future-annotations

Python: Account for non-evaluation of annotations in cyclic imports.
This commit is contained in:
Rasmus Wriedt Larsen
2019-11-27 13:31:53 +01:00
committed by GitHub
7 changed files with 67 additions and 1 deletions

View File

@@ -60,7 +60,8 @@ predicate import_time_module_use(ModuleValue m, ModuleValue enclosing, Expr use,
exists(Expr mod |
use.getEnclosingModule() = enclosing.getScope() and
not use.getScope+() instanceof Function and
mod.pointsTo(m)
mod.pointsTo(m) and
not is_annotation_with_from_future_import_annotations(use)
|
// either 'M.foo'
use.(Attribute).getObject() = mod and use.(Attribute).getName() = attr
@@ -70,6 +71,30 @@ predicate import_time_module_use(ModuleValue m, ModuleValue enclosing, Expr use,
)
}
/**
* Holds if `use` appears inside an annotation.
*/
predicate is_used_in_annotation(Expr use) {
exists(FunctionExpr f |
f.getReturns().getASubExpression*() = use or
f.getArgs().getAnAnnotation().getASubExpression*() = use
)
or
exists(AnnAssign a | a.getAnnotation().getASubExpression*() = use)
}
/**
* Holds if `use` appears as a subexpression of an annotation, _and_ if the
* postponed evaluation of annotations presented in PEP 563 is in effect.
* See https://www.python.org/dev/peps/pep-0563/
*/
predicate is_annotation_with_from_future_import_annotations(Expr use) {
exists(ImportMember i | i.getScope() = use.getEnclosingModule() |
i.getModule().pointsTo().getName() = "__future__" and i.getName() = "annotations"
) and
is_used_in_annotation(use)
}
/**
* Whether importing module 'first' before importing module 'other' will fail at runtime, due to an
* AttributeError at 'use' (in module 'other') caused by 'first.attr' not being defined as its definition can

View File

@@ -0,0 +1,2 @@
| 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 |
| 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 |

View File

@@ -0,0 +1 @@
Imports/ModuleLevelCyclicImport.ql

View File

@@ -0,0 +1,10 @@
from __future__ import annotations
import dataclasses
import typing
import module2
@dataclasses.dataclass()
class Foo:
bars: typing.List[module2.Bar]

View File

@@ -0,0 +1,11 @@
from __future__ import annotations
import dataclasses
import typing
import module1
@dataclasses.dataclass()
class Bar:
def is_in_foo(self, foo: module1.Foo):
return self in foo.bars

View File

@@ -0,0 +1,8 @@
import dataclasses
import typing
import module4
@dataclasses.dataclass()
class Foo:
bars: typing.List[module4.Bar]

View File

@@ -0,0 +1,9 @@
import dataclasses
import typing
import module3
@dataclasses.dataclass()
class Bar:
def is_in_foo(self, foo: module3.Foo):
return self in foo.bars