[ty] Infer type[Unknown] for calls to type() when overload evaluation is ambiguous (#22569)

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
This commit is contained in:
Alex Waygood
2026-01-14 14:23:07 +00:00
committed by GitHub
parent ba0736385d
commit 7f0ce3e88d
5 changed files with 122 additions and 110 deletions

View File

@@ -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: <class 'Foo'>
reveal_type(type("Bar", (int,), {}, weird_other_arg=42)) # revealed: <class 'Bar'>
# You can't pass `metaclass=` to the `type()` constructor, but the intent is clear,
# so we infer `<class 'Baz'>` 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: <class 'Baz'>
```
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: <class 'str'>
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: <class 'D'>
# 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")
```

View File

@@ -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
```

View File

@@ -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:

View File

@@ -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(

View File

@@ -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<Definition<'db>>,
) -> Option<Type<'db>> {
) -> 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