Compare commits
2 Commits
main
...
alex/union
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c872fe4c08 | ||
|
|
f497167798 |
@@ -1833,7 +1833,7 @@ impl<'db> Type<'db> {
|
||||
///
|
||||
/// This method may have false negatives, but it should not have false positives. It should be
|
||||
/// a cheap shallow check, not an exhaustive recursive check.
|
||||
fn subtyping_is_always_reflexive(self) -> bool {
|
||||
const fn subtyping_is_always_reflexive(self) -> bool {
|
||||
match self {
|
||||
Type::Never
|
||||
| Type::FunctionLiteral(..)
|
||||
@@ -1854,6 +1854,7 @@ impl<'db> Type<'db> {
|
||||
| Type::AlwaysFalsy
|
||||
| Type::AlwaysTruthy
|
||||
| Type::PropertyInstance(_)
|
||||
| Type::TypeVar(_)
|
||||
// might inherit `Any`, but subtyping is still reflexive
|
||||
| Type::ClassLiteral(_)
|
||||
=> true,
|
||||
@@ -1865,7 +1866,6 @@ impl<'db> Type<'db> {
|
||||
| Type::Union(_)
|
||||
| Type::Intersection(_)
|
||||
| Type::Callable(_)
|
||||
| Type::TypeVar(_)
|
||||
| Type::BoundSuper(_)
|
||||
| Type::TypeIs(_)
|
||||
| Type::TypeGuard(_)
|
||||
@@ -11724,8 +11724,8 @@ pub(super) struct MetaclassCandidate<'db> {
|
||||
#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
|
||||
pub struct UnionType<'db> {
|
||||
/// The union type includes values in any of these types.
|
||||
#[returns(deref)]
|
||||
pub elements: Box<[Type<'db>]>,
|
||||
#[returns(ref)]
|
||||
pub elements: FxOrderSet<Type<'db>>,
|
||||
/// Whether the value pointed to by this type is recursively defined.
|
||||
/// If `Yes`, union literal widening is performed early.
|
||||
recursively_defined: RecursivelyDefined,
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
//! shares exactly the same possible super-types, and none of them are subtypes of each other
|
||||
//! (unless exactly the same literal type), we can avoid many unnecessary redundancy checks.
|
||||
|
||||
use std::hash::BuildHasherDefault;
|
||||
|
||||
use crate::types::enums::{enum_member_literals, enum_metadata};
|
||||
use crate::types::type_ordering::union_or_intersection_elements_ordering;
|
||||
use crate::types::{
|
||||
@@ -706,7 +708,10 @@ impl<'db> UnionBuilder<'db> {
|
||||
}
|
||||
|
||||
pub(crate) fn try_build(self) -> Option<Type<'db>> {
|
||||
let mut types = vec![];
|
||||
let mut types: FxOrderSet<Type<'db>> = FxOrderSet::with_capacity_and_hasher(
|
||||
self.elements.len(),
|
||||
BuildHasherDefault::default(),
|
||||
);
|
||||
for element in self.elements {
|
||||
match element {
|
||||
UnionElement::IntLiterals(literals) => {
|
||||
@@ -721,7 +726,9 @@ impl<'db> UnionBuilder<'db> {
|
||||
UnionElement::EnumLiterals { literals, .. } => {
|
||||
types.extend(literals.into_iter().map(Type::EnumLiteral));
|
||||
}
|
||||
UnionElement::Type(ty) => types.push(ty),
|
||||
UnionElement::Type(ty) => {
|
||||
types.insert(ty);
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.order_elements {
|
||||
@@ -730,11 +737,14 @@ impl<'db> UnionBuilder<'db> {
|
||||
match types.len() {
|
||||
0 => None,
|
||||
1 => Some(types[0]),
|
||||
_ => Some(Type::Union(UnionType::new(
|
||||
self.db,
|
||||
types.into_boxed_slice(),
|
||||
self.recursively_defined,
|
||||
))),
|
||||
_ => {
|
||||
types.shrink_to_fit();
|
||||
Some(Type::Union(UnionType::new(
|
||||
self.db,
|
||||
types,
|
||||
self.recursively_defined,
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -847,7 +857,7 @@ impl<'db> IntersectionBuilder<'db> {
|
||||
db,
|
||||
enum_member_literals(db, instance.class_literal(db), None)
|
||||
.expect("Calling `enum_member_literals` on an enum class")
|
||||
.collect::<Box<[_]>>(),
|
||||
.collect::<FxOrderSet<_>>(),
|
||||
RecursivelyDefined::No,
|
||||
)),
|
||||
seen_aliases,
|
||||
@@ -1412,6 +1422,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
mod tests {
|
||||
use super::{IntersectionBuilder, Type, UnionBuilder, UnionType};
|
||||
|
||||
use crate::FxOrderSet;
|
||||
use crate::db::tests::setup_db;
|
||||
use crate::place::known_module_symbol;
|
||||
use crate::types::enums::enum_member_literals;
|
||||
@@ -1445,7 +1456,7 @@ mod tests {
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let union = UnionType::from_elements(&db, [t0, t1]).expect_union();
|
||||
|
||||
assert_eq!(union.elements(&db), &[t0, t1]);
|
||||
assert_eq!(union.elements(&db), &FxOrderSet::from_iter([t0, t1]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -4,10 +4,10 @@ use std::fmt::Display;
|
||||
use itertools::{Either, Itertools};
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
use crate::Db;
|
||||
use crate::types::KnownClass;
|
||||
use crate::types::enums::{enum_member_literals, enum_metadata};
|
||||
use crate::types::tuple::{Tuple, TupleType};
|
||||
use crate::{Db, FxOrderSet};
|
||||
|
||||
use super::Type;
|
||||
|
||||
@@ -362,17 +362,17 @@ pub(crate) fn is_expandable_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool {
|
||||
/// Expands a type into its possible subtypes, if applicable.
|
||||
///
|
||||
/// Returns [`None`] if the type cannot be expanded.
|
||||
fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Vec<Type<'db>>> {
|
||||
fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<FxOrderSet<Type<'db>>> {
|
||||
// NOTE: Update `is_expandable_type` if this logic changes accordingly.
|
||||
match ty {
|
||||
Type::NominalInstance(instance) => {
|
||||
let class = instance.class(db);
|
||||
|
||||
if class.is_known(db, KnownClass::Bool) {
|
||||
return Some(vec![
|
||||
return Some(FxOrderSet::from_iter([
|
||||
Type::BooleanLiteral(true),
|
||||
Type::BooleanLiteral(false),
|
||||
]);
|
||||
]));
|
||||
}
|
||||
|
||||
// If the class is a fixed-length tuple subtype, we expand it to its elements.
|
||||
@@ -390,7 +390,7 @@ fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Vec<Type<'db>>> {
|
||||
})
|
||||
.multi_cartesian_product()
|
||||
.map(|types| Type::tuple(TupleType::heterogeneous(db, types)))
|
||||
.collect::<Vec<_>>();
|
||||
.collect::<FxOrderSet<_>>();
|
||||
|
||||
if expanded.len() == 1 {
|
||||
// There are no elements in the tuple type that can be expanded.
|
||||
@@ -409,7 +409,7 @@ fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Vec<Type<'db>>> {
|
||||
|
||||
None
|
||||
}
|
||||
Type::Union(union) => Some(union.elements(db).to_vec()),
|
||||
Type::Union(union) => Some(union.elements(db).clone()),
|
||||
// We don't handle `type[A | B]` here because it's already stored in the expanded form
|
||||
// i.e., `type[A] | type[B]` which is handled by the `Type::Union` case.
|
||||
_ => None,
|
||||
@@ -418,6 +418,7 @@ fn expand_type<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Vec<Type<'db>>> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::FxOrderSet;
|
||||
use crate::db::tests::setup_db;
|
||||
use crate::types::tuple::TupleType;
|
||||
use crate::types::{KnownClass, Type, UnionType};
|
||||
@@ -427,12 +428,12 @@ mod tests {
|
||||
#[test]
|
||||
fn expand_union_type() {
|
||||
let db = setup_db();
|
||||
let types = [
|
||||
let types = FxOrderSet::from_iter([
|
||||
KnownClass::Int.to_instance(&db),
|
||||
KnownClass::Str.to_instance(&db),
|
||||
KnownClass::Bytes.to_instance(&db),
|
||||
];
|
||||
let union_type = UnionType::from_elements(&db, types);
|
||||
]);
|
||||
let union_type = UnionType::from_elements(&db, &types);
|
||||
let expanded = expand_type(&db, union_type).unwrap();
|
||||
assert_eq!(expanded.len(), types.len());
|
||||
assert_eq!(expanded, types);
|
||||
@@ -443,7 +444,8 @@ mod tests {
|
||||
let db = setup_db();
|
||||
let bool_instance = KnownClass::Bool.to_instance(&db);
|
||||
let expanded = expand_type(&db, bool_instance).unwrap();
|
||||
let expected_types = [Type::BooleanLiteral(true), Type::BooleanLiteral(false)];
|
||||
let expected_types =
|
||||
FxOrderSet::from_iter([Type::BooleanLiteral(true), Type::BooleanLiteral(false)]);
|
||||
assert_eq!(expanded.len(), expected_types.len());
|
||||
assert_eq!(expanded, expected_types);
|
||||
}
|
||||
@@ -477,14 +479,14 @@ mod tests {
|
||||
UnionType::from_elements(&db, [int_ty, str_ty, bytes_ty]),
|
||||
],
|
||||
);
|
||||
let expected_types = [
|
||||
let expected_types = FxOrderSet::from_iter([
|
||||
Type::heterogeneous_tuple(&db, [true_ty, int_ty]),
|
||||
Type::heterogeneous_tuple(&db, [true_ty, str_ty]),
|
||||
Type::heterogeneous_tuple(&db, [true_ty, bytes_ty]),
|
||||
Type::heterogeneous_tuple(&db, [false_ty, int_ty]),
|
||||
Type::heterogeneous_tuple(&db, [false_ty, str_ty]),
|
||||
Type::heterogeneous_tuple(&db, [false_ty, bytes_ty]),
|
||||
];
|
||||
]);
|
||||
let expanded = expand_type(&db, tuple_type2).unwrap();
|
||||
assert_eq!(expanded, expected_types);
|
||||
|
||||
@@ -498,12 +500,12 @@ mod tests {
|
||||
str_ty,
|
||||
],
|
||||
);
|
||||
let expected_types = [
|
||||
let expected_types = FxOrderSet::from_iter([
|
||||
Type::heterogeneous_tuple(&db, [true_ty, int_ty, str_ty, str_ty]),
|
||||
Type::heterogeneous_tuple(&db, [true_ty, int_ty, bytes_ty, str_ty]),
|
||||
Type::heterogeneous_tuple(&db, [false_ty, int_ty, str_ty, str_ty]),
|
||||
Type::heterogeneous_tuple(&db, [false_ty, int_ty, bytes_ty, str_ty]),
|
||||
];
|
||||
]);
|
||||
let expanded = expand_type(&db, tuple_type3).unwrap();
|
||||
assert_eq!(expanded, expected_types);
|
||||
|
||||
|
||||
@@ -2784,18 +2784,25 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> {
|
||||
// will allow us to error when `*args: P.args` is matched against, for example,
|
||||
// `n: int` and correctly type check when `*args: P.args` is matched against
|
||||
// `*args: P.args` (another ParamSpec).
|
||||
match union.elements(db) {
|
||||
[paramspec @ Type::TypeVar(typevar), other]
|
||||
| [other, paramspec @ Type::TypeVar(typevar)]
|
||||
if typevar.is_paramspec(db) && other.is_unknown() =>
|
||||
{
|
||||
VariadicArgumentType::ParamSpec(*paramspec)
|
||||
}
|
||||
_ => {
|
||||
// TODO: Same todo comment as in the non-paramspec case below
|
||||
VariadicArgumentType::Other(argument_type.iterate(db))
|
||||
let elements = union.elements(db);
|
||||
let mut paramspec = None;
|
||||
if elements.len() == 2 {
|
||||
match (elements[0], elements[1]) {
|
||||
(Type::TypeVar(typevar), Type::Dynamic(_))
|
||||
| (Type::Dynamic(_), Type::TypeVar(typevar))
|
||||
if typevar.is_paramspec(db) =>
|
||||
{
|
||||
paramspec = Some(Type::TypeVar(typevar));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if let Some(paramspec) = paramspec {
|
||||
VariadicArgumentType::ParamSpec(paramspec)
|
||||
} else {
|
||||
// TODO: Same todo comment as in the non-paramspec case below
|
||||
VariadicArgumentType::Other(argument_type.iterate(db))
|
||||
}
|
||||
}
|
||||
Some(paramspec @ Type::TypeVar(typevar)) if typevar.is_paramspec(db) => {
|
||||
VariadicArgumentType::ParamSpec(paramspec)
|
||||
@@ -2915,15 +2922,20 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> {
|
||||
let value_type = match argument_type {
|
||||
Some(argument_type @ Type::Union(union)) => {
|
||||
// See the comment in `match_variadic` for why we special case this situation.
|
||||
match union.elements(db) {
|
||||
[paramspec @ Type::TypeVar(typevar), other]
|
||||
| [other, paramspec @ Type::TypeVar(typevar)]
|
||||
if typevar.is_paramspec(db) && other.is_unknown() =>
|
||||
{
|
||||
*paramspec
|
||||
let elements = union.elements(db);
|
||||
let mut paramspec = None;
|
||||
if elements.len() == 2 {
|
||||
match (elements[0], elements[1]) {
|
||||
(Type::TypeVar(typevar), Type::Dynamic(_))
|
||||
| (Type::Dynamic(_), Type::TypeVar(typevar))
|
||||
if typevar.is_paramspec(db) =>
|
||||
{
|
||||
paramspec = Some(Type::TypeVar(typevar));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
_ => dunder_getitem_return_type(argument_type),
|
||||
}
|
||||
paramspec.unwrap_or_else(|| dunder_getitem_return_type(argument_type))
|
||||
}
|
||||
Some(paramspec @ Type::TypeVar(typevar)) if typevar.is_paramspec(db) => paramspec,
|
||||
Some(argument_type) => dunder_getitem_return_type(argument_type),
|
||||
@@ -3572,15 +3584,20 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> {
|
||||
let value_type = match argument_type {
|
||||
Type::Union(union) => {
|
||||
// See the comment in `match_variadic` for why we special case this situation.
|
||||
match union.elements(self.db) {
|
||||
[paramspec @ Type::TypeVar(typevar), other]
|
||||
| [other, paramspec @ Type::TypeVar(typevar)]
|
||||
if typevar.is_paramspec(self.db) && other.is_unknown() =>
|
||||
{
|
||||
Some(*paramspec)
|
||||
let elements = union.elements(self.db);
|
||||
let mut paramspec = None;
|
||||
if elements.len() == 2 {
|
||||
match (elements[0], elements[1]) {
|
||||
(Type::TypeVar(typevar), Type::Dynamic(_))
|
||||
| (Type::Dynamic(_), Type::TypeVar(typevar))
|
||||
if typevar.is_paramspec(self.db) =>
|
||||
{
|
||||
paramspec = Some(Type::TypeVar(typevar));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
_ => value_type_fallback(argument_type),
|
||||
}
|
||||
paramspec.or_else(|| value_type_fallback(argument_type))
|
||||
}
|
||||
Type::TypeVar(typevar) if typevar.is_paramspec(self.db) => Some(argument_type),
|
||||
_ => value_type_fallback(argument_type),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::FxIndexSet;
|
||||
use crate::FxOrderSet;
|
||||
use crate::place::builtins_module_scope;
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::semantic_index::definition::DefinitionKind;
|
||||
@@ -228,8 +229,8 @@ pub fn definitions_for_attribute<'db>(
|
||||
};
|
||||
|
||||
let tys = match lhs_ty {
|
||||
Type::Union(union) => union.elements(model.db()).to_vec(),
|
||||
_ => vec![lhs_ty],
|
||||
Type::Union(union) => union.elements(model.db()).clone(),
|
||||
_ => FxOrderSet::from_iter([lhs_ty]),
|
||||
};
|
||||
|
||||
// Expand intersections for each subtype into their components
|
||||
|
||||
@@ -7268,15 +7268,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
.any(|overload| overload.signature.generic_context.is_some());
|
||||
|
||||
// If the type context is a union, attempt to narrow to a specific element.
|
||||
let narrow_targets: &[_] = match call_expression_tcx.annotation {
|
||||
let narrow_targets = match call_expression_tcx.annotation {
|
||||
// TODO: We could theoretically attempt to narrow to every element of
|
||||
// the power set of this union. However, this leads to an exponential
|
||||
// explosion of inference attempts, and is rarely needed in practice.
|
||||
//
|
||||
// We only need to attempt narrowing on generic calls, otherwise the type
|
||||
// context has no effect.
|
||||
Some(Type::Union(union)) if has_generic_context => union.elements(db),
|
||||
_ => &[],
|
||||
Some(Type::Union(union)) if has_generic_context => {
|
||||
Either::Left(union.elements(db).iter().copied())
|
||||
}
|
||||
_ => Either::Right(std::iter::empty()),
|
||||
};
|
||||
|
||||
// We silence diagnostics until we successfully narrow to a specific type.
|
||||
@@ -7346,10 +7348,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
|
||||
// Prefer the declared type of generic classes.
|
||||
for narrowed_ty in narrow_targets
|
||||
.iter()
|
||||
.clone()
|
||||
.filter(|ty| ty.class_specialization(db).is_some())
|
||||
{
|
||||
if let Some(result) = try_narrow(*narrowed_ty) {
|
||||
if let Some(result) = try_narrow(narrowed_ty) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -7358,11 +7360,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
//
|
||||
// TODO: We could also attempt an inference without type context, but this
|
||||
// leads to similar performance issues.
|
||||
for narrowed_ty in narrow_targets
|
||||
.iter()
|
||||
.filter(|ty| ty.class_specialization(db).is_none())
|
||||
{
|
||||
if let Some(result) = try_narrow(*narrowed_ty) {
|
||||
for narrowed_ty in narrow_targets.filter(|ty| ty.class_specialization(db).is_none()) {
|
||||
if let Some(result) = try_narrow(narrowed_ty) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,23 +467,24 @@ impl<'db> Type<'db> {
|
||||
//
|
||||
// However, there is one exception to this general rule: for any given typevar `T`,
|
||||
// `T` will always be a subtype of any union containing `T`.
|
||||
(Type::TypeVar(bound_typevar), Type::Union(union))
|
||||
if !bound_typevar.is_inferable(db, inferable)
|
||||
(_, Type::Union(union))
|
||||
if (!relation.is_subtyping() || self.subtyping_is_always_reflexive())
|
||||
&& union.elements(db).contains(&self) =>
|
||||
{
|
||||
ConstraintSet::from(true)
|
||||
}
|
||||
|
||||
// A similar rule applies in reverse to intersection types.
|
||||
(Type::Intersection(intersection), Type::TypeVar(bound_typevar))
|
||||
if !bound_typevar.is_inferable(db, inferable)
|
||||
(Type::Intersection(intersection), _)
|
||||
if (!relation.is_subtyping() || target.subtyping_is_always_reflexive())
|
||||
&& intersection.positive(db).contains(&target) =>
|
||||
{
|
||||
ConstraintSet::from(true)
|
||||
}
|
||||
(Type::Intersection(intersection), Type::TypeVar(bound_typevar))
|
||||
if !bound_typevar.is_inferable(db, inferable)
|
||||
&& intersection.negative(db).contains(&target) =>
|
||||
(Type::Intersection(intersection), _)
|
||||
if (!relation.is_subtyping() || target.subtyping_is_always_reflexive())
|
||||
&& intersection.negative(db).contains(&target)
|
||||
&& !intersection.positive(db).iter().any(Type::is_dynamic) =>
|
||||
{
|
||||
ConstraintSet::from(false)
|
||||
}
|
||||
|
||||
@@ -1785,7 +1785,7 @@ impl<'db> Tuple<Type<'db>> {
|
||||
|
||||
// TODO: just grab this type from typeshed (it's a `sys._ReleaseLevel` type alias there)
|
||||
let release_level_ty = {
|
||||
let elements: Box<[Type<'db>]> = ["alpha", "beta", "candidate", "final"]
|
||||
let elements: FxOrderSet<_> = ["alpha", "beta", "candidate", "final"]
|
||||
.iter()
|
||||
.map(|level| Type::string_literal(db, level))
|
||||
.collect();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use itertools::Either;
|
||||
use ruff_db::parsed::ParsedModuleRef;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
@@ -124,11 +125,13 @@ impl<'db, 'ast> Unpacker<'db, 'ast> {
|
||||
// See <https://github.com/astral-sh/ruff/pull/20377#issuecomment-3401380305>
|
||||
// for more discussion.
|
||||
let unpack_types = match value_ty {
|
||||
Type::Union(union_ty) => union_ty.elements(self.db()),
|
||||
_ => std::slice::from_ref(&value_ty),
|
||||
Type::Union(union_ty) => {
|
||||
Either::Left(union_ty.elements(self.db()).iter().copied())
|
||||
}
|
||||
_ => Either::Right(std::iter::once(value_ty)),
|
||||
};
|
||||
|
||||
for ty in unpack_types.iter().copied() {
|
||||
for ty in unpack_types {
|
||||
let tuple = ty.try_iterate(self.db()).unwrap_or_else(|err| {
|
||||
err.report_diagnostic(&self.context, ty, value_expr);
|
||||
Cow::Owned(TupleSpec::homogeneous(err.fallback_element_type(self.db())))
|
||||
|
||||
Reference in New Issue
Block a user