Skip to content

Commit 6e08879

Browse files
committed
Prohibit typevars defaulting to something out of scope
1 parent 841db1f commit 6e08879

File tree

4 files changed

+118
-36
lines changed

4 files changed

+118
-36
lines changed

mypy/semanal.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -451,13 +451,13 @@ def __init__(
451451
self.type_stack = []
452452
# Are the namespaces of classes being processed complete?
453453
self.incomplete_type_stack: list[bool] = []
454-
self.tvar_scope = TypeVarLikeScope()
455454
self.function_stack = []
456455
self.block_depth = [0]
457456
self.loop_depth = [0]
458457
self.errors = errors
459458
self.modules = modules
460459
self.msg = MessageBuilder(errors, modules)
460+
self.tvar_scope = TypeVarLikeScope(msg=self.msg)
461461
self.missing_modules = missing_modules
462462
self.missing_names = [set()]
463463
# These namespaces are still in process of being populated. If we encounter a
@@ -859,7 +859,7 @@ def file_context(
859859
self._is_stub_file = file_node.path.lower().endswith(".pyi")
860860
self._is_typeshed_stub_file = file_node.is_typeshed_file(options)
861861
self.globals = file_node.names
862-
self.tvar_scope = TypeVarLikeScope()
862+
self.tvar_scope = TypeVarLikeScope(msg=self.msg)
863863

864864
self.named_tuple_analyzer = NamedTupleAnalyzer(options, self, self.msg)
865865
self.typed_dict_analyzer = TypedDictAnalyzer(options, self, self.msg)
@@ -2404,7 +2404,7 @@ def tvar_defs_from_tvars(
24042404
self.fail(
24052405
message_registry.TYPE_VAR_REDECLARED_IN_NESTED_CLASS.format(name), context
24062406
)
2407-
tvar_def = self.tvar_scope.bind_new(name, tvar_expr)
2407+
tvar_def = self.tvar_scope.bind_new(name, tvar_expr, context)
24082408
if last_tvar_name_with_default is not None and not tvar_def.has_default():
24092409
self.msg.tvar_without_default_type(
24102410
tvar_def.name, last_tvar_name_with_default, context
@@ -2422,19 +2422,18 @@ def get_and_bind_all_tvars(self, type_exprs: list[Expression]) -> list[TypeVarLi
24222422
a simplified version of the logic we use for ClassDef bases. We duplicate
24232423
some amount of code, because it is hard to refactor common pieces.
24242424
"""
2425-
tvars = []
2425+
tvars: dict[str, tuple[TypeVarLikeExpr, Expression]] = {}
24262426
for base_expr in type_exprs:
24272427
try:
24282428
base = self.expr_to_unanalyzed_type(base_expr)
24292429
except TypeTranslationError:
24302430
# This error will be caught later.
24312431
continue
2432-
base_tvars = self.find_type_var_likes(base)
2433-
tvars.extend(base_tvars)
2434-
tvars = remove_dups(tvars) # Variables are defined in order of textual appearance.
2432+
for name, expr in self.find_type_var_likes(base):
2433+
tvars.setdefault(name, (expr, base_expr))
24352434
tvar_defs = []
2436-
for name, tvar_expr in tvars:
2437-
tvar_def = self.tvar_scope.bind_new(name, tvar_expr)
2435+
for name, (tvar_expr, context) in tvars.items():
2436+
tvar_def = self.tvar_scope.bind_new(name, tvar_expr, context)
24382437
tvar_defs.append(tvar_def)
24392438
return tvar_defs
24402439

@@ -7442,7 +7441,7 @@ def analyze_type_expr(self, expr: Expression) -> None:
74427441
# them semantically analyzed, however, if they need to treat it as an expression
74437442
# and not a type. (Which is to say, mypyc needs to do this.) Do the analysis
74447443
# in a fresh tvar scope in order to suppress any errors about using type variables.
7445-
with self.tvar_scope_frame(TypeVarLikeScope()), self.allow_unbound_tvars_set():
7444+
with self.tvar_scope_frame(TypeVarLikeScope(msg=self.msg)), self.allow_unbound_tvars_set():
74467445
expr.accept(self)
74477446

74487447
def type_analyzer(

mypy/tvar_scope.py

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,70 @@
11
from __future__ import annotations
22

3+
from mypy.messages import MessageBuilder
34
from mypy.nodes import (
5+
Context,
46
ParamSpecExpr,
57
SymbolTableNode,
68
TypeVarExpr,
79
TypeVarLikeExpr,
810
TypeVarTupleExpr,
911
)
1012
from mypy.types import (
13+
AnyType,
1114
ParamSpecFlavor,
1215
ParamSpecType,
16+
TrivialSyntheticTypeTranslator,
17+
Type,
18+
TypeAliasType,
19+
TypeOfAny,
1320
TypeVarId,
1421
TypeVarLikeType,
1522
TypeVarTupleType,
1623
TypeVarType,
1724
)
18-
from mypy.typetraverser import TypeTraverserVisitor
1925

2026

21-
class TypeVarLikeNamespaceSetter(TypeTraverserVisitor):
27+
class TypeVarLikeDefaultFixer(TrivialSyntheticTypeTranslator):
2228
"""Set namespace for all TypeVarLikeTypes types."""
2329

24-
def __init__(self, namespace: str) -> None:
25-
self.namespace = namespace
26-
27-
def visit_type_var(self, t: TypeVarType) -> None:
28-
t.id.namespace = self.namespace
29-
super().visit_type_var(t)
30-
31-
def visit_param_spec(self, t: ParamSpecType) -> None:
32-
t.id.namespace = self.namespace
33-
return super().visit_param_spec(t)
34-
35-
def visit_type_var_tuple(self, t: TypeVarTupleType) -> None:
36-
t.id.namespace = self.namespace
37-
super().visit_type_var_tuple(t)
30+
def __init__(
31+
self, scope: TypeVarLikeScope, source_tv: TypeVarLikeExpr, context: Context
32+
) -> None:
33+
self.scope = scope
34+
self.source_tv = source_tv
35+
self.context = context
36+
super().__init__()
37+
38+
def visit_type_var(self, t: TypeVarType) -> Type:
39+
existing = self.scope.get_binding(t.fullname)
40+
if existing is None:
41+
self._report_unbound_tvar(t)
42+
return AnyType(TypeOfAny.from_error)
43+
return existing
44+
45+
def visit_param_spec(self, t: ParamSpecType) -> Type:
46+
existing = self.scope.get_binding(t.fullname)
47+
if existing is None:
48+
self._report_unbound_tvar(t)
49+
return AnyType(TypeOfAny.from_error)
50+
return existing
51+
52+
def visit_type_var_tuple(self, t: TypeVarTupleType) -> Type:
53+
existing = self.scope.get_binding(t.fullname)
54+
if existing is None:
55+
self._report_unbound_tvar(t)
56+
return AnyType(TypeOfAny.from_error)
57+
return existing
58+
59+
def visit_type_alias_type(self, t: TypeAliasType) -> Type:
60+
return t
61+
62+
def _report_unbound_tvar(self, tvar: TypeVarLikeType) -> None:
63+
self.scope.msg.fail(
64+
f"Type variable {tvar.name} referenced in the default"
65+
f" of {self.source_tv.name} is unbound",
66+
self.context,
67+
)
3868

3969

4070
class TypeVarLikeScope:
@@ -49,6 +79,8 @@ def __init__(
4979
is_class_scope: bool = False,
5080
prohibited: TypeVarLikeScope | None = None,
5181
namespace: str = "",
82+
*,
83+
msg: MessageBuilder,
5284
) -> None:
5385
"""Initializer for TypeVarLikeScope
5486
@@ -65,6 +97,7 @@ def __init__(
6597
self.is_class_scope = is_class_scope
6698
self.prohibited = prohibited
6799
self.namespace = namespace
100+
self.msg = msg
68101
if parent is not None:
69102
self.func_id = parent.func_id
70103
self.class_id = parent.class_id
@@ -87,26 +120,34 @@ def allow_binding(self, fullname: str) -> bool:
87120

88121
def method_frame(self, namespace: str) -> TypeVarLikeScope:
89122
"""A new scope frame for binding a method"""
90-
return TypeVarLikeScope(self, False, None, namespace=namespace)
123+
return TypeVarLikeScope(self, False, None, namespace=namespace, msg=self.msg)
91124

92125
def class_frame(self, namespace: str) -> TypeVarLikeScope:
93126
"""A new scope frame for binding a class. Prohibits *this* class's tvars"""
94-
return TypeVarLikeScope(self.get_function_scope(), True, self, namespace=namespace)
127+
return TypeVarLikeScope(
128+
self.get_function_scope(), True, self, namespace=namespace, msg=self.msg
129+
)
95130

96131
def new_unique_func_id(self) -> TypeVarId:
97132
"""Used by plugin-like code that needs to make synthetic generic functions."""
98133
self.func_id -= 1
99134
return TypeVarId(self.func_id)
100135

101-
def bind_new(self, name: str, tvar_expr: TypeVarLikeExpr) -> TypeVarLikeType:
136+
def bind_new(self, name: str, tvar_expr: TypeVarLikeExpr, context: Context) -> TypeVarLikeType:
102137
if self.is_class_scope:
103138
self.class_id += 1
104139
i = self.class_id
105140
else:
106141
self.func_id -= 1
107142
i = self.func_id
108143
namespace = self.namespace
109-
tvar_expr.default.accept(TypeVarLikeNamespaceSetter(namespace))
144+
145+
# Defaults may reference other type variables. That is only valid when the
146+
# referenced variable is already in scope (textually precedes the definition we're
147+
# processing now).
148+
default = tvar_expr.default.accept(
149+
TypeVarLikeDefaultFixer(self, tvar_expr, context=context)
150+
)
110151

111152
if isinstance(tvar_expr, TypeVarExpr):
112153
tvar_def: TypeVarLikeType = TypeVarType(
@@ -115,7 +156,7 @@ def bind_new(self, name: str, tvar_expr: TypeVarLikeExpr) -> TypeVarLikeType:
115156
id=TypeVarId(i, namespace=namespace),
116157
values=tvar_expr.values,
117158
upper_bound=tvar_expr.upper_bound,
118-
default=tvar_expr.default,
159+
default=default,
119160
variance=tvar_expr.variance,
120161
line=tvar_expr.line,
121162
column=tvar_expr.column,
@@ -127,7 +168,7 @@ def bind_new(self, name: str, tvar_expr: TypeVarLikeExpr) -> TypeVarLikeType:
127168
id=TypeVarId(i, namespace=namespace),
128169
flavor=ParamSpecFlavor.BARE,
129170
upper_bound=tvar_expr.upper_bound,
130-
default=tvar_expr.default,
171+
default=default,
131172
line=tvar_expr.line,
132173
column=tvar_expr.column,
133174
)
@@ -138,7 +179,7 @@ def bind_new(self, name: str, tvar_expr: TypeVarLikeExpr) -> TypeVarLikeType:
138179
id=TypeVarId(i, namespace=namespace),
139180
upper_bound=tvar_expr.upper_bound,
140181
tuple_fallback=tvar_expr.tuple_fallback,
141-
default=tvar_expr.default,
182+
default=default,
142183
line=tvar_expr.line,
143184
column=tvar_expr.column,
144185
)

mypy/typeanal.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1561,7 +1561,7 @@ def analyze_callable_type(self, t: UnboundType) -> Type:
15611561
# below happens at very early stage.
15621562
variables = []
15631563
for name, tvar_expr in self.find_type_var_likes(callable_args):
1564-
variables.append(self.tvar_scope.bind_new(name, tvar_expr))
1564+
variables.append(self.tvar_scope.bind_new(name, tvar_expr, t))
15651565
maybe_ret = self.analyze_callable_args_for_paramspec(
15661566
callable_args, ret_type, fallback
15671567
) or self.analyze_callable_args_for_concatenate(
@@ -1833,7 +1833,7 @@ def bind_function_type_variables(
18331833
assert var_node, "Binding for function type variable not found within function"
18341834
var_expr = var_node.node
18351835
assert isinstance(var_expr, TypeVarLikeExpr)
1836-
binding = self.tvar_scope.bind_new(var.name, var_expr)
1836+
binding = self.tvar_scope.bind_new(var.name, var_expr, fun_type)
18371837
defs.append(binding)
18381838
return tuple(defs), has_self_type
18391839
typevars, has_self_type = self.infer_type_variables(fun_type)
@@ -1846,7 +1846,7 @@ def bind_function_type_variables(
18461846
if not self.tvar_scope.allow_binding(tvar.fullname):
18471847
err_msg = message_registry.TYPE_VAR_REDECLARED_IN_NESTED_CLASS.format(name)
18481848
self.fail(err_msg.value, defn, code=err_msg.code)
1849-
binding = self.tvar_scope.bind_new(name, tvar)
1849+
binding = self.tvar_scope.bind_new(name, tvar, fun_type)
18501850
defs.append(binding)
18511851

18521852
return tuple(defs), has_self_type

test-data/unit/check-typevar-defaults.test

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -880,3 +880,45 @@ reveal_type(A1().x) # N: Revealed type is "TypedDict('__main__.TD', {'foo': bui
880880
reveal_type(A2().x) # N: Revealed type is "tuple[builtins.int, fallback=__main__.NT[builtins.int]]"
881881
reveal_type(A3().x) # N: Revealed type is "TypedDict('__main__.TD', {'foo': builtins.int})"
882882
[builtins fixtures/tuple.pyi]
883+
884+
[case testDefaultsApplicationInAliasNoCrash]
885+
# https://github.com/python/mypy/issues/19186
886+
from typing import Generic, TypeVar
887+
from typing_extensions import TypeAlias
888+
889+
T1 = TypeVar("T1")
890+
T2 = TypeVar("T2", default=T1)
891+
892+
Alias: TypeAlias = "MyClass[T1, T2]"
893+
894+
class MyClass(Generic["T1", "T2"]): ...
895+
[builtins fixtures/tuple.pyi]
896+
897+
[case testDefaultsMustBeInScope]
898+
from typing import Generic, TypeVar
899+
900+
T1 = TypeVar("T1")
901+
T2 = TypeVar("T2", default=T1)
902+
T3 = TypeVar("T3", default=T2)
903+
904+
class A(Generic[T1, T2, T3]): ...
905+
reveal_type(A) # N: Revealed type is "def [T1, T2 = T1`1, T3 = T2`2 = T1`1] () -> __main__.A[T1`1, T2`2 = T1`1, T3`3 = T2`2 = T1`1]"
906+
a: A[int]
907+
reveal_type(a) # N: Revealed type is "__main__.A[builtins.int, builtins.int, T1`1]"
908+
909+
class B(Generic[T1, T3]): ... # E: Type variable T2 referenced in the default of T3 is unbound
910+
reveal_type(B) # N: Revealed type is "def [T1, T3 = Any] () -> __main__.B[T1`1, T3`2 = Any]"
911+
b: B[int]
912+
reveal_type(b) # N: Revealed type is "__main__.B[builtins.int, Any]"
913+
914+
class C(Generic[T2]): ... # E: Type variable T1 referenced in the default of T2 is unbound
915+
reveal_type(C) # N: Revealed type is "def [T2 = Any] () -> __main__.C[T2`1 = Any]"
916+
c: C
917+
reveal_type(c) # N: Revealed type is "__main__.C[Any]"
918+
919+
class D(Generic[T2, T1]): ... # E: Type variable T1 referenced in the default of T2 is unbound \
920+
# E: "T1" cannot appear after "T2" in type parameter list because it has no default type
921+
reveal_type(D) # N: Revealed type is "def [T2 = Any, T1 = Any] () -> __main__.D[T2`1 = Any, T1`2 = Any]"
922+
d: D
923+
reveal_type(d) # N: Revealed type is "__main__.D[Any, Any]"
924+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)