mirror of
https://github.com/astral-sh/ruff
synced 2026-01-21 05:20:49 -05:00
[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:
@@ -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")
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
```
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user