Skip to content

Commit 2833062

Browse files
authored
Fine-grained: Apply semantic analyzer patch callbacks (#4658)
This fixes crashes and at least one edge case. Not sure if all the patch callbacks are necessary when propagating fine-grained dependencies, but it seems safer to invoke them all. Fixes #4657.
1 parent 3e1baae commit 2833062

File tree

6 files changed

+77
-21
lines changed

6 files changed

+77
-21
lines changed

mypy/build.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535

3636
from mypy.nodes import (MODULE_REF, MypyFile, Node, ImportBase, Import, ImportFrom, ImportAll)
3737
from mypy.semanal_pass1 import SemanticAnalyzerPass1
38-
from mypy.semanal import SemanticAnalyzerPass2
38+
from mypy.semanal import SemanticAnalyzerPass2, apply_semantic_analyzer_patches
3939
from mypy.semanal_pass3 import SemanticAnalyzerPass3
4040
from mypy.checker import TypeChecker
4141
from mypy.indirection import TypeIndirectionVisitor
@@ -1960,9 +1960,7 @@ def semantic_analysis_pass_three(self) -> None:
19601960
self.patches = patches + self.patches
19611961

19621962
def semantic_analysis_apply_patches(self) -> None:
1963-
patches_by_priority = sorted(self.patches, key=lambda x: x[0])
1964-
for priority, patch_func in patches_by_priority:
1965-
patch_func()
1963+
apply_semantic_analyzer_patches(self.patches)
19661964

19671965
def type_check_first_pass(self) -> None:
19681966
if self.options.semantic_analysis_only:

mypy/semanal.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -309,24 +309,24 @@ def visit_file(self, file_node: MypyFile, fnam: str, options: Options,
309309
del self.cur_mod_node
310310
del self.globals
311311

312-
def refresh_partial(self, node: Union[MypyFile, FuncItem, OverloadedFuncDef]) -> None:
312+
def refresh_partial(self, node: Union[MypyFile, FuncItem, OverloadedFuncDef],
313+
patches: List[Tuple[int, Callable[[], None]]]) -> None:
313314
"""Refresh a stale target in fine-grained incremental mode."""
315+
self.patches = patches
314316
self.scope.enter_file(self.cur_mod_id)
315317
if isinstance(node, MypyFile):
316318
self.refresh_top_level(node)
317319
else:
318320
self.recurse_into_functions = True
319321
self.accept(node)
320322
self.scope.leave()
323+
del self.patches
321324

322325
def refresh_top_level(self, file_node: MypyFile) -> None:
323326
"""Reanalyze a stale module top-level in fine-grained incremental mode."""
324-
# TODO: Invoke patches in fine-grained incremental mode.
325-
self.patches = []
326327
self.recurse_into_functions = False
327328
for d in file_node.defs:
328329
self.accept(d)
329-
del self.patches
330330

331331
@contextmanager
332332
def file_context(self, file_node: MypyFile, fnam: str, options: Options,
@@ -4307,3 +4307,13 @@ def visit_any(self, t: AnyType) -> Type:
43074307
if t.type_of_any == TypeOfAny.explicit:
43084308
return t.copy_modified(TypeOfAny.special_form)
43094309
return t
4310+
4311+
4312+
def apply_semantic_analyzer_patches(patches: List[Tuple[int, Callable[[], None]]]) -> None:
4313+
"""Call patch callbacks in the right order.
4314+
4315+
This should happen after semantic analyzer pass 3.
4316+
"""
4317+
patches_by_priority = sorted(patches, key=lambda x: x[0])
4318+
for priority, patch_func in patches_by_priority:
4319+
patch_func()

mypy/semanal_pass3.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,10 @@ def visit_file(self, file_node: MypyFile, fnam: str, options: Options,
7171
del self.cur_mod_node
7272
self.patches = []
7373

74-
def refresh_partial(self, node: Union[MypyFile, FuncItem, OverloadedFuncDef]) -> None:
74+
def refresh_partial(self, node: Union[MypyFile, FuncItem, OverloadedFuncDef],
75+
patches: List[Tuple[int, Callable[[], None]]]) -> None:
7576
"""Refresh a stale target in fine-grained incremental mode."""
77+
self.patches = patches
7678
self.scope.enter_file(self.sem.cur_mod_id)
7779
if isinstance(node, MypyFile):
7880
self.recurse_into_functions = False
@@ -81,6 +83,7 @@ def refresh_partial(self, node: Union[MypyFile, FuncItem, OverloadedFuncDef]) ->
8183
self.recurse_into_functions = True
8284
self.accept(node)
8385
self.scope.leave()
86+
self.patches = []
8487

8588
def refresh_top_level(self, file_node: MypyFile) -> None:
8689
"""Reanalyze a stale module top-level in fine-grained incremental mode."""

mypy/server/aststrip.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -78,20 +78,24 @@ def strip_file_top_level(self, file_node: MypyFile) -> None:
7878

7979
def visit_class_def(self, node: ClassDef) -> None:
8080
"""Strip class body and type info, but don't strip methods."""
81-
node.info.type_vars = []
82-
node.info.bases = []
83-
node.info.abstract_attributes = []
84-
node.info.mro = []
85-
node.info.add_type_vars()
86-
node.info.tuple_type = None
87-
node.info.typeddict_type = None
88-
node.info._cache = set()
89-
node.info._cache_proper = set()
81+
self.strip_type_info(node.info)
9082
node.base_type_exprs.extend(node.removed_base_type_exprs)
9183
node.removed_base_type_exprs = []
9284
with self.enter_class(node.info):
9385
super().visit_class_def(node)
9486

87+
def strip_type_info(self, info: TypeInfo) -> None:
88+
info.type_vars = []
89+
info.bases = []
90+
info.abstract_attributes = []
91+
info.mro = []
92+
info.add_type_vars()
93+
info.tuple_type = None
94+
info.typeddict_type = None
95+
info.tuple_type = None
96+
info._cache = set()
97+
info._cache_proper = set()
98+
9599
def visit_func_def(self, node: FuncDef) -> None:
96100
if not self.recurse_into_functions:
97101
return

mypy/server/update.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,9 @@
118118
"""
119119

120120
import os.path
121-
from typing import Dict, List, Set, Tuple, Iterable, Union, Optional, Mapping, NamedTuple
121+
from typing import (
122+
Dict, List, Set, Tuple, Iterable, Union, Optional, Mapping, NamedTuple, Callable
123+
)
122124

123125
from mypy.build import (
124126
BuildManager, State, BuildSource, Graph, load_graph, find_module_clear_caches,
@@ -132,6 +134,7 @@
132134
)
133135
from mypy.options import Options
134136
from mypy.types import Type
137+
from mypy.semanal import apply_semantic_analyzer_patches
135138
from mypy.server.astdiff import (
136139
snapshot_symbol_table, compare_symbol_table_snapshots, is_identical_type, SnapshotItem
137140
)
@@ -743,6 +746,8 @@ def key(node: DeferredNode) -> int:
743746
strip_target(deferred.node)
744747
semantic_analyzer = manager.semantic_analyzer
745748

749+
patches = [] # type: List[Tuple[int, Callable[[], None]]]
750+
746751
# Second pass of semantic analysis. We don't redo the first pass, because it only
747752
# does local things that won't go stale.
748753
for deferred in nodes:
@@ -751,7 +756,7 @@ def key(node: DeferredNode) -> int:
751756
fnam=file_node.path,
752757
options=manager.options,
753758
active_type=deferred.active_typeinfo):
754-
manager.semantic_analyzer.refresh_partial(deferred.node)
759+
manager.semantic_analyzer.refresh_partial(deferred.node, patches)
755760

756761
# Third pass of semantic analysis.
757762
for deferred in nodes:
@@ -760,7 +765,9 @@ def key(node: DeferredNode) -> int:
760765
fnam=file_node.path,
761766
options=manager.options,
762767
active_type=deferred.active_typeinfo):
763-
manager.semantic_analyzer_pass3.refresh_partial(deferred.node)
768+
manager.semantic_analyzer_pass3.refresh_partial(deferred.node, patches)
769+
770+
apply_semantic_analyzer_patches(patches)
764771

765772
# Merge symbol tables to preserve identities of AST nodes. The file node will remain
766773
# the same, but other nodes may have been recreated with different identities, such as

test-data/unit/fine-grained.test

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2475,3 +2475,37 @@ else:
24752475
[builtins fixtures/ops.pyi]
24762476
[out]
24772477
==
2478+
2479+
[case testNamedTupleWithinFunction]
2480+
from typing import NamedTuple
2481+
import b
2482+
def f() -> None:
2483+
b.x
2484+
n = NamedTuple('n', [])
2485+
[file b.py]
2486+
x = 0
2487+
[file b.py.2]
2488+
x = ''
2489+
[out]
2490+
==
2491+
2492+
[case testNamedTupleFallback]
2493+
# This test will fail without semantic analyzer pass 2 patches
2494+
import a
2495+
[file a.py]
2496+
import b
2497+
[file b.py]
2498+
from typing import NamedTuple
2499+
import c
2500+
c.x
2501+
class N(NamedTuple):
2502+
count: int
2503+
[file c.py]
2504+
x = 0
2505+
[file c.py.2]
2506+
x = ''
2507+
[builtins fixtures/tuple.pyi]
2508+
[out]
2509+
b.py:5: error: Incompatible types in assignment (expression has type "int", base class "tuple" defined the type as "Callable[[Tuple[int, ...], Any], int]")
2510+
==
2511+
b.py:5: error: Incompatible types in assignment (expression has type "int", base class "tuple" defined the type as "Callable[[Tuple[int, ...], Any], int]")

0 commit comments

Comments
 (0)