diff --git a/mypy/semanal.py b/mypy/semanal.py index 52861230af37..ed989e0d80c9 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -51,7 +51,7 @@ from __future__ import annotations import re -from collections.abc import Callable, Collection, Iterable, Iterator +from collections.abc import Callable, Collection, Iterable, Iterator, Sequence from contextlib import contextmanager from typing import Any, Final, TypeAlias as _TypeAlias, TypeGuard, TypeVar, cast from typing_extensions import assert_never @@ -60,6 +60,7 @@ from mypy.constant_fold import constant_fold_expr from mypy.errorcodes import PROPERTY_DECORATOR, ErrorCode from mypy.errors import Errors, report_internal_error +from mypy.expandtype import expand_type from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type from mypy.message_registry import CANNOT_DECLARE_TYPE_OF_SPECIAL_FORM, ErrorMessage from mypy.messages import ( @@ -1198,20 +1199,43 @@ def is_expected_self_type(self, typ: Type, is_classmethod: bool) -> bool: return self.is_expected_self_type(typ.item, is_classmethod=False) if isinstance(typ, UnboundType): sym = self.lookup_qualified(typ.name, typ, suppress_errors=True) - if sym is not None and sym.fullname in TYPE_NAMES and typ.args: + if sym is None or not typ.args: + return False + if sym.fullname in TYPE_NAMES: return self.is_expected_self_type(typ.args[0], is_classmethod=False) - if sym is not None and sym.fullname in ANNOTATED_TYPE_NAMES and typ.args: + if sym.fullname in ANNOTATED_TYPE_NAMES: return self.is_expected_self_type(typ.args[0], is_classmethod=True) + expanded = self.expand_type_alias_for_self_check(sym.node, typ.args) + if expanded is not None: + return self.is_expected_self_type(expanded, is_classmethod=True) return False + if isinstance(typ, TypeVarType): return typ == self.type.self_type if isinstance(typ, UnboundType): sym = self.lookup_qualified(typ.name, typ, suppress_errors=True) - if sym is not None and sym.fullname in ANNOTATED_TYPE_NAMES and typ.args: + if sym is None: + return False + if sym.fullname in ANNOTATED_TYPE_NAMES and typ.args: return self.is_expected_self_type(typ.args[0], is_classmethod=False) - return sym is not None and sym.fullname in SELF_TYPE_NAMES + expanded = self.expand_type_alias_for_self_check(sym.node, typ.args) + if expanded is not None: + return self.is_expected_self_type(expanded, is_classmethod=False) + return sym.fullname in SELF_TYPE_NAMES return False + def expand_type_alias_for_self_check( + self, node: SymbolNode | None, args: Sequence[Type] + ) -> Type | None: + """Substitute a type alias's own type parameters with the given arguments + (e.g. "Ident[Self]" for "type Ident[T] = T" gives back "Self"), so the + result can be checked like any other type. + """ + if not isinstance(node, TypeAlias) or len(node.alias_tvars) != len(args): + return None + env = {tvar.id: arg for tvar, arg in zip(node.alias_tvars, args)} + return expand_type(node.target, env) + def set_original_def(self, previous: Node | None, new: FuncDef | Decorator) -> bool: """If 'new' conditionally redefine 'previous', set 'previous' as original diff --git a/test-data/unit/check-selftype.test b/test-data/unit/check-selftype.test index 6f73df5e05c8..d69e8b84ed4f 100644 --- a/test-data/unit/check-selftype.test +++ b/test-data/unit/check-selftype.test @@ -1567,6 +1567,84 @@ reveal_type(C.foo) # N: Revealed type is "def [Self <: __main__.C] (self: Self`2 reveal_type(C.bar) # N: Revealed type is "def (x: builtins.int)" [builtins fixtures/tuple.pyi] +[case testTypingSelfIdentityTypeAliasMinimal] +# flags: --python-version 3.12 +type Ident[T] = T +def f(x: Ident[int]) -> Ident[int]: + return x +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testTypingSelfIdentityTypeAlias] +# flags: --python-version 3.12 +from typing import Self + +type Ident[T] = T + +class C: + def foo(self: Ident[Self], x: int) -> None: + ... + + @classmethod + def bar(cls: Ident[type[Self]], x: int) -> None: + ... + + @classmethod + def baz(cls: type[Ident[Self]], x: int) -> None: + ... + +reveal_type(C.foo) # N: Revealed type is "def [Self <: __main__.C] (self: Self`10, x: builtins.int)" +reveal_type(C.bar) # N: Revealed type is "def (x: builtins.int)" +reveal_type(C.baz) # N: Revealed type is "def (x: builtins.int)" + +class D: + def qux(self: Ident[int]) -> None: # E: The erased type of self "builtins.int" is not a supertype of its class "__main__.D" + ... +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + +[case testTypingSelfIdentityTypeAliasSubstitution] +# flags: --python-version 3.12 +# The identity-alias check works by substituting the alias's own arguments into +# its target (mypy.expandtype.expand_type) and checking the result, rather than +# only recognizing the literal shape "type X[T] = T". This covers, for free: +# extra unused type parameters, any parameter order, and a target that only +# collapses to Self once its members are substituted with the same type. +from typing import Self + +type Extra[T, S] = T +type Extra2[S, T] = T +type UnionSame[T] = T | T + +class C: + def union_collapses(self: UnionSame[Self]) -> None: + ... + + def extra_unused_first(self: Extra[Self, int]) -> None: + ... + + def extra_unused_second(self: Extra2[int, Self]) -> None: + ... + + @classmethod + def extra_classmethod(cls: Extra[type[Self], int]) -> None: + ... + +reveal_type(C.union_collapses) # N: Revealed type is "def [Self <: __main__.C] (self: Self`10 | Self`10)" +reveal_type(C.extra_unused_first) # N: Revealed type is "def [Self <: __main__.C] (self: Self`11)" +reveal_type(C.extra_unused_second) # N: Revealed type is "def [Self <: __main__.C] (self: Self`12)" +reveal_type(C.extra_classmethod) # N: Revealed type is "def ()" + +class D: + # Self supplied for the alias's *unused* type parameter must not be mistaken + # for the one that actually reaches the target -- this must still be a real + # self-type error, not silently accepted. + def wrong_slot(self: Extra[int, Self]) -> None: # E: Method cannot have explicit self annotation and Self type \ + # E: The erased type of self "builtins.int" is not a supertype of its class "__main__.D" + ... +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + [case testTypingSelfOverload] from typing import Self, overload, Union