mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 10:48:32 +00:00
[ty] Make tuple instantiations sound (#18987)
## Summary Ensure that we correctly infer calls such as `tuple((1, 2))`, `tuple(range(42))`, etc. Ensure that we emit errors on invalid calls such as `tuple[int, str]()`. ## Test Plan Mdtests
This commit is contained in:
parent
6802c4702f
commit
a50a993b9c
6 changed files with 105 additions and 19 deletions
|
@ -41,9 +41,7 @@ bar"""
|
|||
reveal_type(len(())) # revealed: Literal[0]
|
||||
reveal_type(len((1,))) # revealed: Literal[1]
|
||||
reveal_type(len((1, 2))) # revealed: Literal[2]
|
||||
|
||||
# TODO: Handle constructor calls
|
||||
reveal_type(len(tuple())) # revealed: int
|
||||
reveal_type(len(tuple())) # revealed: Literal[0]
|
||||
|
||||
# TODO: Handle star unpacks; Should be: Literal[0]
|
||||
reveal_type(len((*[],))) # revealed: Literal[1]
|
||||
|
|
|
@ -23,18 +23,36 @@ specialization of `tuple` we (TODO: should) check that the values passed in matc
|
|||
defined in the specialization.
|
||||
|
||||
```py
|
||||
# TODO: revealed: tuple[()]
|
||||
reveal_type(tuple()) # revealed: tuple[Unknown, ...]
|
||||
# TODO: revealed: tuple[Literal[1]]
|
||||
reveal_type(tuple([1])) # revealed: tuple[Unknown, ...]
|
||||
reveal_type(tuple[int]([1])) # revealed: tuple[int]
|
||||
# TODO: error for invalid arguments
|
||||
reveal_type(tuple[int, str]([1])) # revealed: tuple[int, str]
|
||||
from typing_extensions import Iterable, Never
|
||||
|
||||
reveal_type(tuple()) # revealed: tuple[()]
|
||||
reveal_type(tuple[int]((1,))) # revealed: tuple[int]
|
||||
reveal_type(().__class__()) # revealed: tuple[()]
|
||||
# TODO: error for invalid arguments
|
||||
reveal_type((1, 2).__class__((1, 2))) # revealed: tuple[Literal[1], Literal[2]]
|
||||
|
||||
def f(x: Iterable[int], y: list[str], z: Never, aa: list[Never]):
|
||||
reveal_type(tuple(x)) # revealed: tuple[int, ...]
|
||||
reveal_type(tuple(y)) # revealed: tuple[str, ...]
|
||||
reveal_type(tuple(z)) # revealed: tuple[Unknown, ...]
|
||||
|
||||
# This is correct as the only inhabitants of `list[Never]` can be empty lists
|
||||
reveal_type(tuple(aa)) # revealed: tuple[()]
|
||||
|
||||
reveal_type(tuple((1, 2))) # revealed: tuple[Literal[1], Literal[2]]
|
||||
|
||||
# TODO: should be `tuple[Literal[1], ...]`
|
||||
reveal_type(tuple([1])) # revealed: tuple[Unknown, ...]
|
||||
|
||||
# error: [invalid-argument-type] "Argument is incorrect: Expected `tuple[int]`, found `list[Unknown]`"
|
||||
reveal_type(tuple[int]([1])) # revealed: tuple[int]
|
||||
|
||||
# error: [invalid-argument-type] "Argument is incorrect: Expected `tuple[int, str]`, found `tuple[Literal[1]]`"
|
||||
reveal_type(tuple[int, str]((1,))) # revealed: tuple[int, str]
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `iterable`"
|
||||
reveal_type((1,).__class__()) # revealed: tuple[Literal[1]]
|
||||
# TODO: error for invalid arguments
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `iterable`"
|
||||
reveal_type((1, 2).__class__()) # revealed: tuple[Literal[1], Literal[2]]
|
||||
```
|
||||
|
||||
|
|
|
@ -946,6 +946,13 @@ impl<'db> Type<'db> {
|
|||
matches!(self, Type::ClassLiteral(..))
|
||||
}
|
||||
|
||||
pub(crate) const fn into_tuple(self) -> Option<TupleType<'db>> {
|
||||
match self {
|
||||
Type::Tuple(tuple_type) => Some(tuple_type),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Turn a class literal (`Type::ClassLiteral` or `Type::GenericAlias`) into a `ClassType`.
|
||||
/// Since a `ClassType` must be specialized, apply the default specialization to any
|
||||
/// unspecialized generic class literal.
|
||||
|
@ -4237,6 +4244,27 @@ impl<'db> Type<'db> {
|
|||
.into()
|
||||
}
|
||||
|
||||
Some(KnownClass::Tuple) => {
|
||||
let object = Type::object(db);
|
||||
|
||||
CallableBinding::from_overloads(
|
||||
self,
|
||||
[
|
||||
Signature::new(Parameters::empty(), Some(TupleType::empty(db))),
|
||||
Signature::new(
|
||||
Parameters::new([Parameter::positional_only(Some(
|
||||
Name::new_static("iterable"),
|
||||
))
|
||||
.with_annotated_type(
|
||||
KnownClass::Iterable.to_specialized_instance(db, [object]),
|
||||
)]),
|
||||
Some(TupleType::homogeneous(db, object)),
|
||||
),
|
||||
],
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
// Most class literal constructor calls are handled by `try_call_constructor` and
|
||||
// not via getting the signature here. This signature can still be used in some
|
||||
// cases (e.g. evaluating callable subtyping). TODO improve this definition
|
||||
|
@ -4276,14 +4304,24 @@ impl<'db> Type<'db> {
|
|||
.into()
|
||||
}
|
||||
|
||||
Type::GenericAlias(_) => {
|
||||
Type::GenericAlias(alias) => {
|
||||
let instantiated = Type::instance(db, ClassType::from(alias));
|
||||
|
||||
let parameters = if alias.origin(db).is_known(db, KnownClass::Tuple) {
|
||||
let spec = alias.specialization(db).tuple(db);
|
||||
let mut parameter =
|
||||
Parameter::positional_only(Some(Name::new_static("iterable")))
|
||||
.with_annotated_type(instantiated);
|
||||
if matches!(spec.size_hint().1, Some(0)) {
|
||||
parameter = parameter.with_default_type(TupleType::empty(db));
|
||||
}
|
||||
Parameters::new([parameter])
|
||||
} else {
|
||||
Parameters::gradual_form()
|
||||
};
|
||||
// TODO annotated return type on `__new__` or metaclass `__call__`
|
||||
// TODO check call vs signatures of `__new__` and/or `__init__`
|
||||
Binding::single(
|
||||
self,
|
||||
Signature::new(Parameters::gradual_form(), self.to_instance(db)),
|
||||
)
|
||||
.into()
|
||||
Binding::single(self, Signature::new(parameters, Some(instantiated))).into()
|
||||
}
|
||||
|
||||
Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() {
|
||||
|
|
|
@ -972,6 +972,28 @@ impl<'db> Bindings<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
Some(KnownClass::Tuple) if overload_index == 1 => {
|
||||
if let [Some(argument)] = overload.parameter_types() {
|
||||
let overridden_return =
|
||||
argument.into_tuple().map(Type::Tuple).unwrap_or_else(|| {
|
||||
// Some awkward special handling is required here because of the fact
|
||||
// that calling `try_iterate()` on `Never` returns `Never`,
|
||||
// but `tuple[Never, ...]` eagerly simplifies to `tuple[()]`,
|
||||
// which will cause us to emit false positives if we index into the tuple
|
||||
let specialization = if argument.is_never() {
|
||||
Type::unknown()
|
||||
} else {
|
||||
argument.try_iterate(db).expect(
|
||||
"try_iterate() should not fail on a type \
|
||||
assignable to `Iterable`",
|
||||
)
|
||||
};
|
||||
TupleType::homogeneous(db, specialization)
|
||||
});
|
||||
overload.set_return_type(overridden_return);
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
},
|
||||
|
||||
|
|
|
@ -2334,6 +2334,7 @@ pub enum KnownClass {
|
|||
NamedTuple,
|
||||
NewType,
|
||||
SupportsIndex,
|
||||
Iterable,
|
||||
// Collections
|
||||
ChainMap,
|
||||
Counter,
|
||||
|
@ -2426,6 +2427,7 @@ impl KnownClass {
|
|||
| Self::Float
|
||||
| Self::Enum
|
||||
| Self::ABCMeta
|
||||
| KnownClass::Iterable
|
||||
// Empty tuples are AlwaysFalse; non-empty tuples are AlwaysTrue
|
||||
| Self::NamedTuple
|
||||
// Evaluating `NotImplementedType` in a boolean context was deprecated in Python 3.9
|
||||
|
@ -2513,6 +2515,7 @@ impl KnownClass {
|
|||
| Self::DefaultDict
|
||||
| Self::OrderedDict
|
||||
| Self::NewType
|
||||
| Self::Iterable
|
||||
| Self::BaseExceptionGroup => false,
|
||||
}
|
||||
}
|
||||
|
@ -2531,7 +2534,7 @@ impl KnownClass {
|
|||
/// 2. It's probably more performant.
|
||||
const fn is_protocol(self) -> bool {
|
||||
match self {
|
||||
Self::SupportsIndex => true,
|
||||
Self::SupportsIndex | Self::Iterable => true,
|
||||
|
||||
Self::Any
|
||||
| Self::Bool
|
||||
|
@ -2648,6 +2651,7 @@ impl KnownClass {
|
|||
Self::Enum => "Enum",
|
||||
Self::ABCMeta => "ABCMeta",
|
||||
Self::Super => "super",
|
||||
Self::Iterable => "Iterable",
|
||||
// For example, `typing.List` is defined as `List = _Alias()` in typeshed
|
||||
Self::StdlibAlias => "_Alias",
|
||||
// This is the name the type of `sys.version_info` has in typeshed,
|
||||
|
@ -2882,6 +2886,7 @@ impl KnownClass {
|
|||
| Self::TypeVar
|
||||
| Self::NamedTuple
|
||||
| Self::StdlibAlias
|
||||
| Self::Iterable
|
||||
| Self::SupportsIndex => KnownModule::Typing,
|
||||
Self::TypeAliasType
|
||||
| Self::TypeVarTuple
|
||||
|
@ -2984,6 +2989,7 @@ impl KnownClass {
|
|||
| Self::NewType
|
||||
| Self::Field
|
||||
| Self::KwOnly
|
||||
| Self::Iterable
|
||||
| Self::NamedTupleFallback => false,
|
||||
}
|
||||
}
|
||||
|
@ -3052,6 +3058,7 @@ impl KnownClass {
|
|||
| Self::NewType
|
||||
| Self::Field
|
||||
| Self::KwOnly
|
||||
| Self::Iterable
|
||||
| Self::NamedTupleFallback => false,
|
||||
}
|
||||
}
|
||||
|
@ -3101,6 +3108,7 @@ impl KnownClass {
|
|||
"NewType" => Self::NewType,
|
||||
"TypeAliasType" => Self::TypeAliasType,
|
||||
"TypeVar" => Self::TypeVar,
|
||||
"Iterable" => Self::Iterable,
|
||||
"ParamSpec" => Self::ParamSpec,
|
||||
"ParamSpecArgs" => Self::ParamSpecArgs,
|
||||
"ParamSpecKwargs" => Self::ParamSpecKwargs,
|
||||
|
@ -3197,6 +3205,7 @@ impl KnownClass {
|
|||
| Self::ParamSpecKwargs
|
||||
| Self::TypeVarTuple
|
||||
| Self::NamedTuple
|
||||
| Self::Iterable
|
||||
| Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5343,6 +5343,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
| KnownClass::TypeVar
|
||||
| KnownClass::NamedTuple
|
||||
| KnownClass::TypeAliasType
|
||||
| KnownClass::Tuple
|
||||
)
|
||||
)
|
||||
// temporary special-casing for all subclasses of `enum.Enum`
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue