Skip to content

Commit b172a6b

Browse files
authored
Fine-grained: Fix refresh of class nested inside function (#4473)
Fix crash on refreshing class nested inside function. Typically this seemed to happen when the base class got triggered. Classes nested within functions aren't supposed to be targets separate from the surrounding function, and this PR effectively adds them to the surrounding target. Previously they were treated as separate targets, which broke some assumptions when looking up targets. Also refactor current target tracking in dependency generation since the logic would otherwise have been pretty messy. The main change is to not include classes defined within functions in target names. Some weird dependencies are still generated for nested classes, but they may be harmless -- extra dependencies should not be a problem. Fixes #4462.
1 parent d436eeb commit b172a6b

File tree

3 files changed

+128
-65
lines changed

3 files changed

+128
-65
lines changed

mypy/server/deps.py

Lines changed: 82 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def get_dependencies_of_target(module_id: str,
116116
"""Get dependencies of a target -- don't recursive into nested targets."""
117117
# TODO: Add tests for this function.
118118
visitor = DependencyVisitor(type_map, python_version)
119-
visitor.enter_file_scope(module_id)
119+
visitor.scope.enter_file(module_id)
120120
if isinstance(target, MypyFile):
121121
# Only get dependencies of the top-level of the module. Don't recurse into
122122
# functions.
@@ -127,25 +127,20 @@ def get_dependencies_of_target(module_id: str,
127127
elif isinstance(target, FuncBase) and target.info:
128128
# It's a method.
129129
# TODO: Methods in nested classes.
130-
visitor.enter_class_scope(target.info)
130+
visitor.scope.enter_class(target.info)
131131
target.accept(visitor)
132-
visitor.leave_scope()
132+
visitor.scope.leave()
133133
else:
134134
target.accept(visitor)
135-
visitor.leave_scope()
135+
visitor.scope.leave()
136136
return visitor.map
137137

138138

139139
class DependencyVisitor(TraverserVisitor):
140140
def __init__(self,
141141
type_map: Dict[Expression, Type],
142142
python_version: Tuple[int, int]) -> None:
143-
# Stack of names of targets being processed. For stack targets we use the
144-
# surrounding module.
145-
self.target_stack = [] # type: List[str]
146-
# Stack of names of targets being processed, including class targets.
147-
self.full_target_stack = [] # type: List[str]
148-
self.scope_stack = [] # type: List[Union[None, TypeInfo, FuncDef]]
143+
self.scope = Scope()
149144
self.type_map = type_map
150145
self.python2 = python_version[0] == 2
151146
self.map = {} # type: Dict[str, Set[str]]
@@ -165,20 +160,14 @@ def __init__(self,
165160
# type variable with value restriction
166161

167162
def visit_mypy_file(self, o: MypyFile) -> None:
168-
self.enter_file_scope(o.fullname())
163+
self.scope.enter_file(o.fullname())
169164
self.is_package_init_file = o.is_package_init_file()
170165
super().visit_mypy_file(o)
171-
self.leave_scope()
166+
self.scope.leave()
172167

173168
def visit_func_def(self, o: FuncDef) -> None:
174-
if not isinstance(self.current_scope(), FuncDef):
175-
# Not a nested function, so create a new target.
176-
new_scope = True
177-
target = self.enter_function_scope(o)
178-
else:
179-
# Treat nested functions as components of the parent function target.
180-
new_scope = False
181-
target = self.current_target()
169+
self.scope.enter_function(o)
170+
target = self.scope.current_target()
182171
if o.type:
183172
if self.is_class and isinstance(o.type, FunctionLike):
184173
signature = bind_self(o.type) # type: Type
@@ -191,15 +180,15 @@ def visit_func_def(self, o: FuncDef) -> None:
191180
for base in non_trivial_bases(o.info):
192181
self.add_dependency(make_trigger(base.fullname() + '.' + o.name()))
193182
super().visit_func_def(o)
194-
if new_scope:
195-
self.leave_scope()
183+
self.scope.leave()
196184

197185
def visit_decorator(self, o: Decorator) -> None:
198186
self.add_dependency(make_trigger(o.func.fullname()))
199187
super().visit_decorator(o)
200188

201189
def visit_class_def(self, o: ClassDef) -> None:
202-
target = self.enter_class_scope(o.info)
190+
self.scope.enter_class(o.info)
191+
target = self.scope.current_full_target()
203192
self.add_dependency(make_trigger(target), target)
204193
old_is_class = self.is_class
205194
self.is_class = True
@@ -226,15 +215,15 @@ def visit_class_def(self, o: ClassDef) -> None:
226215
target=make_trigger(info.fullname() + '.' + name))
227216
self.add_dependency(make_trigger(base_info.fullname() + '.__init__'),
228217
target=make_trigger(info.fullname() + '.__init__'))
229-
self.leave_scope()
218+
self.scope.leave()
230219

231220
def visit_import(self, o: Import) -> None:
232221
for id, as_id in o.ids:
233222
# TODO: as_id
234-
self.add_dependency(make_trigger(id), self.current_target())
223+
self.add_dependency(make_trigger(id), self.scope.current_target())
235224

236225
def visit_import_from(self, o: ImportFrom) -> None:
237-
module_id, _ = correct_relative_import(self.current_module_id(),
226+
module_id, _ = correct_relative_import(self.scope.current_module_id(),
238227
o.relative,
239228
o.id,
240229
self.is_package_init_file)
@@ -260,7 +249,7 @@ def visit_assignment_stmt(self, o: AssignmentStmt) -> None:
260249
elif isinstance(rvalue, CallExpr) and isinstance(rvalue.analyzed, NamedTupleExpr):
261250
# Depend on types of named tuple items.
262251
info = rvalue.analyzed.info
263-
prefix = '%s.%s' % (self.current_full_target(), info.name())
252+
prefix = '%s.%s' % (self.scope.current_full_target(), info.name())
264253
for name, symnode in info.names.items():
265254
if not name.startswith('_') and isinstance(symnode.node, Var):
266255
typ = symnode.node.type
@@ -293,7 +282,8 @@ def process_lvalue(self, lvalue: Expression) -> None:
293282
# global variable.
294283
lvalue_type = self.get_non_partial_lvalue_type(lvalue)
295284
type_triggers = get_type_triggers(lvalue_type)
296-
attr_trigger = make_trigger('%s.%s' % (self.full_target_stack[-1], lvalue.name))
285+
attr_trigger = make_trigger('%s.%s' % (self.scope.current_full_target(),
286+
lvalue.name))
297287
for type_trigger in type_triggers:
298288
self.add_dependency(type_trigger, attr_trigger)
299289
elif isinstance(lvalue, MemberExpr):
@@ -515,7 +505,7 @@ def add_dependency(self, trigger: str, target: Optional[str] = None) -> None:
515505
# anyway.
516506
return
517507
if target is None:
518-
target = self.current_target()
508+
target = self.scope.current_target()
519509
self.map.setdefault(trigger, set()).add(target)
520510

521511
def add_type_dependencies(self, typ: Type, target: Optional[str] = None) -> None:
@@ -569,50 +559,77 @@ def add_iter_dependency(self, node: Expression) -> None:
569559
if typ:
570560
self.add_attribute_dependency(typ, '__iter__')
571561

572-
def current_module_id(self) -> str:
573-
return self.target_stack[0]
574-
575-
def enter_file_scope(self, prefix: str) -> None:
576-
"""Enter a module target scope."""
577-
assert not self.target_stack
578-
self.target_stack.append(prefix)
579-
self.full_target_stack.append(prefix)
580-
self.scope_stack.append(None)
581-
582-
def enter_function_scope(self, fdef: FuncDef) -> str:
583-
"""Enter a function target scope."""
584-
target = '%s.%s' % (self.full_target_stack[-1], fdef.name())
585-
self.target_stack.append(target)
586-
self.full_target_stack.append(target)
587-
self.scope_stack.append(fdef)
588-
return target
589562

590-
def enter_class_scope(self, info: TypeInfo) -> str:
591-
"""Enter a class target scope."""
592-
# Duplicate the previous top non-class target (it can't be a class but since the
593-
# depths of all stacks must agree we need something).
594-
self.target_stack.append(self.target_stack[-1])
595-
full_target = '%s.%s' % (self.full_target_stack[-1], info.name())
596-
self.full_target_stack.append(full_target)
597-
self.scope_stack.append(info)
598-
return full_target
599-
600-
def leave_scope(self) -> None:
601-
"""Leave a target scope."""
602-
self.target_stack.pop()
603-
self.full_target_stack.pop()
604-
self.scope_stack.pop()
563+
class Scope:
564+
"""Track which target we are processing at any given time."""
565+
566+
def __init__(self) -> None:
567+
self.module = None # type: Optional[str]
568+
self.classes = [] # type: List[TypeInfo]
569+
self.function = None # type: Optional[FuncDef]
570+
# Number of nested scopes ignored (that don't get their own separate targets)
571+
self.ignored = 0
572+
573+
def current_module_id(self) -> str:
574+
assert self.module
575+
return self.module
605576

606577
def current_target(self) -> str:
607578
"""Return the current target (non-class; for a class return enclosing module)."""
608-
return self.target_stack[-1]
579+
assert self.module
580+
target = self.module
581+
if self.function:
582+
if self.classes:
583+
target += '.' + '.'.join(c.name() for c in self.classes)
584+
target += '.' + self.function.name()
585+
return target
609586

610587
def current_full_target(self) -> str:
611588
"""Return the current target (may be a class)."""
612-
return self.full_target_stack[-1]
589+
assert self.module
590+
target = self.module
591+
if self.classes:
592+
target += '.' + '.'.join(c.name() for c in self.classes)
593+
if self.function:
594+
target += '.' + self.function.name()
595+
return target
596+
597+
def enter_file(self, prefix: str) -> None:
598+
self.module = prefix
599+
self.classes = []
600+
self.function = None
601+
self.ignored = 0
602+
603+
def enter_function(self, fdef: FuncDef) -> None:
604+
if not self.function:
605+
self.function = fdef
606+
else:
607+
# Nested functions are part of the topmost function target.
608+
self.ignored += 1
613609

614-
def current_scope(self) -> Optional[Node]:
615-
return self.scope_stack[-1]
610+
def enter_class(self, info: TypeInfo) -> None:
611+
"""Enter a class target scope."""
612+
if not self.function:
613+
self.classes.append(info)
614+
else:
615+
# Classes within functions are part of the enclosing function target.
616+
self.ignored += 1
617+
618+
def leave(self) -> None:
619+
"""Leave the innermost scope (can be any kind of scope)."""
620+
if self.ignored:
621+
# Leave a scope that's included in the enclosing target.
622+
self.ignored -= 1
623+
elif self.function:
624+
# Function is always the innermost target.
625+
self.function = None
626+
elif self.classes:
627+
# Leave the innermost class.
628+
self.classes.pop()
629+
else:
630+
# Leave module.
631+
assert self.module
632+
self.module = None
616633

617634

618635
def get_type_triggers(typ: Type) -> List[str]:

test-data/unit/deps-classes.test

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,20 @@ class C:
4949
[out]
5050
<m.C.x> -> m.C.__init__
5151
<m.C> -> m.C
52+
53+
[case testClassNestedWithinFunction]
54+
class C: pass
55+
56+
def f() -> None:
57+
class S1(C): pass
58+
59+
class D:
60+
def g(self) -> None:
61+
class S2(C): pass
62+
[out]
63+
-- TODO: Is it okay to have targets like m.S1@4.__init__?
64+
<m.C.__init__> -> <m.S1@4.__init__>, <m.S2@8.__init__>
65+
<m.C> -> m.C, m.D.g, m.f
66+
<m.D.g> -> m.D.g
67+
<m.D> -> m.D
68+
<m.f> -> m.f

test-data/unit/fine-grained.test

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1534,3 +1534,32 @@ def g() -> None:
15341534
x.f(a)
15351535
[out]
15361536
==
1537+
1538+
[case testRefreshSubclassNestedInFunction1]
1539+
from a import C
1540+
def f() -> None:
1541+
class D(C): pass
1542+
[file a.py]
1543+
class C: pass
1544+
[file a.py.2]
1545+
[out]
1546+
==
1547+
main:1: error: Module 'a' has no attribute 'C'
1548+
1549+
[case testRefreshSubclassNestedInFunction2]
1550+
from a import C
1551+
def f() -> None:
1552+
class D(C):
1553+
def g(self) -> None:
1554+
super().__init__()
1555+
d = D()
1556+
[file a.py]
1557+
class C:
1558+
def __init__(self) -> None: pass
1559+
[file a.py.2]
1560+
class C:
1561+
def __init__(self, x: int) -> None: pass
1562+
[out]
1563+
==
1564+
main:5: error: Too few arguments for "__init__" of "C"
1565+
main:6: error: Too few arguments for "D"

0 commit comments

Comments
 (0)