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 33 commits
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
7 changes: 5 additions & 2 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ def __init__(self, data_dir: str,
self.semantic_analyzer = SemanticAnalyzer(self.modules, self.missing_modules,
lib_path, self.errors, self.plugin)
self.modules = self.semantic_analyzer.modules
self.semantic_analyzer_pass3 = ThirdPass(self.modules, self.errors)
self.semantic_analyzer_pass3 = ThirdPass(self.modules, self.errors, self.semantic_analyzer)
self.all_types = {} # type: Dict[Expression, Type]
self.indirection_detector = TypeIndirectionVisitor()
self.stale_modules = set() # type: Set[str]
Expand Down Expand Up @@ -1722,10 +1722,13 @@ def semantic_analysis(self) -> None:

def semantic_analysis_pass_three(self) -> None:
assert self.tree is not None, "Internal error: method must be called on parsed file only"
patches = [] # type: List[Callable[[], None]]
with self.wrap_context():
self.manager.semantic_analyzer_pass3.visit_file(self.tree, self.xpath, self.options)
self.manager.semantic_analyzer_pass3.visit_file(self.tree, self.xpath,
self.options, patches)
if self.options.dump_type_stats:
dump_type_stats(self.tree, self.xpath)
self.patches = patches + self.patches

def semantic_analysis_apply_patches(self) -> None:
for patch_func in self.patches:
Expand Down
3 changes: 3 additions & 0 deletions mypy/indirection.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,6 @@ def visit_ellipsis_type(self, t: types.EllipsisType) -> Set[str]:

def visit_type_type(self, t: types.TypeType) -> Set[str]:
return self._visit(t.item)

def visit_forwardref_type(self, t: types.ForwardRef) -> Set[str]:
return self._visit(t.link)
8 changes: 6 additions & 2 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from mypy.types import (
Type, CallableType, Instance, TypeVarType, TupleType, TypedDictType,
UnionType, NoneTyp, AnyType, Overloaded, FunctionLike, DeletedType, TypeType,
UninhabitedType, TypeOfAny
UninhabitedType, TypeOfAny, ForwardRef, UnboundType
)
from mypy.nodes import (
TypeInfo, Context, MypyFile, op_methods, FuncDef, reverse_type_aliases,
Expand Down Expand Up @@ -194,7 +194,7 @@ def quote_type_string(self, type_string: str) -> str:
"""Quotes a type representation for use in messages."""
no_quote_regex = r'^<(tuple|union): \d+ items>$'
if (type_string in ['Module', 'overloaded function', '<nothing>', '<deleted>']
or re.match(no_quote_regex, type_string) is not None):
or re.match(no_quote_regex, type_string) is not None or type_string.endswith('?')):
# Messages are easier to read if these aren't quoted. We use a
# regex to match strings with variable contents.
return type_string
Expand Down Expand Up @@ -309,6 +309,8 @@ def format_bare(self, typ: Type, verbosity: int = 0) -> str:
return '<nothing>'
elif isinstance(typ, TypeType):
return 'Type[{}]'.format(self.format_bare(typ.item, verbosity))
elif isinstance(typ, ForwardRef): # may appear in semanal.py
return self.format_bare(typ.link, verbosity)
elif isinstance(typ, FunctionLike):
func = typ
if func.is_type_obj():
Expand Down Expand Up @@ -350,6 +352,8 @@ def format_bare(self, typ: Type, verbosity: int = 0) -> str:
# function types may result in long and difficult-to-read
# error messages.
return 'overloaded function'
elif isinstance(typ, UnboundType):
return str(typ)
elif typ is None:
raise RuntimeError('Type is None')
else:
Expand Down
6 changes: 6 additions & 0 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1976,6 +1976,12 @@ class is generic then it will be a type constructor of higher kind.
# Is this a newtype type?
is_newtype = False

# If during analysis of ClassDef associated with this TypeInfo a syntethic
# type (NamedTuple or TypedDict) was generated, store the corresponding
# TypeInfo here. (This attribute does not need to be serialized, it is only
# needed during the semantic passes.)
replaced = None # type: TypeInfo

FLAGS = [
'is_abstract', 'is_enum', 'fallback_to_any', 'is_named_tuple',
'is_newtype', 'is_protocol', 'runtime_protocol'
Expand Down
290 changes: 254 additions & 36 deletions mypy/semanal.py

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion mypy/server/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from mypy.types import (
Type, Instance, AnyType, NoneTyp, TypeVisitor, CallableType, DeletedType, PartialType,
TupleType, TypeType, TypeVarType, TypedDictType, UnboundType, UninhabitedType, UnionType,
FunctionLike
FunctionLike, ForwardRef
)
from mypy.server.trigger import make_trigger

Expand Down Expand Up @@ -212,6 +212,9 @@ def visit_type_type(self, typ: TypeType) -> List[str]:
# TODO: replace with actual implementation
return []

def visit_forwardref_type(self, typ: ForwardRef) -> List[str]:
return get_type_dependencies(typ.link)

def visit_type_var(self, typ: TypeVarType) -> List[str]:
# TODO: replace with actual implementation
return []
Expand Down
54 changes: 45 additions & 9 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Semantic analysis of types"""

from collections import OrderedDict
from typing import Callable, List, Optional, Set, Tuple, Iterator, TypeVar, Iterable
from typing import Callable, List, Optional, Set, Tuple, Iterator, TypeVar, Iterable, Dict
from itertools import chain

from contextlib import contextmanager
Expand All @@ -14,13 +14,13 @@
Type, UnboundType, TypeVarType, TupleType, TypedDictType, UnionType, Instance, AnyType,
CallableType, NoneTyp, DeletedType, TypeList, TypeVarDef, TypeVisitor, SyntheticTypeVisitor,
StarType, PartialType, EllipsisType, UninhabitedType, TypeType, get_typ_args, set_typ_args,
CallableArgument, get_type_vars, TypeQuery, union_items, TypeOfAny
CallableArgument, get_type_vars, TypeQuery, union_items, TypeOfAny, ForwardRef
)

from mypy.nodes import (
TVAR, TYPE_ALIAS, UNBOUND_IMPORTED, TypeInfo, Context, SymbolTableNode, Var, Expression,
IndexExpr, RefExpr, nongen_builtins, check_arg_names, check_arg_kinds, ARG_POS, ARG_NAMED,
ARG_OPT, ARG_NAMED_OPT, ARG_STAR, ARG_STAR2, TypeVarExpr, CallExpr, NameExpr
ARG_OPT, ARG_NAMED_OPT, ARG_STAR, ARG_STAR2, TypeVarExpr, FuncDef, CallExpr, NameExpr
)
from mypy.tvar_scope import TypeVarScope
from mypy.sametypes import is_same_type
Expand All @@ -29,6 +29,9 @@
from mypy.plugin import Plugin, AnalyzerPluginInterface, AnalyzeTypeContext
from mypy import nodes, messages

MYPY = False
if MYPY:
from mypy.semanal import SemanticAnalyzer

T = TypeVar('T')

Expand Down Expand Up @@ -59,7 +62,8 @@ def analyze_type_alias(node: Expression,
plugin: Plugin,
options: Options,
is_typeshed_stub: bool,
allow_unnormalized: bool = False) -> Optional[Type]:
allow_unnormalized: bool = False,
in_dynamic_func: bool = False) -> Optional[Type]:
"""Return type if node is valid as a type alias rvalue.

Return None otherwise. 'node' must have been semantically analyzed.
Expand Down Expand Up @@ -113,6 +117,7 @@ def analyze_type_alias(node: Expression,
return None
analyzer = TypeAnalyser(lookup_func, lookup_fqn_func, tvar_scope, fail_func, plugin, options,
is_typeshed_stub, aliasing=True, allow_unnormalized=allow_unnormalized)
analyzer.in_dynamic_func = in_dynamic_func
return type.accept(analyzer)


Expand All @@ -130,6 +135,9 @@ class TypeAnalyser(SyntheticTypeVisitor[Type], AnalyzerPluginInterface):
Converts unbound types into bound types.
"""

# Is this called from an untyped function definition
in_dynamic_func = False # type: bool

def __init__(self,
lookup_func: Callable[[str, Context], SymbolTableNode],
lookup_fqn_func: Callable[[str], SymbolTableNode],
Expand All @@ -140,7 +148,8 @@ def __init__(self,
is_typeshed_stub: bool, *,
aliasing: bool = False,
allow_tuple_literal: bool = False,
allow_unnormalized: bool = False) -> None:
allow_unnormalized: bool = False,
third_pass: bool = False) -> None:
self.lookup = lookup_func
self.lookup_fqn_func = lookup_fqn_func
self.fail_func = fail_func
Expand All @@ -153,14 +162,15 @@ def __init__(self,
self.plugin = plugin
self.options = options
self.is_typeshed_stub = is_typeshed_stub
self.third_pass = third_pass

def visit_unbound_type(self, t: UnboundType) -> Type:
if t.optional:
t.optional = False
# We don't need to worry about double-wrapping Optionals or
# wrapping Anys: Union simplification will take care of that.
return make_optional_type(self.visit_unbound_type(t))
sym = self.lookup(t.name, t)
sym = self.lookup(t.name, t, suppress_errors=self.third_pass) # type: ignore
if sym is not None:
if sym.node is None:
# UNBOUND_IMPORTED can happen if an unknown name was imported.
Expand Down Expand Up @@ -269,6 +279,10 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
# Allow unbound type variables when defining an alias
if not (self.aliasing and sym.kind == TVAR and
self.tvar_scope.get_binding(sym) is None):
if (not self.third_pass and not self.in_dynamic_func and
not (isinstance(sym.node, FuncDef) or
isinstance(sym.node, Var) and sym.node.is_ready)):
return ForwardRef(t)
self.fail('Invalid type "{}"'.format(name), t)
return t
info = sym.node # type: TypeInfo
Expand Down Expand Up @@ -304,6 +318,9 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
fallback=instance)
return instance
else:
if self.third_pass:
self.fail('Invalid type {}'.format(t), t)
return AnyType(TypeOfAny.from_error)
return AnyType(TypeOfAny.special_form)

def visit_any(self, t: AnyType) -> Type:
Expand Down Expand Up @@ -387,6 +404,9 @@ def visit_ellipsis_type(self, t: EllipsisType) -> Type:
def visit_type_type(self, t: TypeType) -> Type:
return TypeType.make_normalized(self.anal_type(t.item), line=t.line)

def visit_forwardref_type(self, t: ForwardRef) -> Type:
return t

def analyze_callable_type(self, t: UnboundType) -> Type:
fallback = self.named_type('builtins.function')
if len(t.args) == 0:
Expand Down Expand Up @@ -590,13 +610,18 @@ class TypeAnalyserPass3(TypeVisitor[None]):
def __init__(self,
fail_func: Callable[[str, Context], None],
options: Options,
is_typeshed_stub: bool) -> None:
is_typeshed_stub: bool,
sem: 'SemanticAnalyzer', indicator: Dict[str, bool]) -> None:
self.fail = fail_func
self.options = options
self.is_typeshed_stub = is_typeshed_stub
self.sem = sem
self.indicator = indicator

def visit_instance(self, t: Instance) -> None:
info = t.type
if info.replaced or info.tuple_type:
self.indicator['synthetic'] = True
# Check type argument count.
if len(t.args) != len(info.type_vars):
if len(t.args) == 0:
Expand Down Expand Up @@ -647,6 +672,8 @@ def visit_instance(self, t: Instance) -> None:
else:
arg_values = [arg]
self.check_type_var_values(info, arg_values, tvar.name, tvar.values, i + 1, t)
if isinstance(arg, ForwardRef):
arg = arg.link
if not is_subtype(arg, tvar.upper_bound):
self.fail('Type argument "{}" of "{}" must be '
'a subtype of "{}"'.format(
Expand Down Expand Up @@ -712,13 +739,22 @@ def visit_type_list(self, t: TypeList) -> None:
self.fail('Invalid type', t)

def visit_type_var(self, t: TypeVarType) -> None:
pass
if t.upper_bound:
t.upper_bound.accept(self)
if t.values:
for v in t.values:
v.accept(self)

def visit_partial_type(self, t: PartialType) -> None:
pass

def visit_type_type(self, t: TypeType) -> None:
pass
t.item.accept(self)

def visit_forwardref_type(self, t: ForwardRef) -> None:
self.indicator['forward'] = True
if isinstance(t.link, UnboundType):
t.link = self.sem.anal_type(t.link, third_pass=True)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is kind of hacky -- we call the second pass of semantic analysis from the third pass of type analysis, without having all the normal context for the second pass set up. This causes some errors:

from typing import List, TypeVar

T = TypeVar('T')

def f(x: T) -> None:
    y: A[T]  # "Invalid type "t.T"

A = List[T]

Another example that doesn't work because of this issue:

    from typing import List, TypeVar

    def f() -> None:
        X = List[int]
        x: A[X]  # "Invalid type X?"

    T = TypeVar('T')
    A = List[T]

Not sure what's the best way to approach this. We perhaps don't want to set up the full context for second pass during the third pass, as this can add a lot of extra complexity. A simpler workaround would be to not support forward references with type arguments, and give an error such as Forward reference to "A" not supported. This at least would fix the current error messages, which are confusing.

Also, I'd prefer if we didn't depend on mypy.semanal here. A better approach would be to remove the cyclic import to mypy.semanal and call TypeAnalyser directly here. TypeAnalyserPass3 would take the lookup functions etc. as arguments instead of getting them indirectly from SemanticAnalyzer. The rationale is that this makes it explicit what context the third pass depends on, and it will make the code clearer. Also, we won't depend on the internals of SemanticAnalyzer.

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, this is one of the hard parts. I will try to see what is the minimal context needed, basically we just need lookup to work in third pass. If it will be too hard, I will just prohibit things like F[<something>], where F is a forward reference, with a clearer error message.



TypeVarList = List[Tuple[str, TypeVarExpr]]
Expand Down
32 changes: 32 additions & 0 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -1375,6 +1375,26 @@ def deserialize(cls, data: JsonDict) -> Type:
return TypeType.make_normalized(deserialize_type(data['item']))


class ForwardRef(Type):
"""Class to wrap forward references to other types."""
Copy link
Collaborator

Choose a reason for hiding this comment

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

Add a longer description since this is a little tricky. For example, give an example where this is useful (say, a forward reference to a TypedDict). Also mention that these are temporary and will be replaced with the linked type in the third pass of semantic analysis. The type checker doesn't see these at all.

link = None # type: Type # the wrapped type
Copy link
Collaborator

Choose a reason for hiding this comment

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

Style nit: Use two spaces before the non-type comment and capitalize the wrapped type.


def __init__(self, link: Type) -> None:
self.link = link

def accept(self, visitor: 'TypeVisitor[T]') -> T:
return visitor.visit_forwardref_type(self)

def serialize(self):
if isinstance(self.link, UnboundType):
name = self.link.name
if isinstance(self.link, Instance):
name = self.link.type.name()
else:
name = self.link.__class__.__name__
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).



#
# Visitor-related classes
#
Expand Down Expand Up @@ -1450,6 +1470,9 @@ def visit_partial_type(self, t: PartialType) -> T:
def visit_type_type(self, t: TypeType) -> T:
pass

def visit_forwardref_type(self, t: ForwardRef) -> T:
raise RuntimeError('Internal error: unresolved forward reference')


class SyntheticTypeVisitor(TypeVisitor[T]):
"""A TypeVisitor that also knows how to visit synthetic AST constructs.
Expand Down Expand Up @@ -1552,6 +1575,9 @@ def visit_overloaded(self, t: Overloaded) -> Type:
def visit_type_type(self, t: TypeType) -> Type:
return TypeType.make_normalized(t.item.accept(self), line=t.line, column=t.column)

def visit_forwardref_type(self, t: ForwardRef) -> Type:
return t


class TypeStrVisitor(SyntheticTypeVisitor[str]):
"""Visitor for pretty-printing types into strings.
Expand Down Expand Up @@ -1707,6 +1733,9 @@ def visit_ellipsis_type(self, t: EllipsisType) -> str:
def visit_type_type(self, t: TypeType) -> str:
return 'Type[{}]'.format(t.item.accept(self))

def visit_forwardref_type(self, t: ForwardRef) -> str:
return '~{}'.format(t.link.accept(self))

def list_str(self, a: List[Type]) -> str:
"""Convert items of an array to strings (pretty-print types)
and join the results with commas.
Expand Down Expand Up @@ -1786,6 +1815,9 @@ def visit_overloaded(self, t: Overloaded) -> T:
def visit_type_type(self, t: TypeType) -> T:
return t.item.accept(self)

def visit_forwardref_type(self, t: ForwardRef) -> T:
return t.link.accept(self)

def visit_ellipsis_type(self, t: EllipsisType) -> T:
return self.strategy([])

Expand Down
Loading