Skip to content

Commit 75b5604

Browse files
authored
Fix crashes on incorectly detected recursive aliases (#18625)
Fixes #18505 Fixes #16757 Fixing the crash is trivial, we simply give an error on something like `type X = X`, instead of crashing. But then I looked at what people actually want to do, they want to create an alias to something currently in scope (not a meaningless no-op alias). Right now we special-case classes/aliases to allow forward references (including recursive type aliases). However, I don't think we have any clear "scoping rules" for forward references. For example: ```python class C: Y = X class X: ... class X: ... ``` where `Y` should point to, `__main__.X` or `__main__.C.X`? Moreover, before this PR forward references can take precedence over real references: ```python class X: ... class C: Y = X # this resolves to __main__.C.X class X: ... ``` After some thinking I found this is not something I can fix in a simple PR. So instead I do just two things here: * Fix the actual crashes (and other potential similar crashes). * Add minimal change to accommodate the typical use case.
1 parent c8fad3f commit 75b5604

File tree

4 files changed

+110
-4
lines changed

4 files changed

+110
-4
lines changed

mypy/semanal.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4194,7 +4194,16 @@ def disable_invalid_recursive_aliases(
41944194
) -> None:
41954195
"""Prohibit and fix recursive type aliases that are invalid/unsupported."""
41964196
messages = []
4197-
if is_invalid_recursive_alias({current_node}, current_node.target):
4197+
if (
4198+
isinstance(current_node.target, TypeAliasType)
4199+
and current_node.target.alias is current_node
4200+
):
4201+
# We want to have consistent error messages, but not calling name_not_defined(),
4202+
# since it will do a bunch of unrelated things we don't want here.
4203+
messages.append(
4204+
f'Cannot resolve name "{current_node.name}" (possible cyclic definition)'
4205+
)
4206+
elif is_invalid_recursive_alias({current_node}, current_node.target):
41984207
target = (
41994208
"tuple" if isinstance(get_proper_type(current_node.target), TupleType) else "union"
42004209
)
@@ -6315,12 +6324,24 @@ class C:
63156324
if self.statement is None:
63166325
# Assume it's fine -- don't have enough context to check
63176326
return True
6318-
return (
6327+
if (
63196328
node is None
63206329
or self.is_textually_before_statement(node)
63216330
or not self.is_defined_in_current_module(node.fullname)
6322-
or isinstance(node, (TypeInfo, TypeAlias))
6323-
or (isinstance(node, PlaceholderNode) and node.becomes_typeinfo)
6331+
):
6332+
return True
6333+
if self.is_type_like(node):
6334+
# Allow forward references to classes/type aliases (see docstring), but
6335+
# a forward reference should never shadow an existing regular reference.
6336+
if node.name not in self.globals:
6337+
return True
6338+
global_node = self.globals[node.name]
6339+
return not self.is_type_like(global_node.node)
6340+
return False
6341+
6342+
def is_type_like(self, node: SymbolNode | None) -> bool:
6343+
return isinstance(node, (TypeInfo, TypeAlias)) or (
6344+
isinstance(node, PlaceholderNode) and node.becomes_typeinfo
63246345
)
63256346

63266347
def is_textually_before_statement(self, node: SymbolNode) -> bool:

test-data/unit/check-newsemanal.test

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3140,6 +3140,22 @@ from typing import Final
31403140
x: Final = 0
31413141
x = x # E: Cannot assign to final name "x"
31423142

3143+
[case testNewAnalyzerIdentityAssignmentClassImplicit]
3144+
class C: ...
3145+
class A:
3146+
C = C[str] # E: "C" expects no type arguments, but 1 given
3147+
[builtins fixtures/tuple.pyi]
3148+
3149+
[case testNewAnalyzerIdentityAssignmentClassExplicit]
3150+
from typing_extensions import TypeAlias
3151+
3152+
class A:
3153+
C: TypeAlias = C
3154+
class C: ...
3155+
c: A.C
3156+
reveal_type(c) # N: Revealed type is "__main__.C"
3157+
[builtins fixtures/tuple.pyi]
3158+
31433159
[case testNewAnalyzerClassPropertiesInAllScopes]
31443160
from abc import abstractmethod, ABCMeta
31453161

test-data/unit/check-python312.test

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1988,3 +1988,44 @@ bis: RK_functionBIS = ff
19881988
res: int = bis(1.0, 2, 3)
19891989
[builtins fixtures/tuple.pyi]
19901990
[typing fixtures/typing-full.pyi]
1991+
1992+
[case testPEP695TypeAliasNotReadyClass]
1993+
class CustomizeResponse:
1994+
related_resources: "ResourceRule"
1995+
1996+
class ResourceRule: pass
1997+
1998+
class DecoratorController:
1999+
type CustomizeResponse = CustomizeResponse
2000+
2001+
x: DecoratorController.CustomizeResponse
2002+
reveal_type(x.related_resources) # N: Revealed type is "__main__.ResourceRule"
2003+
[builtins fixtures/tuple.pyi]
2004+
2005+
[case testPEP695TypeAliasRecursiveOuterClass]
2006+
class A:
2007+
type X = X
2008+
class X: ...
2009+
2010+
class Y: ...
2011+
class B:
2012+
type Y = Y
2013+
2014+
x: A.X
2015+
reveal_type(x) # N: Revealed type is "__main__.X"
2016+
y: B.Y
2017+
reveal_type(y) # N: Revealed type is "__main__.Y"
2018+
[builtins fixtures/tuple.pyi]
2019+
2020+
[case testPEP695TypeAliasRecursiveInvalid]
2021+
type X = X # E: Cannot resolve name "X" (possible cyclic definition)
2022+
type Z = Z[int] # E: Cannot resolve name "Z" (possible cyclic definition)
2023+
def foo() -> None:
2024+
type X = X # OK, refers to outer (invalid) X
2025+
x: X
2026+
reveal_type(x) # N: Revealed type is "Any"
2027+
type Y = Y # E: Cannot resolve name "Y" (possible cyclic definition) \
2028+
# N: Recursive types are not allowed at function scope
2029+
class Z: ... # E: Name "Z" already defined on line 2
2030+
[builtins fixtures/tuple.pyi]
2031+
[typing fixtures/typing-full.pyi]

test-data/unit/check-type-aliases.test

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1286,3 +1286,31 @@ x2: Explicit[str] # E: Bad number of arguments for type alias, expected 0, give
12861286
assert_type(x1, Callable[..., Any])
12871287
assert_type(x2, Callable[..., Any])
12881288
[builtins fixtures/tuple.pyi]
1289+
1290+
[case testExplicitTypeAliasToSameNameOuterProhibited]
1291+
from typing import TypeVar, Generic
1292+
from typing_extensions import TypeAlias
1293+
1294+
T = TypeVar("T")
1295+
class Foo(Generic[T]):
1296+
bar: Bar[T]
1297+
1298+
class Bar(Generic[T]):
1299+
Foo: TypeAlias = Foo[T] # E: Can't use bound type variable "T" to define generic alias
1300+
[builtins fixtures/tuple.pyi]
1301+
1302+
[case testExplicitTypeAliasToSameNameOuterAllowed]
1303+
from typing import TypeVar, Generic
1304+
from typing_extensions import TypeAlias
1305+
1306+
T = TypeVar("T")
1307+
class Foo(Generic[T]):
1308+
bar: Bar[T]
1309+
1310+
U = TypeVar("U")
1311+
class Bar(Generic[T]):
1312+
Foo: TypeAlias = Foo[U]
1313+
var: Foo[T]
1314+
x: Bar[int]
1315+
reveal_type(x.var.bar) # N: Revealed type is "__main__.Bar[builtins.int]"
1316+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)