|
1 | 1 | use compact_str::ToCompactString; |
2 | 2 | use itertools::Itertools; |
3 | 3 | use ruff_diagnostics::{Edit, Fix}; |
4 | | -use rustc_hash::FxHashMap; |
| 4 | +use rustc_hash::{FxHashMap, FxHashSet}; |
5 | 5 |
|
6 | 6 | use std::borrow::Cow; |
7 | 7 | use std::cell::OnceCell; |
@@ -862,6 +862,84 @@ fn recursive_type_normalize_type_guard_like<'db, T: TypeGuardLike<'db>>( |
862 | 862 | }; |
863 | 863 | Some(guard.with_type(db, ty)) |
864 | 864 | } |
| 865 | +// Represents the members of a union that are all homogeneously-typed, fixed-length tuples and that |
| 866 | +// share the same element type. |
| 867 | +// |
| 868 | +// EXAMPLE: |
| 869 | +// |
| 870 | +// Given a union like `str | tuple[int] | tuple[int, int] | tuple[str, str]`, we would build the |
| 871 | +// following instances of this struct: |
| 872 | +// |
| 873 | +// HomogenousTupleGroup{ |
| 874 | +// element_type: int, |
| 875 | +// original_tuple_types: [tuple[int], tuple[int, int]] |
| 876 | +// } |
| 877 | +// |
| 878 | +// HomogenousTupleGroup{ |
| 879 | +// element_type: str, |
| 880 | +// original_tuple_types: [tuple[str, str]] |
| 881 | +// } |
| 882 | +struct HomogeneousTupleGroup<'db> { |
| 883 | + element_type: Type<'db>, |
| 884 | + original_tuple_types: Vec<Type<'db>>, |
| 885 | +} |
| 886 | + |
| 887 | +impl<'db> HomogeneousTupleGroup<'db> { |
| 888 | + fn new(element_type: Type<'db>, original_tuple_type: Type<'db>) -> Self { |
| 889 | + Self { |
| 890 | + element_type, |
| 891 | + original_tuple_types: vec![original_tuple_type], |
| 892 | + } |
| 893 | + } |
| 894 | + |
| 895 | + // Add a new tuple to this group. |
| 896 | + fn add(&mut self, original_tuple_type: Type<'db>) { |
| 897 | + self.original_tuple_types.push(original_tuple_type); |
| 898 | + } |
| 899 | + |
| 900 | + // Does the group have tuples of different lengths? |
| 901 | + // |
| 902 | + // This determines whether or not we should promote the types in this group to a single tuple |
| 903 | + // of variable-length. |
| 904 | + fn has_multiple_lengths(&self, db: &'db dyn Db) -> bool { |
| 905 | + let mut lengths = self.original_tuple_types.iter().filter_map(|tuple_type| { |
| 906 | + tuple_type |
| 907 | + .homogeneous_fixed_length_tuple_instance(db) |
| 908 | + .map(|(_, length)| length) |
| 909 | + }); |
| 910 | + |
| 911 | + let Some(first_length) = lengths.next() else { |
| 912 | + return false; |
| 913 | + }; |
| 914 | + |
| 915 | + lengths.any(|length| length != first_length) |
| 916 | + } |
| 917 | +} |
| 918 | + |
| 919 | +fn partition_homogeneous_fixed_length_tuple_union_elements<'db>( |
| 920 | + db: &'db dyn Db, |
| 921 | + elements: impl IntoIterator<Item = Type<'db>>, |
| 922 | +) -> (Vec<Type<'db>>, Vec<HomogeneousTupleGroup<'db>>) { |
| 923 | + let mut other_union_elements = Vec::new(); |
| 924 | + let mut tuple_groups: Vec<HomogeneousTupleGroup<'db>> = Vec::new(); |
| 925 | + |
| 926 | + for element in elements { |
| 927 | + if let Some((tuple_element_type, _)) = element.homogeneous_fixed_length_tuple_instance(db) { |
| 928 | + if let Some(group) = tuple_groups |
| 929 | + .iter_mut() |
| 930 | + .find(|group| group.element_type.is_equivalent_to(db, tuple_element_type)) |
| 931 | + { |
| 932 | + group.add(element); |
| 933 | + } else { |
| 934 | + tuple_groups.push(HomogeneousTupleGroup::new(tuple_element_type, element)); |
| 935 | + } |
| 936 | + } else { |
| 937 | + other_union_elements.push(element); |
| 938 | + } |
| 939 | + } |
| 940 | + |
| 941 | + (other_union_elements, tuple_groups) |
| 942 | +} |
865 | 943 |
|
866 | 944 | #[derive(Debug, Clone, Copy)] |
867 | 945 | #[expect(clippy::struct_field_names)] |
@@ -1297,6 +1375,32 @@ impl<'db> Type<'db> { |
1297 | 1375 | .and_then(|instance| instance.own_tuple_spec(db)) |
1298 | 1376 | } |
1299 | 1377 |
|
| 1378 | + /// Detects whether or not this type is a homogeneously-typed tuple of fixed length |
| 1379 | + /// e.g. `tuple[str, str]` but NOT `tuple[str, int]` or `tuple[str, ...]`. |
| 1380 | + /// |
| 1381 | + /// If this type is indeed a homogeneously-typed, fixed-length tuple, then returns the |
| 1382 | + /// tuple's homogeneous element type and its length. |
| 1383 | + pub(crate) fn homogeneous_fixed_length_tuple_instance( |
| 1384 | + self, |
| 1385 | + db: &'db dyn Db, |
| 1386 | + ) -> Option<(Type<'db>, usize)> { |
| 1387 | + let tuple_spec = self.exact_tuple_instance_spec(db)?; |
| 1388 | + let TupleSpec::Fixed(tuple) = tuple_spec.as_ref() else { |
| 1389 | + return None; |
| 1390 | + }; |
| 1391 | + |
| 1392 | + let length = tuple.len(); |
| 1393 | + if length == 0 { |
| 1394 | + return None; |
| 1395 | + } |
| 1396 | + |
| 1397 | + let mut elements = tuple.iter_all_elements(); |
| 1398 | + let element_type = elements.next()?; |
| 1399 | + elements |
| 1400 | + .all(|element| element.is_equivalent_to(db, element_type)) |
| 1401 | + .then_some((element_type, length)) |
| 1402 | + } |
| 1403 | + |
1300 | 1404 | /// Returns the materialization of this type depending on the given `variance`. |
1301 | 1405 | /// |
1302 | 1406 | /// More concretely, `T'`, the materialization of `T`, is the type `T` with all occurrences of |
@@ -1925,6 +2029,78 @@ impl<'db> Type<'db> { |
1925 | 2029 | ) |
1926 | 2030 | } |
1927 | 2031 |
|
| 2032 | + /// Promote unions of homogeneously-typed, fixed-length tuples with different lengths to a |
| 2033 | + /// single variable-length tuple when every tuple in that homogeneous group came from a tuple |
| 2034 | + /// literal in the collection literal currently being inferred. |
| 2035 | + /// |
| 2036 | + /// This deliberately only applies to unions; a standalone fixed-length tuple keeps its shape, |
| 2037 | + /// and groups that include non-literal tuple members remain unchanged. |
| 2038 | + /// |
| 2039 | + /// EXAMPLE: |
| 2040 | + /// |
| 2041 | + /// In the code below, we promote `dict[str, tuple[str, str] | tuple[str, str, str, str]]` |
| 2042 | + /// to `dict[str, tuple[str,...]]`. |
| 2043 | + /// |
| 2044 | + /// languages = { |
| 2045 | + /// "python": (".py", ".pyi"), |
| 2046 | + /// "javascript": (".js", ".jsx", ".ts", ".tsx"), |
| 2047 | + /// } |
| 2048 | + /// reveal_type(languages) # revealed: dict[str, tuple[str, ...]] |
| 2049 | + /// |
| 2050 | + pub(crate) fn promote_tuple_literal_unions( |
| 2051 | + self, |
| 2052 | + db: &'db dyn Db, |
| 2053 | + tuple_literal_candidates: &FxHashSet<Type<'db>>, |
| 2054 | + non_literal_tuple_candidates: &FxHashSet<Type<'db>>, |
| 2055 | + ) -> Type<'db> { |
| 2056 | + let Type::Union(union) = self else { |
| 2057 | + return self; |
| 2058 | + }; |
| 2059 | + |
| 2060 | + let (other_union_elements, tuple_groups) = |
| 2061 | + partition_homogeneous_fixed_length_tuple_union_elements( |
| 2062 | + db, |
| 2063 | + union.elements(db).iter().copied(), |
| 2064 | + ); |
| 2065 | + |
| 2066 | + if !tuple_groups.iter().any(|group| { |
| 2067 | + group.has_multiple_lengths(db) |
| 2068 | + && group.original_tuple_types.iter().all(|tuple_type| { |
| 2069 | + tuple_literal_candidates.contains(tuple_type) |
| 2070 | + && !non_literal_tuple_candidates.contains(tuple_type) |
| 2071 | + }) |
| 2072 | + }) { |
| 2073 | + // No tuple group consisted entirely of tuple-literal members with differing lengths, |
| 2074 | + // so there is nothing to promote. Return early to avoid rebuilding the union type. |
| 2075 | + return self; |
| 2076 | + } |
| 2077 | + |
| 2078 | + let mut builder = UnionBuilder::new(db) |
| 2079 | + .unpack_aliases(false) |
| 2080 | + .recursively_defined(union.recursively_defined(db)); |
| 2081 | + |
| 2082 | + for element in other_union_elements { |
| 2083 | + builder = builder.add(element); |
| 2084 | + } |
| 2085 | + |
| 2086 | + for group in tuple_groups { |
| 2087 | + if group.has_multiple_lengths(db) |
| 2088 | + && group.original_tuple_types.iter().all(|tuple_type| { |
| 2089 | + tuple_literal_candidates.contains(tuple_type) |
| 2090 | + && !non_literal_tuple_candidates.contains(tuple_type) |
| 2091 | + }) |
| 2092 | + { |
| 2093 | + builder = builder.add(Type::homogeneous_tuple(db, group.element_type)); |
| 2094 | + } else { |
| 2095 | + for element in group.original_tuple_types { |
| 2096 | + builder = builder.add(element); |
| 2097 | + } |
| 2098 | + } |
| 2099 | + } |
| 2100 | + |
| 2101 | + builder.build() |
| 2102 | + } |
| 2103 | + |
1928 | 2104 | /// Promote a top-level singleton type (like `None`, `EllipsisType`) to `T | Unknown`. |
1929 | 2105 | pub(crate) fn promote_singletons(self, db: &'db dyn Db) -> Type<'db> { |
1930 | 2106 | self.promote_singletons_impl(db) |
|
0 commit comments