From 7f0ce3e88db413877d28bdfcb63e09714e721da8 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 14 Jan 2026 14:23:07 +0000 Subject: [PATCH] [ty] Infer `type[Unknown]` for calls to `type()` when overload evaluation is ambiguous (#22569) Co-authored-by: Charlie Marsh --- .../resources/mdtest/call/type.md | 34 ++--- ...-_A_class_constructor_…_(dd9f8a8f736a329).snap | 1 + crates/ty_python_semantic/src/types.rs | 49 ------ .../ty_python_semantic/src/types/call/bind.rs | 6 - .../src/types/infer/builder.rs | 142 +++++++++++++----- 5 files changed, 122 insertions(+), 110 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/type.md b/crates/ty_python_semantic/resources/mdtest/call/type.md index 828e050202..550abee5f7 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/type.md +++ b/crates/ty_python_semantic/resources/mdtest/call/type.md @@ -496,19 +496,20 @@ Other numbers of arguments are invalid: ```py # error: [no-matching-overload] "No overload of class `type` matches arguments" -reveal_type(type("Foo", ())) # revealed: Unknown +reveal_type(type("Foo", ())) # revealed: type[Unknown] # TODO: the keyword arguments for `Foo`/`Bar`/`Baz` here are invalid -# (you cannot pass `metaclass=` to `type()`, and none of them have -# base classes with `__init_subclass__` methods), -# but `type[Unknown]` would be better than `Unknown` here +# (none of them have base classes with `__init_subclass__` methods). # +# The intent to create a new class is however clear, +# so we still infer these as class-literal types. +reveal_type(type("Foo", (), {}, weird_other_arg=42)) # revealed: +reveal_type(type("Bar", (int,), {}, weird_other_arg=42)) # revealed: + +# You can't pass `metaclass=` to the `type()` constructor, but the intent is clear, +# so we infer `` here rather than `type[Unknown]` # error: [no-matching-overload] "No overload of class `type` matches arguments" -reveal_type(type("Foo", (), {}, weird_other_arg=42)) # revealed: Unknown -# error: [no-matching-overload] "No overload of class `type` matches arguments" -reveal_type(type("Bar", (int,), {}, weird_other_arg=42)) # revealed: Unknown -# error: [no-matching-overload] "No overload of class `type` matches arguments" -reveal_type(type("Baz", (), {}, metaclass=type)) # revealed: Unknown +reveal_type(type("Baz", (), {}, metaclass=type)) # revealed: ``` The following calls are also invalid, due to incorrect argument types: @@ -861,26 +862,22 @@ def f(*args, **kwargs): # Has a string first arg, but unknown additional args from *args B = type("B", *args, **kwargs) - # TODO: `type[Unknown]` would cause fewer false positives - reveal_type(B) # revealed: + reveal_type(B) # revealed: type[Unknown] # Has string and tuple, but unknown additional args C = type("C", (), *args, **kwargs) - # TODO: `type[Unknown]` would cause fewer false positives - reveal_type(C) # revealed: type + reveal_type(C) # revealed: type[Unknown] # All three positional args provided, only **kwargs unknown D = type("D", (), {}, **kwargs) - # TODO: `type[Unknown]` would cause fewer false positives - reveal_type(D) # revealed: type + reveal_type(D) # revealed: # Three starred expressions - we can't know how they expand a = ("E",) b = ((),) c = ({},) E = type(*a, *b, *c) - # TODO: `type[Unknown]` would cause fewer false positives - reveal_type(E) # revealed: type + reveal_type(E) # revealed: type[Unknown] ``` ## Explicit type annotations @@ -1027,9 +1024,6 @@ class Child(Base, required_arg="value"): # The dynamically assigned attribute has Unknown in its type reveal_type(Child.config) # revealed: Unknown | str -# Dynamic class creation - keyword arguments are not yet supported -# TODO: This should work: type("DynamicChild", (Base,), {}, required_arg="value") -# error: [no-matching-overload] DynamicChild = type("DynamicChild", (Base,), {}, required_arg="value") ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload…_-_No_matching_overload…_-_A_class_constructor_…_(dd9f8a8f736a329).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload…_-_No_matching_overload…_-_A_class_constructor_…_(dd9f8a8f736a329).snap index 2f63c59bc7..f109146b9a 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload…_-_No_matching_overload…_-_A_class_constructor_…_(dd9f8a8f736a329).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/no_matching_overload…_-_No_matching_overload…_-_A_class_constructor_…_(dd9f8a8f736a329).snap @@ -25,6 +25,7 @@ error[no-matching-overload]: No overload of class `type` matches arguments 1 | type() # error: [no-matching-overload] | ^^^^^^ | +help: `builtins.type()` can either be called with one or three positional arguments (got 0) info: rule `no-matching-overload` is enabled by default ``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index f206765569..b6ff5bae68 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4111,55 +4111,6 @@ impl<'db> Type<'db> { .into() } - Some(KnownClass::Type) => { - let str_instance = KnownClass::Str.to_instance(db); - let type_instance = KnownClass::Type.to_instance(db); - - // ```py - // class type: - // @overload - // def __init__(self, o: object, /) -> None: ... - // @overload - // def __init__(self, name: str, bases: tuple[type, ...], dict: dict[str, Any], /, **kwds: Any) -> None: ... - // ``` - CallableBinding::from_overloads( - self, - [ - Signature::new( - Parameters::new( - db, - [Parameter::positional_only(Some(Name::new_static("o"))) - .with_annotated_type(Type::any())], - ), - type_instance, - ), - Signature::new( - Parameters::new( - db, - [ - Parameter::positional_only(Some(Name::new_static("name"))) - .with_annotated_type(str_instance), - Parameter::positional_only(Some(Name::new_static("bases"))) - .with_annotated_type(Type::homogeneous_tuple( - db, - type_instance, - )), - Parameter::positional_only(Some(Name::new_static("dict"))) - .with_annotated_type( - KnownClass::Dict.to_specialized_instance( - db, - &[str_instance, Type::any()], - ), - ), - ], - ), - type_instance, - ), - ], - ) - .into() - } - Some(KnownClass::Object) => { // ```py // class object: diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 79bdb1de21..e22baa02b9 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -1443,12 +1443,6 @@ impl<'db> Bindings<'db> { } } - Some(KnownClass::Type) if overload_index == 0 => { - if let [Some(arg)] = overload.parameter_types() { - overload.set_return_type(arg.dunder_class(db)); - } - } - Some(KnownClass::Property) => { if let [getter, setter, ..] = overload.parameter_types() { overload.set_return_type(Type::PropertyInstance( diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 9ae32ee3a1..41397c6b43 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -70,11 +70,11 @@ use crate::types::diagnostic::{ INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_ARGUMENTS, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_GUARD_DEFINITION, INVALID_TYPE_VARIABLE_CONSTRAINTS, INVALID_TYPED_DICT_STATEMENT, IncompatibleBases, - NOT_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, - POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, TypedDictDeleteErrorKind, UNDEFINED_REVEAL, - UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, - UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, - hint_if_stdlib_attribute_exists_on_other_versions, + NO_MATCHING_OVERLOAD, NOT_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE, + POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, + TypedDictDeleteErrorKind, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, + UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR, + USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions, hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation, report_bad_dunder_set_call, report_bad_frozen_dataclass_inheritance, report_cannot_delete_typed_dict_key, report_cannot_pop_required_field_on_typed_dict, @@ -5582,10 +5582,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Try to extract the dynamic class with definition. // This returns `None` if it's not a three-arg call to `type()`, // signalling that we must fall back to normal call inference. - self.infer_dynamic_type_expression(call_expr, Some(definition)) - .unwrap_or_else(|| { - self.infer_call_expression_impl(call_expr, callable_type, tcx) - }) + self.infer_builtins_type_call(call_expr, Some(definition)) } Some(_) | None => { self.infer_call_expression_impl(call_expr, callable_type, tcx) @@ -6190,19 +6187,21 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - /// Try to infer a 3-argument `type(name, bases, dict)` call expression, capturing the definition. + /// Infer a call to `builtins.type()`. /// - /// This is called when we detect a `type()` call in assignment context and want to - /// associate the resulting `DynamicClassLiteral` with its definition for go-to-definition. + /// `builtins.type` has two overloads: a single-argument overload (e.g. `type("foo")`, + /// and a 3-argument `type(name, bases, dict)` overload. Both are handled here. + /// The `definition` parameter should be `Some()` if this call to `builtins.type()` + /// occurs on the right-hand side of an assignment statement that has a [`Definition`] + /// associated with it in the semantic index. /// - /// Returns `None` if any keywords were provided or the number of arguments is not three, - /// signalling that no types were stored for any AST sub-expressions and that we should - /// therefore fallback to normal call binding for error reporting. - fn infer_dynamic_type_expression( + /// If it's unclear which overload we should pick, we return `type[Unknown]`, + /// to avoid cascading errors later on. + fn infer_builtins_type_call( &mut self, call_expr: &ast::ExprCall, definition: Option>, - ) -> Option> { + ) -> Type<'db> { let db = self.db(); let ast::Arguments { @@ -6212,27 +6211,101 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { node_index: _, } = &call_expr.arguments; - if !keywords.is_empty() { - return None; + for keyword in keywords { + self.infer_expression(&keyword.value, TypeContext::default()); } - let [name_arg, bases_arg, namespace_arg] = &**args else { - return None; + let [name_arg, bases_arg, namespace_arg] = match &**args { + [single] => { + let arg_type = self.infer_expression(single, TypeContext::default()); + + return if keywords.is_empty() { + arg_type.dunder_class(db) + } else { + if keywords.iter().any(|keyword| keyword.arg.is_some()) + && let Some(builder) = + self.context.report_lint(&NO_MATCHING_OVERLOAD, call_expr) + { + let mut diagnostic = builder + .into_diagnostic("No overload of class `type` matches arguments"); + diagnostic.help(format_args!( + "`builtins.type()` expects no keyword arguments", + )); + } + SubclassOfType::subclass_of_unknown() + }; + } + + [first, second] if second.is_starred_expr() => { + self.infer_expression(first, TypeContext::default()); + self.infer_expression(second, TypeContext::default()); + + match &**keywords { + [single] if single.arg.is_none() => { + return SubclassOfType::subclass_of_unknown(); + } + _ => { + if let Some(builder) = + self.context.report_lint(&NO_MATCHING_OVERLOAD, call_expr) + { + let mut diagnostic = builder + .into_diagnostic("No overload of class `type` matches arguments"); + diagnostic.help(format_args!( + "`builtins.type()` expects no keyword arguments", + )); + } + + return SubclassOfType::subclass_of_unknown(); + } + } + } + + [name, bases, namespace] => [name, bases, namespace], + + _ => { + for arg in args { + self.infer_expression(arg, TypeContext::default()); + } + + if let Some(builder) = self.context.report_lint(&NO_MATCHING_OVERLOAD, call_expr) { + let mut diagnostic = + builder.into_diagnostic("No overload of class `type` matches arguments"); + diagnostic.help(format_args!( + "`builtins.type()` can either be called with one or three \ + positional arguments (got {})", + args.len() + )); + } + + return SubclassOfType::subclass_of_unknown(); + } }; - // If any argument is a starred expression, we can't know how many positional arguments - // we're receiving, so fall back to normal call binding. - if args.iter().any(ast::Expr::is_starred_expr) { - return None; - } - - // Infer the argument types. let name_type = self.infer_expression(name_arg, TypeContext::default()); let bases_type = self.infer_expression(bases_arg, TypeContext::default()); + let namespace_type = self.infer_expression(namespace_arg, TypeContext::default()); + + // TODO: validate other keywords against `__init_subclass__` methods of superclasses + if keywords + .iter() + .filter_map(|keyword| keyword.arg.as_deref()) + .contains("metaclass") + { + if let Some(builder) = self.context.report_lint(&NO_MATCHING_OVERLOAD, call_expr) { + let mut diagnostic = + builder.into_diagnostic("No overload of class `type` matches arguments"); + diagnostic + .help("The `metaclass` keyword argument is not supported in `type()` calls"); + } + } + + // If any argument is a starred expression, we can't know how many positional arguments + // we're receiving, so fall back to `type[Unknown]` to avoid false-positive errors. + if args.iter().any(ast::Expr::is_starred_expr) { + return SubclassOfType::subclass_of_unknown(); + } // Extract members from the namespace dict (third argument). - // Infer the whole dict first to avoid double-inferring individual values. - let namespace_type = self.infer_expression(namespace_arg, TypeContext::default()); let (members, has_dynamic_namespace): (Box<[(ast::name::Name, Type<'db>)]>, bool) = if let ast::Expr::Dict(dict) = namespace_arg { // Check if all keys are string literal types. If any key is not a string literal @@ -6259,7 +6332,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .collect(); (members, !all_keys_are_string_literals) } else if let Type::TypedDict(typed_dict) = namespace_type { - // Namespace is a TypedDict instance. Extract known keys as members. + // `namespace` is a TypedDict instance. Extract known keys as members. // TypedDicts are "open" (can have additional string keys), so this // is still a dynamic namespace for unknown attributes. let members: Box<[(ast::name::Name, Type<'db>)]> = typed_dict @@ -6269,7 +6342,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .collect(); (members, true) } else { - // Namespace is not a dict literal, so it's dynamic. + // `namespace` is not a dict literal, so it's dynamic. (Box::new([]), true) }; @@ -6407,7 +6480,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); } - Some(Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class))) + Type::ClassLiteral(ClassLiteral::Dynamic(dynamic_class)) } /// Extract base classes from the second argument of a `type()` call. @@ -9643,9 +9716,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Handle 3-argument `type(name, bases, dict)`. if let Type::ClassLiteral(class) = callable_type && class.is_known(self.db(), KnownClass::Type) - && let Some(dynamic_type) = self.infer_dynamic_type_expression(call_expression, None) { - return dynamic_type; + return self.infer_builtins_type_call(call_expression, None); } // We don't call `Type::try_call`, because we want to perform type inference on the