From ab5c9789ccff911bb40081c142b09a993525354d Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 6 Feb 2025 11:48:18 +0000 Subject: [PATCH 1/3] Fix crashes on incorectly detected recursive aliases --- mypy/semanal.py | 30 +++++++++++++++---- test-data/unit/check-newsemanal.test | 16 ++++++++++ test-data/unit/check-python312.test | 41 ++++++++++++++++++++++++++ test-data/unit/check-type-aliases.test | 28 ++++++++++++++++++ 4 files changed, 110 insertions(+), 5 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index d769178dc2985..d81512ad0685c 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -4194,7 +4194,16 @@ def disable_invalid_recursive_aliases( ) -> None: """Prohibit and fix recursive type aliases that are invalid/unsupported.""" messages = [] - if is_invalid_recursive_alias({current_node}, current_node.target): + if ( + isinstance(current_node.target, TypeAliasType) + and current_node.target.alias is current_node + ): + # We want to have consistent error messages, but not calling name_not_defined(), + # since it will do a bunch of unrelated things we don't want here. + messages.append( + f'Cannot resolve name "{current_node.name}" (possible cyclic definition)' + ) + elif is_invalid_recursive_alias({current_node}, current_node.target): target = ( "tuple" if isinstance(get_proper_type(current_node.target), TupleType) else "union" ) @@ -6315,13 +6324,24 @@ class C: if self.statement is None: # Assume it's fine -- don't have enough context to check return True - return ( + if ( node is None or self.is_textually_before_statement(node) or not self.is_defined_in_current_module(node.fullname) - or isinstance(node, (TypeInfo, TypeAlias)) - or (isinstance(node, PlaceholderNode) and node.becomes_typeinfo) - ) + ): + return True + if ( + isinstance(node, (TypeInfo, TypeAlias)) + or isinstance(node, PlaceholderNode) + and node.becomes_typeinfo + ): + # Allow forward references to classes/type aliases (see docstring), but + # a forward reference should never shadow an existing regular reference. + if node.name not in self.globals: + return True + global_node = self.globals[node.name] + return not global_node.node or not self.is_textually_before_statement(global_node.node) + return False def is_textually_before_statement(self, node: SymbolNode) -> bool: """Check if a node is defined textually before the current statement diff --git a/test-data/unit/check-newsemanal.test b/test-data/unit/check-newsemanal.test index d5101e2e25f37..1eafd462aa515 100644 --- a/test-data/unit/check-newsemanal.test +++ b/test-data/unit/check-newsemanal.test @@ -3140,6 +3140,22 @@ from typing import Final x: Final = 0 x = x # E: Cannot assign to final name "x" +[case testNewAnalyzerIdentityAssignmentClassImplicit] +class C: ... +class A: + C = C[str] # E: "C" expects no type arguments, but 1 given +[builtins fixtures/tuple.pyi] + +[case testNewAnalyzerIdentityAssignmentClassExplicit] +from typing_extensions import TypeAlias + +class A: + C: TypeAlias = C +class C: ... +c: A.C +reveal_type(c) # N: Revealed type is "__main__.C" +[builtins fixtures/tuple.pyi] + [case testNewAnalyzerClassPropertiesInAllScopes] from abc import abstractmethod, ABCMeta diff --git a/test-data/unit/check-python312.test b/test-data/unit/check-python312.test index 80cceea85581f..f1b2f38a5af2c 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -1988,3 +1988,44 @@ bis: RK_functionBIS = ff res: int = bis(1.0, 2, 3) [builtins fixtures/tuple.pyi] [typing fixtures/typing-full.pyi] + +[case testPEP695TypeAliasNotReadyClass] +class CustomizeResponse: + related_resources: "ResourceRule" + +class ResourceRule: pass + +class DecoratorController: + type CustomizeResponse = CustomizeResponse + +x: DecoratorController.CustomizeResponse +reveal_type(x.related_resources) # N: Revealed type is "__main__.ResourceRule" +[builtins fixtures/tuple.pyi] + +[case testPEP695TypeAliasRecursiveOuterClass] +class A: + type X = X # E: Cannot resolve name "X" (possible cyclic definition) +class X: ... + +class Y: ... +class B: + type Y = Y # OK + +x: A.X +reveal_type(x) # N: Revealed type is "Any" +y: B.Y +reveal_type(y) # N: Revealed type is "__main__.Y" +[builtins fixtures/tuple.pyi] + +[case testPEP695TypeAliasRecursiveInvalid] +type X = X # E: Cannot resolve name "X" (possible cyclic definition) +type Z = Z[int] # E: Cannot resolve name "Z" (possible cyclic definition) +def foo() -> None: + type X = X # OK, refers to outer (invalid) X + x: X + reveal_type(x) # N: Revealed type is "Any" + type Y = Y # E: Cannot resolve name "Y" (possible cyclic definition) \ + # N: Recursive types are not allowed at function scope +class Z: ... # E: Name "Z" already defined on line 2 +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] diff --git a/test-data/unit/check-type-aliases.test b/test-data/unit/check-type-aliases.test index 9527c85ed26a7..c5915176a5ffb 100644 --- a/test-data/unit/check-type-aliases.test +++ b/test-data/unit/check-type-aliases.test @@ -1286,3 +1286,31 @@ x2: Explicit[str] # E: Bad number of arguments for type alias, expected 0, give assert_type(x1, Callable[..., Any]) assert_type(x2, Callable[..., Any]) [builtins fixtures/tuple.pyi] + +[case testExplicitTypeAliasToSameNameOuterProhibited] +from typing import TypeVar, Generic +from typing_extensions import TypeAlias + +T = TypeVar("T") +class Foo(Generic[T]): + bar: Bar[T] + +class Bar(Generic[T]): + Foo: TypeAlias = Foo[T] # E: Can't use bound type variable "T" to define generic alias +[builtins fixtures/tuple.pyi] + +[case testExplicitTypeAliasToSameNameOuterAllowed] +from typing import TypeVar, Generic +from typing_extensions import TypeAlias + +T = TypeVar("T") +class Foo(Generic[T]): + bar: Bar[T] + +U = TypeVar("U") +class Bar(Generic[T]): + Foo: TypeAlias = Foo[U] + var: Foo[T] +x: Bar[int] +reveal_type(x.var.bar) # N: Revealed type is "__main__.Bar[builtins.int]" +[builtins fixtures/tuple.pyi] From 0621dc2b424ccf2407773e392277c9bf3a2361cf Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 6 Feb 2025 23:22:54 +0000 Subject: [PATCH 2/3] Use a bit more consistent logic --- mypy/semanal.py | 6 +++++- test-data/unit/check-python312.test | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index d81512ad0685c..3c0746f5b3d88 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -6340,7 +6340,11 @@ class C: if node.name not in self.globals: return True global_node = self.globals[node.name] - return not global_node.node or not self.is_textually_before_statement(global_node.node) + return ( + isinstance(global_node.node, (TypeInfo, TypeAlias)) + or isinstance(global_node.node, PlaceholderNode) + and global_node.node.becomes_typeinfo + ) return False def is_textually_before_statement(self, node: SymbolNode) -> bool: diff --git a/test-data/unit/check-python312.test b/test-data/unit/check-python312.test index f1b2f38a5af2c..ba4104a50048e 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -2004,15 +2004,15 @@ reveal_type(x.related_resources) # N: Revealed type is "__main__.ResourceRule" [case testPEP695TypeAliasRecursiveOuterClass] class A: - type X = X # E: Cannot resolve name "X" (possible cyclic definition) + type X = X class X: ... class Y: ... class B: - type Y = Y # OK + type Y = Y x: A.X -reveal_type(x) # N: Revealed type is "Any" +reveal_type(x) # N: Revealed type is "__main__.X" y: B.Y reveal_type(y) # N: Revealed type is "__main__.Y" [builtins fixtures/tuple.pyi] From 7538a645d99f05d473993ebd19a0cbaf6468d8bc Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 6 Feb 2025 23:44:28 +0000 Subject: [PATCH 3/3] Refactor --- mypy/semanal.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 3c0746f5b3d88..86234d100c277 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -6330,23 +6330,20 @@ class C: or not self.is_defined_in_current_module(node.fullname) ): return True - if ( - isinstance(node, (TypeInfo, TypeAlias)) - or isinstance(node, PlaceholderNode) - and node.becomes_typeinfo - ): + if self.is_type_like(node): # Allow forward references to classes/type aliases (see docstring), but # a forward reference should never shadow an existing regular reference. if node.name not in self.globals: return True global_node = self.globals[node.name] - return ( - isinstance(global_node.node, (TypeInfo, TypeAlias)) - or isinstance(global_node.node, PlaceholderNode) - and global_node.node.becomes_typeinfo - ) + return not self.is_type_like(global_node.node) return False + def is_type_like(self, node: SymbolNode | None) -> bool: + return isinstance(node, (TypeInfo, TypeAlias)) or ( + isinstance(node, PlaceholderNode) and node.becomes_typeinfo + ) + def is_textually_before_statement(self, node: SymbolNode) -> bool: """Check if a node is defined textually before the current statement