Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix crashes and fails in forward references #3952

Merged
merged 43 commits into from
Sep 27, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
45e5931
Add basic tests, more details will be added when they will not crash
ilevkivskyi Aug 31, 2017
cb4caa5
Correct tests
ilevkivskyi Sep 1, 2017
1cdc980
Implement ForwardRef type, wrap UnboundType, pass SecondPass to third…
ilevkivskyi Sep 11, 2017
260ef02
Add ForwardRefRemover
ilevkivskyi Sep 11, 2017
a58a217
Add elimination patches
ilevkivskyi Sep 11, 2017
950a022
Fix replacement logic; fix newtype error formatting
ilevkivskyi Sep 11, 2017
411b24d
Fix third pass (need to go deeper)
ilevkivskyi Sep 11, 2017
b9b8528
Implement syntethic replacer
ilevkivskyi Sep 11, 2017
48d6de4
Need to go deeper (as usual)
ilevkivskyi Sep 11, 2017
ec45441
Fix postponed fallback join
ilevkivskyi Sep 11, 2017
ac32ed4
Simplify some code and add annotations
ilevkivskyi Sep 11, 2017
3fb3019
Simplify traversal logic; add loads of tests
ilevkivskyi Sep 12, 2017
f9b1320
Take care about one more special case; add few tests and dcostrings
ilevkivskyi Sep 12, 2017
cf014b8
Unify visitors
ilevkivskyi Sep 12, 2017
665236b
Add some more comments and docstrings
ilevkivskyi Sep 12, 2017
9a318aa
Add recursive type warnings
ilevkivskyi Sep 12, 2017
757fbd9
Fix lint
ilevkivskyi Sep 12, 2017
4502ce2
Also clean-up bases; add more tests and allow some previously skipped
ilevkivskyi Sep 13, 2017
3b39d40
One more TypedDict test
ilevkivskyi Sep 13, 2017
c8b28fe
Add another simple self-referrential NamedTuple test
ilevkivskyi Sep 13, 2017
9f92b0f
Fix type_override; add tests for recursive aliases; fix Callable TODO…
Sep 13, 2017
9779103
Merge branch 'master' into fix-synthetic-crashes
ilevkivskyi Sep 14, 2017
b914bdb
Merge remote-tracking branch 'upstream/master' into fix-synthetic-cra…
ilevkivskyi Sep 19, 2017
3568fdb
Skip the whole ForwardRef dance in unchecked functions
ilevkivskyi Sep 19, 2017
54d9331
Simplify test
ilevkivskyi Sep 19, 2017
b9ddacc
Fix self-check
ilevkivskyi Sep 19, 2017
5bfe9ca
Fix cross-file forward references (+test)
ilevkivskyi Sep 19, 2017
a2912e9
More tests
ilevkivskyi Sep 19, 2017
10c65b8
Merge branch 'master' into fix-synthetic-crashes
ilevkivskyi Sep 20, 2017
21dfbfe
Fix situation when recursive namedtuple appears directly in base clas…
ilevkivskyi Sep 20, 2017
f2ddbcd
Merge branch 'fix-synthetic-crashes' of https://github.com/ilevkivsky…
ilevkivskyi Sep 20, 2017
03597ee
Clean-up PR: Remove unnecesary imports, outdated comment, unnecessary…
ilevkivskyi Sep 20, 2017
649ef32
Add tests for generic classes, enums, with statements and for statements
ilevkivskyi Sep 20, 2017
83f8907
Add processing for for and with statements (+more tests)
ilevkivskyi Sep 20, 2017
13c7176
Add support for generic types with forward references
ilevkivskyi Sep 20, 2017
79b10d6
Prohibit forward refs to type vars and subscripted forward refs to al…
ilevkivskyi Sep 21, 2017
321a809
Refactor code to avoid passing semantic analyzer to type analyzer, on…
ilevkivskyi Sep 21, 2017
076c909
Address the rest of the review comments
ilevkivskyi Sep 22, 2017
c1a63ec
Improve two tests
ilevkivskyi Sep 22, 2017
97e6f47
Add one more test as suggested in #3990
ilevkivskyi Sep 23, 2017
8f52654
Address latest review comments
ilevkivskyi Sep 26, 2017
6edd078
Improve tests; Fix one more crash on NewType MRO
ilevkivskyi Sep 27, 2017
514b8bd
Fix formatting in tests
ilevkivskyi Sep 27, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 49 additions & 38 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -4212,9 +4212,11 @@ def visit_decorator(self, dec: Decorator) -> None:
dec.var.type = sig

def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
"""Traverse the actual assignment statement and synthetic types
"""Traverse the assignment statement.

This includes the actual assignment and synthetic types
resulted from this assignment (if any). Currently this includes
NewType, TypedDict, and NamedTuple.
NewType, TypedDict, NamedTuple, and TypeVar.
"""
self.analyze(s.type, s)
if isinstance(s.rvalue, IndexExpr) and isinstance(s.rvalue.analyzed, TypeAliasExpr):
Expand All @@ -4240,6 +4242,9 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
self.accept(sym.node)
if isinstance(sym.node, Var):
self.analyze(sym.node.type, sym.node)
# We need to pay additional attention to assignments that define a type alias.
# The resulting type is also stored in the 'type_override' attribute of
# the corresponding SymbolTableNode.
if isinstance(s.lvalues[0], RefExpr) and isinstance(s.lvalues[0].node, Var):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unclear what this is for. Please add a comment.

self.analyze(s.lvalues[0].node.type, s.lvalues[0].node)
if isinstance(s.lvalues[0], NameExpr):
Expand Down Expand Up @@ -4347,8 +4352,8 @@ def analyze(self, type: Optional[Type], node: Union[Node, SymbolTableNode],
if indicator.get('forward') or indicator.get('synthetic'):
def patch() -> None:
self.perform_transform(node,
lambda tp: tp.accept(TypeReplacer(self.fail,
node, warn)))
lambda tp: tp.accept(ForwardReferenceResolver(self.fail,
node, warn)))
self.patches.append(patch)

def analyze_types(self, types: List[Type], node: Node) -> None:
Expand All @@ -4368,8 +4373,8 @@ def analyze_types(self, types: List[Type], node: Node) -> None:
if indicator.get('forward') or indicator.get('synthetic'):
def patch() -> None:
self.perform_transform(node,
lambda tp: tp.accept(TypeReplacer(self.fail,
node, warn=False)))
lambda tp: tp.accept(ForwardReferenceResolver(self.fail,
node, warn=False)))
self.patches.append(patch)

def check_for_omitted_generics(self, typ: Type) -> None:
Expand Down Expand Up @@ -4798,9 +4803,17 @@ def visit_any(self, t: AnyType) -> Type:
return t


class TypeReplacer(TypeTranslator):
"""This is very similar TypeTranslator but tracks visited nodes to avoid
class ForwardReferenceResolver(TypeTranslator):
"""Visitor to replace previously detected forward reference to synthetic types.

This is similar to TypeTranslator but tracks visited nodes to avoid
infinite recursion on potentially circular (self- or mutually-referential) types.
This visitor:
* Fixes forward references by unwrapping the linked type.
* Generates errors for unsupported type recursion and breaks recursion by resolving
recursive back references to Any types.
* Replaces instance types generated from unanalyzed NamedTuple and TypedDict class syntax
found in first pass with analyzed TupleType and TypedDictType.
"""
def __init__(self, fail: Callable[[str, Context], None],
start: Union[Node, SymbolTableNode], warn: bool) -> None:
Expand All @@ -4809,66 +4822,64 @@ def __init__(self, fail: Callable[[str, Context], None],
self.start = start
self.warn = warn

def warn_recursion(self) -> None:
if self.warn:
assert isinstance(self.start, Node), "Internal error: invalid error context"
self.fail('Recursive types not fully supported yet,'
' nested types replaced with "Any"', self.start)

def check(self, t: Type) -> bool:
def check_recursion(self, t: Type) -> bool:
if any(t is s for s in self.seen):
self.warn_recursion()
if self.warn:
assert isinstance(self.start, Node), "Internal error: invalid error context"
self.fail('Recursive types not fully supported yet,'
' nested types replaced with "Any"', self.start)
return True
self.seen.append(t)
return False

def visit_forwardref_type(self, t: ForwardRef) -> Type:
"""This visitor method tracks situations like this:

x: A # this type is not yet known and therefore wrapped in ForwardRef
# it's content is updated in ThirdPass, now we need to unwrap this type.
x: A # This type is not yet known and therefore wrapped in ForwardRef,
# its content is updated in ThirdPass, now we need to unwrap this type.
A = NewType('A', int)
"""
return t.link.accept(self)

def visit_instance(self, t: Instance, from_fallback: bool = False) -> Type:
"""This visitor method tracks situations like this:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document from_fallback.


x: A # when analyzing this type we will get an Instance from FirstPass
# now we need to update this to actual analyzed TupleType.
x: A # When analyzing this type we will get an Instance from FirstPass.
# Now we need to update this to actual analyzed TupleType.
class A(NamedTuple):
attr: str

If from_fallback is True, then we always return an Instance type. This is needed
since TupleType and TypedDictType fallbacks are always instances.
"""
info = t.type
# Special case, analyzed bases transformed the type into TupleType.
if info.tuple_type and not from_fallback:
items = [it.accept(self) for it in info.tuple_type.items]
info.tuple_type.items = items
return TupleType(items, Instance(info, []))
# Update forward Instance's to corresponding analyzed NamedTuple's.
# Update forward Instances to corresponding analyzed NamedTuples.
if info.replaced and info.replaced.tuple_type:
tp = info.replaced.tuple_type
if any((s is tp) or (s is t) for s in self.seen):
self.warn_recursion()
# The key idea is that when we return to the place where
# we already was, we break the cycle and put AnyType as a leaf.
if self.check_recursion(tp):
# The key idea is that when we recursively return to a type already traversed,
# then we break the cycle and put AnyType as a leaf.
return AnyType(TypeOfAny.from_error)
self.seen.append(t)
return tp.copy_modified(fallback=Instance(info.replaced, [])).accept(self)
# Same as above but for TypedDict's.
# Same as above but for TypedDicts.
if info.replaced and info.replaced.typeddict_type:
td = info.replaced.typeddict_type
if any((s is td) or (s is t) for s in self.seen):
self.warn_recursion()
if self.check_recursion(td):
# We also break the cycles for TypedDicts as explained above for NamedTuples.
return AnyType(TypeOfAny.from_error)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add comment that references the above comment about the same situation with named tuples.

self.seen.append(t)
return td.copy_modified(fallback=Instance(info.replaced, [])).accept(self)
if self.check(t):
if self.check_recursion(t):
# We also need to break a potential cycle with normal (non-synthetic) instance types.
return Instance(t.type, [AnyType(TypeOfAny.from_error)] * len(t.type.defn.type_vars))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add comment about forcibly breaking recursion here to avoid infinite recursion.

return super().visit_instance(t)

def visit_type_var(self, t: TypeVarType) -> Type:
if self.check(t):
if self.check_recursion(t):
return AnyType(TypeOfAny.from_error)
if t.upper_bound:
t.upper_bound = t.upper_bound.accept(self)
Expand All @@ -4877,7 +4888,7 @@ def visit_type_var(self, t: TypeVarType) -> Type:
return t

def visit_callable_type(self, t: CallableType) -> Type:
if self.check(t):
if self.check_recursion(t):
return AnyType(TypeOfAny.from_error)
arg_types = [tp.accept(self) for tp in t.arg_types]
ret_type = t.ret_type.accept(self)
Expand All @@ -4890,29 +4901,29 @@ def visit_callable_type(self, t: CallableType) -> Type:
return t.copy_modified(arg_types=arg_types, ret_type=ret_type, variables=variables)

def visit_overloaded(self, t: Overloaded) -> Type:
if self.check(t):
if self.check_recursion(t):
return AnyType(TypeOfAny.from_error)
return super().visit_overloaded(t)

def visit_tuple_type(self, t: TupleType) -> Type:
if self.check(t):
if self.check_recursion(t):
return AnyType(TypeOfAny.from_error)
items = [it.accept(self) for it in t.items]
fallback = self.visit_instance(t.fallback, from_fallback=True)
assert isinstance(fallback, Instance)
return TupleType(items, fallback)

def visit_typeddict_type(self, t: TypedDictType) -> Type:
if self.check(t):
if self.check_recursion(t):
return AnyType(TypeOfAny.from_error)
return super().visit_typeddict_type(t)

def visit_union_type(self, t: UnionType) -> Type:
if self.check(t):
if self.check_recursion(t):
return AnyType(TypeOfAny.from_error)
return super().visit_union_type(t)

def visit_type_type(self, t: TypeType) -> Type:
if self.check(t):
if self.check_recursion(t):
return AnyType(TypeOfAny.from_error)
return super().visit_type_type(t)
19 changes: 17 additions & 2 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -1376,8 +1376,21 @@ def deserialize(cls, data: JsonDict) -> Type:


class ForwardRef(Type):
"""Class to wrap forward references to other types."""
link = None # type: Type # the wrapped type
"""Class to wrap forward references to other types.

This is used when a forward reference to an (unanalyzed) synthetic type is found,
for example:

x: A
A = TypedDict('A', {'x': int})

To avoid false positives and crashes in such situations, we first wrap the second
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be 'the first occurrence ...'?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, will fix this docstring.

occurrence of 'A' in ForwardRef. Then, the wrapped UnboundType is updated in the third
pass of semantic analysis and ultimately fixed in the patches after the third pass.
So that ForwardRefs are temporary and will be completely replaced with the linked types
or Any (to avoid cyclic references) before the type checking stage.
"""
link = None # type: Type # The wrapped type

def __init__(self, link: Type) -> None:
self.link = link
Expand All @@ -1392,6 +1405,8 @@ def serialize(self):
name = self.link.type.name()
else:
name = self.link.__class__.__name__
# We should never get here since all forward references should be resolved
# and removed during semantic analysis.
assert False, "Internal error: Unresolved forward reference to {}".format(name)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add comment about why we shouldn't get here (all forward references should be resolved and removed during semantic analysis).



Expand Down
21 changes: 14 additions & 7 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -3611,6 +3611,10 @@ NameInfo = NewType('NameInfo', NameInfoBase)
def parse_ast(name_dict: NameDict) -> None:
if isinstance(name_dict[''], int):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reveal tyhe type of name_dict and verify that it actually behaves like a NewType.

pass
x = name_dict['']
reveal_type(x) # E: Revealed type is '__main__.NameInfo*'
x = NameInfo(NameInfoBase()) # OK
x = NameInfoBase() # E: Incompatible types in assignment (expression has type "NameInfoBase", variable has type "NameInfo")
[builtins fixtures/isinstancelist.pyi]
[out]

Expand Down Expand Up @@ -3666,12 +3670,13 @@ from typing import Union, NamedTuple

NodeType = Union['Foo', 'Bar']
class Foo(NamedTuple):
pass
x: int
class Bar(NamedTuple):
pass
x: int

def foo(node: NodeType):
def foo(node: NodeType) -> int:
x = node
return x.x
[out]

[case testCrashOnForwardUnionOfTypedDicts]
Expand All @@ -3680,12 +3685,13 @@ from typing import Union

NodeType = Union['Foo', 'Bar']
class Foo(TypedDict):
pass
x: int
class Bar(TypedDict):
pass
x: int

def foo(node: NodeType):
def foo(node: NodeType) -> int:
x = node
return x['x']
[builtins fixtures/isinstancelist.pyi]
[out]

Expand All @@ -3696,8 +3702,9 @@ NodeType = Union['Foo', 'Bar']
Foo = NewType('Foo', int)
Bar = NewType('Bar', str)

def foo(node: NodeType):
def foo(node: NodeType) -> NodeType:
x = node
return x
[out]

[case testCrashOnComplexCheckWithNamedTupleUnion]
Expand Down