Skip to content

Commit c352d0c

Browse files
JukkaLilevkivskyi
authored andcommitted
New semantic analyzer: fix daemon crash during AST strip (#6916)
Fix occasional crash when method and top level are updated at the same time. Stripping the top level removes attributes from class symbol table. Previously we could process a method after stripping and infer that initialization happens in the wrong place, resulting in two initializers for a single attribute. This could result in the same attribute being stripped twice, which causes a KeyError. This fixes the issue by re-applying removed attributes before processing any methods. There is an extra complication in that if we re-process a method that defines an attribute, we can't re-apply that attribute as the type could change. Added logic to handle that by filtering out saved attributes when their initialization assignment is stripped. I changed the data structure used for this away from a list of patch functions to something that allows us to examine the data about saved attributes. Fixes #6914.
1 parent 17b2924 commit c352d0c

File tree

4 files changed

+77
-41
lines changed

4 files changed

+77
-41
lines changed

mypy/newsemanal/semanal_main.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from typing import List, Tuple, Optional, Union, Callable, Iterator
2929

3030
from mypy.nodes import (
31-
MypyFile, TypeInfo, FuncDef, Decorator, OverloadedFuncDef
31+
MypyFile, TypeInfo, FuncDef, Decorator, OverloadedFuncDef, SymbolTableNode, Var, ClassDef
3232
)
3333
from mypy.newsemanal.semanal_typeargs import TypeArgumentAnalyzer
3434
from mypy.state import strict_optional_set
@@ -42,6 +42,7 @@
4242
from mypy.errors import Errors
4343
from mypy.newsemanal.semanal_infer import infer_decorator_signature_if_simple
4444
from mypy.checker import FineGrainedDeferredNode
45+
from mypy.server.aststripnew import SavedAttributes
4546
import mypy.build
4647

4748
MYPY = False
@@ -96,21 +97,26 @@ def cleanup_builtin_scc(state: 'State') -> None:
9697
remove_imported_names_from_symtable(state.tree.names, 'builtins')
9798

9899

99-
def semantic_analysis_for_targets(state: 'State', nodes: List[FineGrainedDeferredNode],
100-
graph: 'Graph', strip_patches: List[Callable[[], None]]) -> None:
100+
def semantic_analysis_for_targets(
101+
state: 'State',
102+
nodes: List[FineGrainedDeferredNode],
103+
graph: 'Graph',
104+
saved_attrs: SavedAttributes) -> None:
101105
"""Semantically analyze only selected nodes in a given module.
102106
103107
This essentially mirrors the logic of semantic_analysis_for_scc()
104108
except that we process only some targets. This is used in fine grained
105109
incremental mode, when propagating an update.
106110
107-
The strip_patches are additional patches that may be produced by aststrip.py to
108-
re-introduce implicitly declared instance variables (attributes defined on self).
111+
The saved_attrs are implicitly declared instance attributes (attributes
112+
defined on self) removed by AST stripper that may need to be reintroduced
113+
here. They must be added before any methods are analyzed.
109114
"""
110115
patches = [] # type: Patches
111116
if any(isinstance(n.node, MypyFile) for n in nodes):
112117
# Process module top level first (if needed).
113118
process_top_levels(graph, [state.id], patches)
119+
restore_saved_attrs(saved_attrs)
114120
analyzer = state.manager.new_semantic_analyzer
115121
for n in nodes:
116122
if isinstance(n.node, MypyFile):
@@ -119,13 +125,30 @@ def semantic_analysis_for_targets(state: 'State', nodes: List[FineGrainedDeferre
119125
process_top_level_function(analyzer, state, state.id,
120126
n.node.fullname(), n.node, n.active_typeinfo, patches)
121127
apply_semantic_analyzer_patches(patches)
122-
for patch in strip_patches:
123-
patch()
124128

125129
check_type_arguments_in_targets(nodes, state, state.manager.errors)
126130
calculate_class_properties(graph, [state.id], state.manager.errors)
127131

128132

133+
def restore_saved_attrs(saved_attrs: SavedAttributes) -> None:
134+
"""Restore instance variables removed during AST strip that haven't been added yet."""
135+
for (cdef, name), sym in saved_attrs.items():
136+
info = cdef.info
137+
existing = info.get(name)
138+
defined_in_this_class = name in info.names
139+
assert isinstance(sym.node, Var)
140+
# This needs to mimic the logic in SemanticAnalyzer.analyze_member_lvalue()
141+
# regarding the existing variable in class body or in a superclass:
142+
# If the attribute of self is not defined in superclasses, create a new Var.
143+
if (existing is None or
144+
# (An abstract Var is considered as not defined.)
145+
(isinstance(existing.node, Var) and existing.node.is_abstract_var) or
146+
# Also an explicit declaration on self creates a new Var unless
147+
# there is already one defined in the class body.
148+
sym.node.explicit_self_type and not defined_in_this_class):
149+
info.names[name] = sym
150+
151+
129152
def process_top_levels(graph: 'Graph', scc: List[str], patches: Patches) -> None:
130153
# Process top levels until everything has been bound.
131154

mypy/server/aststripnew.py

Lines changed: 20 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"""
55

66
import contextlib
7-
from typing import Union, Iterator, Optional, List, Callable
7+
from typing import Union, Iterator, Optional, List, Callable, Dict, Tuple
88

99
from mypy.nodes import (
1010
FuncDef, NameExpr, MemberExpr, RefExpr, MypyFile, ClassDef, AssignmentStmt,
@@ -18,8 +18,11 @@
1818
from mypy.server.aststrip import nothing
1919

2020

21-
def strip_target_new(node: Union[MypyFile, FuncDef, OverloadedFuncDef]
22-
) -> List[Callable[[], None]]:
21+
SavedAttributes = Dict[Tuple[ClassDef, str], SymbolTableNode]
22+
23+
24+
def strip_target_new(node: Union[MypyFile, FuncDef, OverloadedFuncDef],
25+
saved_attrs: SavedAttributes) -> None:
2326
"""Reset a fine-grained incremental target to state before main pass of semantic analysis.
2427
2528
The most notable difference from the old version of strip_target() is that new semantic
@@ -28,24 +31,28 @@ def strip_target_new(node: Union[MypyFile, FuncDef, OverloadedFuncDef]
2831
defined as attributes on self. This is done by patches (callbacks) returned from this function
2932
that re-add these variables when called.
3033
"""
31-
visitor = NodeStripVisitor()
34+
visitor = NodeStripVisitor(saved_attrs)
3235
if isinstance(node, MypyFile):
3336
visitor.strip_file_top_level(node)
3437
else:
3538
node.accept(visitor)
36-
return visitor.patches
3739

3840

3941
class NodeStripVisitor(TraverserVisitor):
40-
def __init__(self) -> None:
42+
def __init__(self, saved_class_attrs: SavedAttributes) -> None:
4143
# The current active class.
4244
self.type = None # type: Optional[TypeInfo]
4345
# This is True at class scope, but not in methods.
4446
self.is_class_body = False
4547
# By default, process function definitions. If False, don't -- this is used for
4648
# processing module top levels.
4749
self.recurse_into_functions = True
48-
self.patches = [] # type: List[Callable[[], None]]
50+
# These attributes were removed from top-level classes during strip and
51+
# will be added afterwards (if no existing definition is found). These
52+
# must be added back before semantically analyzing any methods. These
53+
# allow moving attribute definition from a method (through self.x) to a
54+
# definition inside class body (x = ...).
55+
self.saved_class_attrs = saved_class_attrs
4956

5057
def strip_file_top_level(self, file_node: MypyFile) -> None:
5158
"""Strip a module top-level (don't recursive into functions)."""
@@ -66,7 +73,7 @@ def visit_class_def(self, node: ClassDef) -> None:
6673
# be lost if we only reprocess top-levels (this kills TypeInfos)
6774
# but not the methods that defined those variables.
6875
if not self.recurse_into_functions:
69-
self.prepare_implicit_var_patches(node)
76+
self.save_implicit_attributes(node)
7077
# We need to delete any entries that were generated by plugins,
7178
# since they will get regenerated.
7279
to_delete = {v.node for v in node.info.names.values() if v.plugin_generated}
@@ -81,30 +88,11 @@ def visit_class_def(self, node: ClassDef) -> None:
8188
# Kill the TypeInfo, since there is none before semantic analysis.
8289
node.info = CLASSDEF_NO_INFO
8390

84-
def prepare_implicit_var_patches(self, node: ClassDef) -> None:
91+
def save_implicit_attributes(self, node: ClassDef) -> None:
8592
"""Produce callbacks that re-add attributes defined on self."""
8693
for name, sym in node.info.names.items():
8794
if isinstance(sym.node, Var) and sym.implicit:
88-
explicit_self_type = sym.node.explicit_self_type
89-
90-
def patch(name: str, sym: SymbolTableNode) -> None:
91-
existing = node.info.get(name)
92-
defined_in_this_class = name in node.info.names
93-
# This needs to mimic the logic in SemanticAnalyzer.analyze_member_lvalue()
94-
# regarding the existing variable in class body or in a superclass:
95-
# If the attribute of self is not defined in superclasses, create a new Var.
96-
if (existing is None or
97-
# (An abstract Var is considered as not defined.)
98-
(isinstance(existing.node, Var) and existing.node.is_abstract_var) or
99-
# Also an explicit declaration on self creates a new Var unless
100-
# there is already one defined in the class body.
101-
explicit_self_type and not defined_in_this_class):
102-
node.info.names[name] = sym
103-
104-
# Capture the current name, sym in a weird hacky way,
105-
# because mypyc doesn't yet support capturing them in
106-
# the usual hacky way (as default arguments).
107-
self.patches.append((lambda name, sym: lambda: patch(name, sym))(name, sym))
95+
self.saved_class_attrs[node, name] = sym
10896

10997
def visit_func_def(self, node: FuncDef) -> None:
11098
if not self.recurse_into_functions:
@@ -199,6 +187,9 @@ def process_lvalue_in_method(self, lvalue: Node) -> None:
199187
# self, since only those can define new attributes.
200188
assert self.type is not None
201189
del self.type.names[lvalue.name]
190+
key = (self.type.defn, lvalue.name)
191+
if key in self.saved_class_attrs:
192+
del self.saved_class_attrs[key]
202193
elif isinstance(lvalue, (TupleExpr, ListExpr)):
203194
for item in lvalue.items:
204195
self.process_lvalue_in_method(item)

mypy/server/update.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@
139139
from mypy.newsemanal.semanal_main import semantic_analysis_for_scc, semantic_analysis_for_targets
140140
from mypy.server.astmerge import merge_asts
141141
from mypy.server.aststrip import strip_target
142-
from mypy.server.aststripnew import strip_target_new
142+
from mypy.server.aststripnew import strip_target_new, SavedAttributes
143143
from mypy.server.deps import get_dependencies_of_target
144144
from mypy.server.target import module_prefix, split_target, trigger_to_target
145145
from mypy.server.trigger import make_trigger, WILDCARD_TAG
@@ -944,18 +944,17 @@ def key(node: FineGrainedDeferredNode) -> int:
944944
manager.errors.clear_errors_in_targets(file_node.path, targets)
945945

946946
# Strip semantic analysis information.
947-
patches = [] # type: List[Callable[[], None]]
947+
saved_attrs = {} # type: SavedAttributes
948948
for deferred in nodes:
949949
processed_targets.append(deferred.node.fullname())
950950
if not manager.options.new_semantic_analyzer:
951951
strip_target(deferred.node)
952952
else:
953-
new_patches = strip_target_new(deferred.node)
954-
patches.extend(new_patches)
953+
strip_target_new(deferred.node, saved_attrs)
955954
if not options.new_semantic_analyzer:
956955
re_analyze_nodes(file_node, nodes, manager, options)
957956
else:
958-
semantic_analysis_for_targets(graph[module_id], nodes, graph, patches)
957+
semantic_analysis_for_targets(graph[module_id], nodes, graph, saved_attrs)
959958
# Merge symbol tables to preserve identities of AST nodes. The file node will remain
960959
# the same, but other nodes may have been recreated with different identities, such as
961960
# NamedTuples defined using assignment statements.

test-data/unit/fine-grained.test

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8928,3 +8928,26 @@ class A(Generic[T]): ...
89288928
[out]
89298929
==
89308930
main:5: error: Missing type parameters for generic type
8931+
8932+
[case testStripNewAnalyzer]
8933+
# flags: --ignore-missing-imports
8934+
import a
8935+
[file a.py]
8936+
from typing import List
8937+
from b import B
8938+
8939+
class A:
8940+
def __init__(self) -> None:
8941+
self.x: List[int] = []
8942+
8943+
def method(self) -> None:
8944+
B()
8945+
self.x = []
8946+
8947+
[file b.py]
8948+
class B: ...
8949+
[delete b.py.2]
8950+
8951+
[builtins fixtures/list.pyi]
8952+
[out]
8953+
==

0 commit comments

Comments
 (0)