Skip to content

Commit 68c6e96

Browse files
gvanrossumJukkaL
authored andcommitted
Check deferred nodes after all first passes in an import cycle (#2264)
* Use a separate TypeChecker per file. * Change the TypeChecker toplevel interface. * Do deferred (second) type checker pass after all first passes in an SCC. * Add simple tests. These verify that deferral due to a cross-module dependency works. * Fix test failures due to semantic_analysis_only flag. * Add TODOs about getting rid of BuildResult and all_types. * Also hide cross-module notes with --hide-error-context * Expand docstring for check_first_pass() * Suppress reveal_type() output when current node is being deferred * Add reveal_type() calls to testSymmetricImportCycle* * WIP skip deferred queue duplicates * Make it easy to do more than 2 passes * Add a test that would require three passes * Make lint happy * Test case for error reported by pass two * Test case for deferred decorator * Explicit test case that reveal_type() shuts up in deferred function first time around * Remove print calls * Remove --hide-error-context from new tests and adjust expected output * Add reversed version of testErrorInPassTwo * Make decorator more complicated (both b.deco and a.g are now deferred)
1 parent 6938e7b commit 68c6e96

File tree

6 files changed

+221
-53
lines changed

6 files changed

+221
-53
lines changed

mypy/build.py

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,11 @@
3333
from mypy import moduleinfo
3434
from mypy import util
3535
from mypy.fixup import fixup_module_pass_one, fixup_module_pass_two
36+
from mypy.nodes import Expression
3637
from mypy.options import Options
3738
from mypy.parse import parse
3839
from mypy.stats import dump_type_stats
40+
from mypy.types import Type
3941
from mypy.version import __version__
4042

4143

@@ -49,6 +51,7 @@
4951
Graph = Dict[str, 'State']
5052

5153

54+
# TODO: Get rid of BuildResult. We might as well return a BuildManager.
5255
class BuildResult:
5356
"""The result of a successful build.
5457
@@ -62,7 +65,7 @@ class BuildResult:
6265
def __init__(self, manager: 'BuildManager') -> None:
6366
self.manager = manager
6467
self.files = manager.modules
65-
self.types = manager.type_checker.type_map
68+
self.types = manager.all_types
6669
self.errors = manager.errors.messages()
6770

6871

@@ -184,7 +187,7 @@ def build(sources: List[BuildSource],
184187
manager.log("Build finished in %.3f seconds with %d modules, %d types, and %d errors" %
185188
(time.time() - manager.start_time,
186189
len(manager.modules),
187-
len(manager.type_checker.type_map),
190+
len(manager.all_types),
188191
manager.errors.num_messages()))
189192
# Finish the HTML or XML reports even if CompileError was raised.
190193
reports.finish()
@@ -307,6 +310,8 @@ def default_lib_path(data_dir: str, pyversion: Tuple[int, int]) -> List[str]:
307310
PRI_ALL = 99 # include all priorities
308311

309312

313+
# TODO: Get rid of all_types. It's not used except for one log message.
314+
# Maybe we could instead publish a map from module ID to its type_map.
310315
class BuildManager:
311316
"""This class holds shared state for building a mypy program.
312317
@@ -322,7 +327,7 @@ class BuildManager:
322327
Semantic analyzer, pass 2
323328
semantic_analyzer_pass3:
324329
Semantic analyzer, pass 3
325-
type_checker: Type checker
330+
all_types: Map {Expression: Type} collected from all modules
326331
errors: Used for reporting all errors
327332
options: Build options
328333
missing_modules: Set of modules that could not be imported encountered so far
@@ -349,7 +354,7 @@ def __init__(self, data_dir: str,
349354
self.semantic_analyzer = SemanticAnalyzer(lib_path, self.errors)
350355
self.modules = self.semantic_analyzer.modules
351356
self.semantic_analyzer_pass3 = ThirdPass(self.modules, self.errors)
352-
self.type_checker = TypeChecker(self.errors, self.modules)
357+
self.all_types = {} # type: Dict[Expression, Type]
353358
self.indirection_detector = TypeIndirectionVisitor()
354359
self.missing_modules = set() # type: Set[str]
355360
self.stale_modules = set() # type: Set[str]
@@ -461,9 +466,9 @@ def module_not_found(self, path: str, line: int, id: str) -> None:
461466
'or using the "--silent-imports" flag would help)',
462467
severity='note', only_once=True)
463468

464-
def report_file(self, file: MypyFile) -> None:
469+
def report_file(self, file: MypyFile, type_map: Dict[Expression, Type]) -> None:
465470
if self.source_set.is_source(file):
466-
self.reports.file(file, type_map=self.type_checker.type_map)
471+
self.reports.file(file, type_map)
467472

468473
def log(self, *message: str) -> None:
469474
if self.options.verbosity >= 1:
@@ -1407,23 +1412,42 @@ def semantic_analysis_pass_three(self) -> None:
14071412
if self.options.dump_type_stats:
14081413
dump_type_stats(self.tree, self.xpath)
14091414

1410-
def type_check(self) -> None:
1415+
def type_check_first_pass(self) -> None:
14111416
manager = self.manager
14121417
if self.options.semantic_analysis_only:
14131418
return
14141419
with self.wrap_context():
1415-
manager.type_checker.visit_file(self.tree, self.xpath, self.options)
1420+
self.type_checker = TypeChecker(manager.errors, manager.modules, self.options,
1421+
self.tree, self.xpath)
1422+
self.type_checker.check_first_pass()
1423+
1424+
def type_check_second_pass(self) -> bool:
1425+
if self.options.semantic_analysis_only:
1426+
return False
1427+
with self.wrap_context():
1428+
return self.type_checker.check_second_pass()
1429+
1430+
def finish_passes(self) -> None:
1431+
manager = self.manager
1432+
if self.options.semantic_analysis_only:
1433+
return
1434+
with self.wrap_context():
1435+
manager.all_types.update(self.type_checker.type_map)
14161436

14171437
if self.options.incremental:
1418-
self._patch_indirect_dependencies(manager.type_checker.module_refs)
1438+
self._patch_indirect_dependencies(self.type_checker.module_refs,
1439+
self.type_checker.type_map)
14191440

14201441
if self.options.dump_inference_stats:
14211442
dump_type_stats(self.tree, self.xpath, inferred=True,
1422-
typemap=manager.type_checker.type_map)
1423-
manager.report_file(self.tree)
1424-
1425-
def _patch_indirect_dependencies(self, module_refs: Set[str]) -> None:
1426-
types = self.manager.type_checker.module_type_map.values()
1443+
typemap=self.type_checker.type_map)
1444+
manager.report_file(self.tree, self.type_checker.type_map)
1445+
1446+
def _patch_indirect_dependencies(self,
1447+
module_refs: Set[str],
1448+
type_map: Dict[Expression, Type]) -> None:
1449+
types = set(type_map.values())
1450+
types.discard(None)
14271451
valid = self.valid_references()
14281452

14291453
encountered = self.manager.indirection_detector.find_modules(types) | module_refs
@@ -1726,7 +1750,15 @@ def process_stale_scc(graph: Graph, scc: List[str]) -> None:
17261750
for id in scc:
17271751
graph[id].semantic_analysis_pass_three()
17281752
for id in scc:
1729-
graph[id].type_check()
1753+
graph[id].type_check_first_pass()
1754+
more = True
1755+
while more:
1756+
more = False
1757+
for id in scc:
1758+
if graph[id].type_check_second_pass():
1759+
more = True
1760+
for id in scc:
1761+
graph[id].finish_passes()
17301762
graph[id].write_cache()
17311763
graph[id].mark_as_rechecked()
17321764

mypy/checker.py

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@
5959

6060
T = TypeVar('T')
6161

62+
LAST_PASS = 1 # Pass numbers start at 0
63+
6264

6365
# A node which is postponed to be type checked during the next pass.
6466
DeferredNode = NamedTuple(
@@ -73,6 +75,8 @@ class TypeChecker(NodeVisitor[Type]):
7375
"""Mypy type checker.
7476
7577
Type check mypy source files that have been semantically analyzed.
78+
79+
You must create a separate instance for each source file.
7680
"""
7781

7882
# Are we type checking a stub?
@@ -83,8 +87,6 @@ class TypeChecker(NodeVisitor[Type]):
8387
msg = None # type: MessageBuilder
8488
# Types of type checked nodes
8589
type_map = None # type: Dict[Expression, Type]
86-
# Types of type checked nodes within this specific module
87-
module_type_map = None # type: Dict[Expression, Type]
8890

8991
# Helper for managing conditional types
9092
binder = None # type: ConditionalTypeBinder
@@ -121,56 +123,60 @@ class TypeChecker(NodeVisitor[Type]):
121123
# directly or indirectly.
122124
module_refs = None # type: Set[str]
123125

124-
def __init__(self, errors: Errors, modules: Dict[str, MypyFile]) -> None:
126+
def __init__(self, errors: Errors, modules: Dict[str, MypyFile], options: Options,
127+
tree: MypyFile, path: str) -> None:
125128
"""Construct a type checker.
126129
127130
Use errors to report type check errors.
128131
"""
129132
self.errors = errors
130133
self.modules = modules
134+
self.options = options
135+
self.tree = tree
136+
self.path = path
131137
self.msg = MessageBuilder(errors, modules)
132-
self.type_map = {}
133-
self.module_type_map = {}
134-
self.binder = ConditionalTypeBinder()
135138
self.expr_checker = mypy.checkexpr.ExpressionChecker(self, self.msg)
139+
self.binder = ConditionalTypeBinder()
140+
self.globals = tree.names
136141
self.return_types = []
137142
self.type_context = []
138143
self.dynamic_funcs = []
139144
self.function_stack = []
140145
self.partial_types = []
141146
self.deferred_nodes = []
142-
self.pass_num = 0
143-
self.current_node_deferred = False
147+
self.type_map = {}
144148
self.module_refs = set()
145-
146-
def visit_file(self, file_node: MypyFile, path: str, options: Options) -> None:
147-
"""Type check a mypy file with the given path."""
148-
self.options = options
149149
self.pass_num = 0
150-
self.is_stub = file_node.is_stub
151-
self.errors.set_file(path)
152-
self.globals = file_node.names
153-
self.enter_partial_types()
154-
self.is_typeshed_stub = self.errors.is_typeshed_file(path)
155-
self.module_type_map = {}
156-
self.module_refs = set()
157-
if self.options.strict_optional_whitelist is None:
158-
self.suppress_none_errors = not self.options.show_none_errors
150+
self.current_node_deferred = False
151+
self.is_stub = tree.is_stub
152+
self.is_typeshed_stub = errors.is_typeshed_file(path)
153+
if options.strict_optional_whitelist is None:
154+
self.suppress_none_errors = not options.show_none_errors
159155
else:
160156
self.suppress_none_errors = not any(fnmatch.fnmatch(path, pattern)
161157
for pattern
162-
in self.options.strict_optional_whitelist)
158+
in options.strict_optional_whitelist)
159+
160+
def check_first_pass(self) -> None:
161+
"""Type check the entire file, but defer functions with unresolved references.
162+
163+
Unresolved references are forward references to variables
164+
whose types haven't been inferred yet. They may occur later
165+
in the same file or in a different file that's being processed
166+
later (usually due to an import cycle).
167+
168+
Deferred functions will be processed by check_second_pass().
169+
"""
170+
self.errors.set_file(self.path)
171+
self.enter_partial_types()
163172

164173
with self.binder.top_frame_context():
165-
for d in file_node.defs:
174+
for d in self.tree.defs:
166175
self.accept(d)
167176

168177
self.leave_partial_types()
169178

170-
if self.deferred_nodes:
171-
self.check_second_pass()
172-
173-
self.current_node_deferred = False
179+
assert not self.current_node_deferred
174180

175181
all_ = self.globals.get('__all__')
176182
if all_ is not None and all_.type is not None:
@@ -181,21 +187,31 @@ def visit_file(self, file_node: MypyFile, path: str, options: Options) -> None:
181187
self.fail(messages.ALL_MUST_BE_SEQ_STR.format(str_seq_s, all_s),
182188
all_.node)
183189

184-
del self.options
190+
def check_second_pass(self) -> bool:
191+
"""Run second or following pass of type checking.
185192
186-
def check_second_pass(self) -> None:
187-
"""Run second pass of type checking which goes through deferred nodes."""
188-
self.pass_num = 1
189-
for node, type_name in self.deferred_nodes:
193+
This goes through deferred nodes, returning True if there were any.
194+
"""
195+
if not self.deferred_nodes:
196+
return False
197+
self.errors.set_file(self.path)
198+
self.pass_num += 1
199+
todo = self.deferred_nodes
200+
self.deferred_nodes = []
201+
done = set() # type: Set[FuncItem]
202+
for node, type_name in todo:
203+
if node in done:
204+
continue
205+
done.add(node)
190206
if type_name:
191207
self.errors.push_type(type_name)
192208
self.accept(node)
193209
if type_name:
194210
self.errors.pop_type()
195-
self.deferred_nodes = []
211+
return True
196212

197213
def handle_cannot_determine_type(self, name: str, context: Context) -> None:
198-
if self.pass_num == 0 and self.function_stack:
214+
if self.pass_num < LAST_PASS and self.function_stack:
199215
# Don't report an error yet. Just defer.
200216
node = self.function_stack[-1]
201217
if self.errors.type_name:
@@ -2232,8 +2248,6 @@ def check_type_equivalency(self, t1: Type, t2: Type, node: Context,
22322248
def store_type(self, node: Expression, typ: Type) -> None:
22332249
"""Store the type of a node in the type map."""
22342250
self.type_map[node] = typ
2235-
if typ is not None:
2236-
self.module_type_map[node] = typ
22372251

22382252
def in_checked_function(self) -> bool:
22392253
"""Should we type-check the current function?

mypy/checkexpr.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1367,7 +1367,8 @@ def is_valid_cast(self, source_type: Type, target_type: Type) -> bool:
13671367
def visit_reveal_type_expr(self, expr: RevealTypeExpr) -> Type:
13681368
"""Type check a reveal_type expression."""
13691369
revealed_type = self.accept(expr.expr)
1370-
self.msg.reveal_type(revealed_type, expr)
1370+
if not self.chk.current_node_deferred:
1371+
self.msg.reveal_type(revealed_type, expr)
13711372
return revealed_type
13721373

13731374
def visit_type_application(self, tapp: TypeApplication) -> Type:

mypy/errors.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,9 @@ def render_messages(self, errors: List[ErrorInfo]) -> List[Tuple[str, int, int,
290290

291291
for e in errors:
292292
# Report module import context, if different from previous message.
293-
if e.import_ctx != prev_import_context:
293+
if self.hide_error_context:
294+
pass
295+
elif e.import_ctx != prev_import_context:
294296
last = len(e.import_ctx) - 1
295297
i = last
296298
while i >= 0:

test-data/unit/check-expressions.test

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1605,6 +1605,14 @@ reveal_type("foo") # E: Argument 1 to "reveal_type" has incompatible type "str";
16051605
reveal_type = 1
16061606
1 + "foo" # E: Unsupported operand types for + ("int" and "str")
16071607

1608+
[case testRevealForward]
1609+
def f() -> None:
1610+
reveal_type(x)
1611+
x = 1 + 1
1612+
[out]
1613+
main: note: In function "f":
1614+
main:2: error: Revealed type is 'builtins.int'
1615+
16081616
[case testEqNone]
16091617
None == None
16101618
[builtins fixtures/ops.pyi]

0 commit comments

Comments
 (0)