Skip to content

Commit c44713f

Browse files
authored
New semantic analyzer: fix falling back to outer scope in class body (#6938)
This fixes scoping issue where code like this was rejected: ```py c: C # Forces second semantic analysis pass class X: pass class C: X = X # Should be no error here ``` The approach is to fall back to the outer scope if the definition in the class body is not ready yet, based on the line numbers of the definitions. Additionally generate an error for references to names defined later in a class body. This only addresses class bodies. Local scopes have a similar issue with references to undefined names. I'm planning to address that in a separate PR. The basic fix is very simple, but various other changes were required to support multi-line definitions and certain edge cases. Work towards #6303.
1 parent 37cc2ea commit c44713f

File tree

6 files changed

+166
-37
lines changed

6 files changed

+166
-37
lines changed

mypy/newsemanal/semanal.py

+71-32
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ class NewSemanticAnalyzer(NodeVisitor[None],
222222
# not be found in phase 1, for example due to * imports.
223223
errors = None # type: Errors # Keeps track of generated errors
224224
plugin = None # type: Plugin # Mypy plugin for special casing of library features
225+
statement = None # type: Node # Statement/definition being analyzed
225226

226227
def __init__(self,
227228
modules: Dict[str, MypyFile],
@@ -510,6 +511,7 @@ def file_context(self,
510511
#
511512

512513
def visit_func_def(self, defn: FuncDef) -> None:
514+
self.statement = defn
513515
defn.is_conditional = self.block_depth[-1] > 0
514516

515517
# Set full names even for those definitionss that aren't added
@@ -629,6 +631,7 @@ def update_function_type_variables(self, fun_type: CallableType, defn: FuncItem)
629631
fun_type.variables = a.bind_function_type_variables(fun_type, defn)
630632

631633
def visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
634+
self.statement = defn
632635
self.add_function_to_symbol_table(defn)
633636

634637
if not self.recurse_into_functions:
@@ -904,6 +907,7 @@ def check_function_signature(self, fdef: FuncItem) -> None:
904907
self.fail('Type signature has too many arguments', fdef, blocker=True)
905908

906909
def visit_decorator(self, dec: Decorator) -> None:
910+
self.statement = dec
907911
dec.func.is_conditional = self.block_depth[-1] > 0
908912
if not dec.is_overload:
909913
self.add_symbol(dec.name(), dec, dec)
@@ -977,6 +981,7 @@ def check_decorated_function_is_method(self, decorator: str,
977981
#
978982

979983
def visit_class_def(self, defn: ClassDef) -> None:
984+
self.statement = defn
980985
with self.tvar_scope_frame(self.tvar_scope.class_frame()):
981986
self.analyze_class(defn)
982987

@@ -1841,6 +1846,7 @@ def visit_import_all(self, i: ImportAll) -> None:
18411846
#
18421847

18431848
def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
1849+
self.statement = s
18441850
tag = self.track_incomplete_refs()
18451851
s.rvalue.accept(self)
18461852
if self.found_incomplete_ref(tag) or self.should_wait_rhs(s.rvalue):
@@ -2403,7 +2409,9 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
24032409
alias_node.normalized = rvalue.node.normalized
24042410
return True
24052411

2406-
def analyze_lvalue(self, lval: Lvalue, nested: bool = False,
2412+
def analyze_lvalue(self,
2413+
lval: Lvalue,
2414+
nested: bool = False,
24072415
explicit_type: bool = False,
24082416
is_final: bool = False) -> None:
24092417
"""Analyze an lvalue or assignment target.
@@ -2484,7 +2492,6 @@ def analyze_name_lvalue(self,
24842492
self.fail("Cannot redefine an existing name as final", lvalue)
24852493
else:
24862494
self.make_name_lvalue_point_to_existing_def(lvalue, explicit_type, is_final)
2487-
# TODO: Special case local '_' assignment to always infer 'Any'
24882495

24892496
def is_final_redefinition(self, kind: int, name: str) -> bool:
24902497
if kind == GDEF:
@@ -2540,30 +2547,25 @@ def make_name_lvalue_point_to_existing_def(
25402547
"""Update an lvalue to point to existing definition in the same scope.
25412548
25422549
Arguments are similar to "analyze_lvalue".
2543-
"""
2544-
# Assume that an existing name exists. Try to find the original definition.
2545-
global_def = self.globals.get(lval.name)
2546-
if self.locals:
2547-
locals_last = self.locals[-1]
2548-
if locals_last:
2549-
local_def = locals_last.get(lval.name)
2550-
else:
2551-
local_def = None
2552-
else:
2553-
local_def = None
2554-
type_def = self.type.names.get(lval.name) if self.type else None
2555-
2556-
original_def = global_def or local_def or type_def
25572550
2558-
# Redefining an existing name with final is always an error.
2551+
Assume that an existing name exists.
2552+
"""
25592553
if is_final:
2554+
# Redefining an existing name with final is always an error.
25602555
self.fail("Cannot redefine an existing name as final", lval)
2556+
original_def = self.lookup(lval.name, lval, suppress_errors=True)
2557+
if original_def is None and self.type and not self.is_func_scope():
2558+
# Workaround to allow "x, x = ..." in class body.
2559+
original_def = self.type.get(lval.name)
25612560
if explicit_type:
2562-
# Don't re-bind types
2561+
# Don't re-bind if there is a type annotation.
25632562
self.name_already_defined(lval.name, lval, original_def)
25642563
else:
25652564
# Bind to an existing name.
2566-
lval.accept(self)
2565+
if original_def:
2566+
self.bind_name_expr(lval, original_def)
2567+
else:
2568+
self.name_not_defined(lval.name, lval)
25672569
self.check_lvalue_validity(lval.node, lval)
25682570

25692571
def analyze_tuple_or_list_lvalue(self, lval: TupleExpr,
@@ -2984,7 +2986,7 @@ def process_module_assignment(self, lvals: List[Lvalue], rval: Expression,
29842986
# respect explicitly annotated type
29852987
if (isinstance(lval.node, Var) and lval.node.type is not None):
29862988
continue
2987-
lnode = self.lookup(lval.name, ctx)
2989+
lnode = self.current_symbol_table().get(lval.name)
29882990
if lnode:
29892991
if isinstance(lnode.node, MypyFile) and lnode.node is not rnode.node:
29902992
self.fail(
@@ -3020,42 +3022,49 @@ def visit_block_maybe(self, b: Optional[Block]) -> None:
30203022
self.visit_block(b)
30213023

30223024
def visit_expression_stmt(self, s: ExpressionStmt) -> None:
3025+
self.statement = s
30233026
s.expr.accept(self)
30243027

30253028
def visit_return_stmt(self, s: ReturnStmt) -> None:
3029+
self.statement = s
30263030
if not self.is_func_scope():
30273031
self.fail("'return' outside function", s)
30283032
if s.expr:
30293033
s.expr.accept(self)
30303034

30313035
def visit_raise_stmt(self, s: RaiseStmt) -> None:
3036+
self.statement = s
30323037
if s.expr:
30333038
s.expr.accept(self)
30343039
if s.from_expr:
30353040
s.from_expr.accept(self)
30363041

30373042
def visit_assert_stmt(self, s: AssertStmt) -> None:
3043+
self.statement = s
30383044
if s.expr:
30393045
s.expr.accept(self)
30403046
if s.msg:
30413047
s.msg.accept(self)
30423048

30433049
def visit_operator_assignment_stmt(self,
30443050
s: OperatorAssignmentStmt) -> None:
3051+
self.statement = s
30453052
s.lvalue.accept(self)
30463053
s.rvalue.accept(self)
30473054
if (isinstance(s.lvalue, NameExpr) and s.lvalue.name == '__all__' and
30483055
s.lvalue.kind == GDEF and isinstance(s.rvalue, (ListExpr, TupleExpr))):
30493056
self.add_exports(s.rvalue.items)
30503057

30513058
def visit_while_stmt(self, s: WhileStmt) -> None:
3059+
self.statement = s
30523060
s.expr.accept(self)
30533061
self.loop_depth += 1
30543062
s.body.accept(self)
30553063
self.loop_depth -= 1
30563064
self.visit_block_maybe(s.else_body)
30573065

30583066
def visit_for_stmt(self, s: ForStmt) -> None:
3067+
self.statement = s
30593068
s.expr.accept(self)
30603069

30613070
# Bind index variables and check if they define new names.
@@ -3076,21 +3085,25 @@ def visit_for_stmt(self, s: ForStmt) -> None:
30763085
self.visit_block_maybe(s.else_body)
30773086

30783087
def visit_break_stmt(self, s: BreakStmt) -> None:
3088+
self.statement = s
30793089
if self.loop_depth == 0:
30803090
self.fail("'break' outside loop", s, True, blocker=True)
30813091

30823092
def visit_continue_stmt(self, s: ContinueStmt) -> None:
3093+
self.statement = s
30833094
if self.loop_depth == 0:
30843095
self.fail("'continue' outside loop", s, True, blocker=True)
30853096

30863097
def visit_if_stmt(self, s: IfStmt) -> None:
3098+
self.statement = s
30873099
infer_reachability_of_if_statement(s, self.options)
30883100
for i in range(len(s.expr)):
30893101
s.expr[i].accept(self)
30903102
self.visit_block(s.body[i])
30913103
self.visit_block_maybe(s.else_body)
30923104

30933105
def visit_try_stmt(self, s: TryStmt) -> None:
3106+
self.statement = s
30943107
self.analyze_try_stmt(s, self)
30953108

30963109
def analyze_try_stmt(self, s: TryStmt, visitor: NodeVisitor[None]) -> None:
@@ -3107,6 +3120,7 @@ def analyze_try_stmt(self, s: TryStmt, visitor: NodeVisitor[None]) -> None:
31073120
s.finally_body.accept(visitor)
31083121

31093122
def visit_with_stmt(self, s: WithStmt) -> None:
3123+
self.statement = s
31103124
types = [] # type: List[Type]
31113125

31123126
if s.unanalyzed_type:
@@ -3151,6 +3165,7 @@ def visit_with_stmt(self, s: WithStmt) -> None:
31513165
self.visit_block(s.body)
31523166

31533167
def visit_del_stmt(self, s: DelStmt) -> None:
3168+
self.statement = s
31543169
s.expr.accept(self)
31553170
if not self.is_valid_del_target(s.expr):
31563171
self.fail('Invalid delete target', s)
@@ -3164,12 +3179,14 @@ def is_valid_del_target(self, s: Expression) -> bool:
31643179
return False
31653180

31663181
def visit_global_decl(self, g: GlobalDecl) -> None:
3182+
self.statement = g
31673183
for name in g.names:
31683184
if name in self.nonlocal_decls[-1]:
31693185
self.fail("Name '{}' is nonlocal and global".format(name), g)
31703186
self.global_decls[-1].add(name)
31713187

31723188
def visit_nonlocal_decl(self, d: NonlocalDecl) -> None:
3189+
self.statement = d
31733190
if not self.is_func_scope():
31743191
self.fail("nonlocal declaration not allowed at module level", d)
31753192
else:
@@ -3189,12 +3206,14 @@ def visit_nonlocal_decl(self, d: NonlocalDecl) -> None:
31893206
self.nonlocal_decls[-1].add(name)
31903207

31913208
def visit_print_stmt(self, s: PrintStmt) -> None:
3209+
self.statement = s
31923210
for arg in s.args:
31933211
arg.accept(self)
31943212
if s.target:
31953213
s.target.accept(self)
31963214

31973215
def visit_exec_stmt(self, s: ExecStmt) -> None:
3216+
self.statement = s
31983217
s.expr.accept(self)
31993218
if s.globals:
32003219
s.globals.accept(self)
@@ -3208,15 +3227,19 @@ def visit_exec_stmt(self, s: ExecStmt) -> None:
32083227
def visit_name_expr(self, expr: NameExpr) -> None:
32093228
n = self.lookup(expr.name, expr)
32103229
if n:
3211-
if isinstance(n.node, TypeVarExpr) and self.tvar_scope.get_binding(n):
3212-
self.fail("'{}' is a type variable and only valid in type "
3213-
"context".format(expr.name), expr)
3214-
elif isinstance(n.node, PlaceholderNode):
3215-
self.process_placeholder(expr.name, 'name', expr)
3216-
else:
3217-
expr.kind = n.kind
3218-
expr.node = n.node
3219-
expr.fullname = n.fullname
3230+
self.bind_name_expr(expr, n)
3231+
3232+
def bind_name_expr(self, expr: NameExpr, sym: SymbolTableNode) -> None:
3233+
"""Bind name expression to a symbol table node."""
3234+
if isinstance(sym.node, TypeVarExpr) and self.tvar_scope.get_binding(sym):
3235+
self.fail("'{}' is a type variable and only valid in type "
3236+
"context".format(expr.name), expr)
3237+
elif isinstance(sym.node, PlaceholderNode):
3238+
self.process_placeholder(expr.name, 'name', expr)
3239+
else:
3240+
expr.kind = sym.kind
3241+
expr.node = sym.node
3242+
expr.fullname = sym.fullname
32203243

32213244
def visit_super_expr(self, expr: SuperExpr) -> None:
32223245
if not self.type:
@@ -3731,9 +3754,25 @@ def lookup(self, name: str, ctx: Context,
37313754
if self.type and not self.is_func_scope() and name in self.type.names:
37323755
node = self.type.names[name]
37333756
if not node.implicit:
3734-
return node
3735-
implicit_name = True
3736-
implicit_node = node
3757+
# Only allow access to class attributes textually after
3758+
# the definition, so that it's possible to fall back to the
3759+
# outer scope. Example:
3760+
#
3761+
# class X: ...
3762+
#
3763+
# class C:
3764+
# X = X # Initializer refers to outer scope
3765+
#
3766+
# Nested classes are an exception, since we want to support
3767+
# arbitrary forward references in type annotations.
3768+
if (node.node is None
3769+
or node.node.line < self.statement.line
3770+
or isinstance(node.node, TypeInfo)):
3771+
return node
3772+
else:
3773+
# Defined through self.x assignment
3774+
implicit_name = True
3775+
implicit_node = node
37373776
# 3. Local (function) scopes
37383777
for table in reversed(self.locals):
37393778
if table is not None and name in table:

mypy/newsemanal/semanal_namedtuple.py

+1
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,7 @@ def add_method(funcname: str,
431431
func.is_class = is_classmethod
432432
func.type = set_callable_name(signature, func)
433433
func._fullname = info.fullname() + '.' + funcname
434+
func.line = line
434435
if is_classmethod:
435436
v = Var(funcname, func.type)
436437
v.is_classmethod = True

mypy/newsemanal/typeanal.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -757,7 +757,7 @@ def bind_function_type_variables(self,
757757
"""Find the type variables of the function type and bind them in our tvar_scope"""
758758
if fun_type.variables:
759759
for var in fun_type.variables:
760-
var_node = self.lookup_qualified(var.name, var)
760+
var_node = self.lookup_qualified(var.name, defn)
761761
assert var_node, "Binding for function type variable not found within function"
762762
var_expr = var_node.node
763763
assert isinstance(var_expr, TypeVarExpr)

test-data/unit/check-basic.test

-2
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,6 @@ a = __file__ # type: int # E: Incompatible types in assignment (expression has
237237

238238

239239
[case testLocalVariableShadowing]
240-
241240
a = None # type: A
242241
if int():
243242
a = B() # E: Incompatible types in assignment (expression has type "B", variable has type "A")
@@ -254,7 +253,6 @@ class A: pass
254253
class B: pass
255254

256255
[case testGlobalDefinedInBlockWithType]
257-
258256
class A: pass
259257
while A:
260258
a = None # type: A

test-data/unit/check-inference.test

+1-1
Original file line numberDiff line numberDiff line change
@@ -2534,7 +2534,7 @@ def foo() -> None:
25342534
_ = 0 # E: Incompatible types in assignment (expression has type "int", variable has type "Callable[[], Any]")
25352535
[builtins fixtures/module.pyi]
25362536

2537-
[case testUnusedTargetNotClass2]
2537+
[case testUnderscoreClass]
25382538
def foo() -> None:
25392539
class _:
25402540
pass

0 commit comments

Comments
 (0)