| """Verify properties of type arguments, like 'int' in C[int] being valid. |
| |
| This must happen after semantic analysis since there can be placeholder |
| types until the end of semantic analysis, and these break various type |
| operations, including subtype checks. |
| """ |
| |
| from __future__ import annotations |
| |
| from typing import Callable |
| |
| from mypy import errorcodes as codes, message_registry |
| from mypy.errorcodes import ErrorCode |
| from mypy.errors import Errors |
| from mypy.message_registry import INVALID_PARAM_SPEC_LOCATION, INVALID_PARAM_SPEC_LOCATION_NOTE |
| from mypy.messages import format_type |
| from mypy.mixedtraverser import MixedTraverserVisitor |
| from mypy.nodes import Block, ClassDef, Context, FakeInfo, FuncItem, MypyFile |
| from mypy.options import Options |
| from mypy.scope import Scope |
| from mypy.subtypes import is_same_type, is_subtype |
| from mypy.types import ( |
| AnyType, |
| CallableType, |
| Instance, |
| Parameters, |
| ParamSpecType, |
| TupleType, |
| Type, |
| TypeAliasType, |
| TypeOfAny, |
| TypeVarLikeType, |
| TypeVarTupleType, |
| TypeVarType, |
| UnboundType, |
| UnpackType, |
| flatten_nested_tuples, |
| get_proper_type, |
| get_proper_types, |
| split_with_prefix_and_suffix, |
| ) |
| from mypy.typevartuples import erased_vars |
| |
| |
| class TypeArgumentAnalyzer(MixedTraverserVisitor): |
| def __init__( |
| self, |
| errors: Errors, |
| options: Options, |
| is_typeshed_file: bool, |
| named_type: Callable[[str, list[Type]], Instance], |
| ) -> None: |
| super().__init__() |
| self.errors = errors |
| self.options = options |
| self.is_typeshed_file = is_typeshed_file |
| self.named_type = named_type |
| self.scope = Scope() |
| # Should we also analyze function definitions, or only module top-levels? |
| self.recurse_into_functions = True |
| # Keep track of the type aliases already visited. This is needed to avoid |
| # infinite recursion on types like A = Union[int, List[A]]. |
| self.seen_aliases: set[TypeAliasType] = set() |
| |
| def visit_mypy_file(self, o: MypyFile) -> None: |
| self.errors.set_file(o.path, o.fullname, scope=self.scope, options=self.options) |
| with self.scope.module_scope(o.fullname): |
| super().visit_mypy_file(o) |
| |
| def visit_func(self, defn: FuncItem) -> None: |
| if not self.recurse_into_functions: |
| return |
| with self.scope.function_scope(defn): |
| super().visit_func(defn) |
| |
| def visit_class_def(self, defn: ClassDef) -> None: |
| with self.scope.class_scope(defn.info): |
| super().visit_class_def(defn) |
| |
| def visit_block(self, o: Block) -> None: |
| if not o.is_unreachable: |
| super().visit_block(o) |
| |
| def visit_type_alias_type(self, t: TypeAliasType) -> None: |
| super().visit_type_alias_type(t) |
| if t in self.seen_aliases: |
| # Avoid infinite recursion on recursive type aliases. |
| # Note: it is fine to skip the aliases we have already seen in non-recursive |
| # types, since errors there have already been reported. |
| return |
| self.seen_aliases.add(t) |
| assert t.alias is not None, f"Unfixed type alias {t.type_ref}" |
| is_error, is_invalid = self.validate_args( |
| t.alias.name, tuple(t.args), t.alias.alias_tvars, t |
| ) |
| if is_invalid: |
| # If there is an arity error (e.g. non-Parameters used for ParamSpec etc.), |
| # then it is safer to erase the arguments completely, to avoid crashes later. |
| # TODO: can we move this logic to typeanal.py? |
| t.args = erased_vars(t.alias.alias_tvars, TypeOfAny.from_error) |
| if not is_error: |
| # If there was already an error for the alias itself, there is no point in checking |
| # the expansion, most likely it will result in the same kind of error. |
| get_proper_type(t).accept(self) |
| |
| def visit_tuple_type(self, t: TupleType) -> None: |
| t.items = flatten_nested_tuples(t.items) |
| # We could also normalize Tuple[*tuple[X, ...]] -> tuple[X, ...] like in |
| # expand_type() but we can't do this here since it is not a translator visitor, |
| # and we need to return an Instance instead of TupleType. |
| super().visit_tuple_type(t) |
| |
| def visit_callable_type(self, t: CallableType) -> None: |
| super().visit_callable_type(t) |
| t.normalize_trivial_unpack() |
| |
| def visit_instance(self, t: Instance) -> None: |
| super().visit_instance(t) |
| # Type argument counts were checked in the main semantic analyzer pass. We assume |
| # that the counts are correct here. |
| info = t.type |
| if isinstance(info, FakeInfo): |
| return # https://github.com/python/mypy/issues/11079 |
| _, is_invalid = self.validate_args(info.name, t.args, info.defn.type_vars, t) |
| if is_invalid: |
| t.args = tuple(erased_vars(info.defn.type_vars, TypeOfAny.from_error)) |
| if t.type.fullname == "builtins.tuple" and len(t.args) == 1: |
| # Normalize Tuple[*Tuple[X, ...], ...] -> Tuple[X, ...] |
| arg = t.args[0] |
| if isinstance(arg, UnpackType): |
| unpacked = get_proper_type(arg.type) |
| if isinstance(unpacked, Instance): |
| assert unpacked.type.fullname == "builtins.tuple" |
| t.args = unpacked.args |
| |
| def validate_args( |
| self, name: str, args: tuple[Type, ...], type_vars: list[TypeVarLikeType], ctx: Context |
| ) -> tuple[bool, bool]: |
| if any(isinstance(v, TypeVarTupleType) for v in type_vars): |
| prefix = next(i for (i, v) in enumerate(type_vars) if isinstance(v, TypeVarTupleType)) |
| tvt = type_vars[prefix] |
| assert isinstance(tvt, TypeVarTupleType) |
| start, middle, end = split_with_prefix_and_suffix( |
| tuple(args), prefix, len(type_vars) - prefix - 1 |
| ) |
| args = start + (TupleType(list(middle), tvt.tuple_fallback),) + end |
| |
| is_error = False |
| is_invalid = False |
| for (i, arg), tvar in zip(enumerate(args), type_vars): |
| context = ctx if arg.line < 0 else arg |
| if isinstance(tvar, TypeVarType): |
| if isinstance(arg, ParamSpecType): |
| is_invalid = True |
| self.fail( |
| INVALID_PARAM_SPEC_LOCATION.format(format_type(arg, self.options)), |
| context, |
| code=codes.VALID_TYPE, |
| ) |
| self.note( |
| INVALID_PARAM_SPEC_LOCATION_NOTE.format(arg.name), |
| context, |
| code=codes.VALID_TYPE, |
| ) |
| continue |
| if isinstance(arg, Parameters): |
| is_invalid = True |
| self.fail( |
| f"Cannot use {format_type(arg, self.options)} for regular type variable," |
| " only for ParamSpec", |
| context, |
| code=codes.VALID_TYPE, |
| ) |
| continue |
| if tvar.values: |
| if isinstance(arg, TypeVarType): |
| if self.in_type_alias_expr: |
| # Type aliases are allowed to use unconstrained type variables |
| # error will be checked at substitution point. |
| continue |
| arg_values = arg.values |
| if not arg_values: |
| is_error = True |
| self.fail( |
| message_registry.INVALID_TYPEVAR_AS_TYPEARG.format(arg.name, name), |
| context, |
| code=codes.TYPE_VAR, |
| ) |
| continue |
| else: |
| arg_values = [arg] |
| if self.check_type_var_values( |
| name, arg_values, tvar.name, tvar.values, context |
| ): |
| is_error = True |
| # Check against upper bound. Since it's object the vast majority of the time, |
| # add fast path to avoid a potentially slow subtype check. |
| upper_bound = tvar.upper_bound |
| object_upper_bound = ( |
| type(upper_bound) is Instance |
| and upper_bound.type.fullname == "builtins.object" |
| ) |
| if not object_upper_bound and not is_subtype(arg, upper_bound): |
| if self.in_type_alias_expr and isinstance(arg, TypeVarType): |
| # Type aliases are allowed to use unconstrained type variables |
| # error will be checked at substitution point. |
| continue |
| is_error = True |
| self.fail( |
| message_registry.INVALID_TYPEVAR_ARG_BOUND.format( |
| format_type(arg, self.options), |
| name, |
| format_type(upper_bound, self.options), |
| ), |
| context, |
| code=codes.TYPE_VAR, |
| ) |
| elif isinstance(tvar, ParamSpecType): |
| if not isinstance( |
| get_proper_type(arg), (ParamSpecType, Parameters, AnyType, UnboundType) |
| ): |
| is_invalid = True |
| self.fail( |
| "Can only replace ParamSpec with a parameter types list or" |
| f" another ParamSpec, got {format_type(arg, self.options)}", |
| context, |
| code=codes.VALID_TYPE, |
| ) |
| if is_invalid: |
| is_error = True |
| return is_error, is_invalid |
| |
| def visit_unpack_type(self, typ: UnpackType) -> None: |
| super().visit_unpack_type(typ) |
| proper_type = get_proper_type(typ.type) |
| if isinstance(proper_type, TupleType): |
| return |
| if isinstance(proper_type, TypeVarTupleType): |
| return |
| # TODO: this should probably be .has_base("builtins.tuple"), also elsewhere. This is |
| # tricky however, since this needs map_instance_to_supertype() available in many places. |
| if isinstance(proper_type, Instance) and proper_type.type.fullname == "builtins.tuple": |
| return |
| if not isinstance(proper_type, (UnboundType, AnyType)): |
| # Avoid extra errors if there were some errors already. Also interpret plain Any |
| # as tuple[Any, ...] (this is better for the code in type checker). |
| self.fail( |
| message_registry.INVALID_UNPACK.format(format_type(proper_type, self.options)), |
| typ.type, |
| code=codes.VALID_TYPE, |
| ) |
| typ.type = self.named_type("builtins.tuple", [AnyType(TypeOfAny.from_error)]) |
| |
| def check_type_var_values( |
| self, name: str, actuals: list[Type], arg_name: str, valids: list[Type], context: Context |
| ) -> bool: |
| is_error = False |
| for actual in get_proper_types(actuals): |
| # We skip UnboundType here, since they may appear in defn.bases, |
| # the error will be caught when visiting info.bases, that have bound type |
| # variables. |
| if not isinstance(actual, (AnyType, UnboundType)) and not any( |
| is_same_type(actual, value) for value in valids |
| ): |
| is_error = True |
| if len(actuals) > 1 or not isinstance(actual, Instance): |
| self.fail( |
| message_registry.INVALID_TYPEVAR_ARG_VALUE.format(name), |
| context, |
| code=codes.TYPE_VAR, |
| ) |
| else: |
| class_name = f'"{name}"' |
| actual_type_name = f'"{actual.type.name}"' |
| self.fail( |
| message_registry.INCOMPATIBLE_TYPEVAR_VALUE.format( |
| arg_name, class_name, actual_type_name |
| ), |
| context, |
| code=codes.TYPE_VAR, |
| ) |
| return is_error |
| |
| def fail(self, msg: str, context: Context, *, code: ErrorCode | None = None) -> None: |
| self.errors.report(context.line, context.column, msg, code=code) |
| |
| def note(self, msg: str, context: Context, *, code: ErrorCode | None = None) -> None: |
| self.errors.report(context.line, context.column, msg, severity="note", code=code) |