Skip to main content

ty_combine/
lib.rs

1#![warn(
2    clippy::disallowed_methods,
3    reason = "Prefer System trait methods over std methods in ty crates"
4)]
5
6use ordermap::OrderMap;
7use ruff_db::system::SystemPathBuf;
8use ruff_python_ast::PythonVersion;
9use ruff_ranged_value::RangedValue;
10use std::{collections::HashMap, hash::BuildHasher};
11
12/// Combine two values, preferring the values in `self`.
13///
14/// The logic should follow that of Cargo's `config.toml`:
15///
16/// > If a key is specified in multiple config files, the values will get merged together.
17/// > Numbers, strings, and booleans will use the value in the deeper config directory taking
18/// > precedence over ancestor directories, where the home directory is the lowest priority.
19/// > Arrays will be joined together with higher precedence items being placed later in the
20/// > merged array.
21///
22/// ## uv Compatibility
23///
24/// The merging behavior differs from uv in that values with higher precedence in arrays
25/// are placed later in the merged array. This is because we want to support overriding
26/// earlier values and values from other configurations, including unsetting them.
27/// For example: patterns coming last in file inclusion and exclusion patterns
28/// allow overriding earlier patterns, matching the `gitignore` behavior.
29/// Generally speaking, it feels more intuitive if later values override earlier values
30/// than the other way around: `ty --exclude png --exclude "!important.png"`.
31///
32/// The main downside of this approach is that the ordering can be surprising in cases
33/// where the option has a "first match" semantic and not a "last match" wins.
34/// One such example is `extra-paths` where the semantics is given by Python:
35/// the module on the first matching search path wins.
36///
37/// ```toml
38/// [environment]
39/// extra-paths = ["b", "c"]
40/// ```
41///
42/// ```bash
43/// ty --extra-paths a
44/// ```
45///
46/// That's why a user might expect that this configuration results in `["a", "b", "c"]`,
47/// because the CLI has higher precedence. However, the current implementation results in a
48/// resolved extra search path of `["b", "c", "a"]`, which means `a` will be tried last.
49///
50/// There's an argument here that the user should be able to specify the order of the paths,
51/// because only then is the user in full control of where to insert the path when specifying `extra-paths`
52/// in multiple sources.
53///
54/// ## Macro
55/// You can automatically derive `Combine` for structs with named fields by using `derive(ruff_macros::Combine)`.
56pub trait Combine {
57    #[must_use]
58    fn combine(mut self, other: Self) -> Self
59    where
60        Self: Sized,
61    {
62        self.combine_with(other);
63        self
64    }
65
66    fn combine_with(&mut self, other: Self);
67}
68
69impl<T> Combine for RangedValue<T>
70where
71    T: Combine,
72{
73    fn combine_with(&mut self, other: Self) {
74        (**self).combine_with(other.into_inner());
75    }
76}
77
78impl<T> Combine for Option<T>
79where
80    T: Combine,
81{
82    fn combine(self, other: Self) -> Self
83    where
84        Self: Sized,
85    {
86        match (self, other) {
87            (Some(a), Some(b)) => Some(a.combine(b)),
88            (None, Some(b)) => Some(b),
89            (a, _) => a,
90        }
91    }
92
93    fn combine_with(&mut self, other: Self) {
94        match (self, other) {
95            (Some(a), Some(b)) => {
96                a.combine_with(b);
97            }
98            (a @ None, Some(b)) => {
99                *a = Some(b);
100            }
101            _ => {}
102        }
103    }
104}
105
106impl<T> Combine for Vec<T> {
107    fn combine_with(&mut self, mut other: Self) {
108        // `self` takes precedence over `other` but values with higher precedence must be placed after.
109        // Swap the vectors so that `other` is the one that gets extended, so that the values of `self` come after.
110        std::mem::swap(self, &mut other);
111        self.extend(other);
112    }
113}
114
115impl<K, V, S> Combine for HashMap<K, V, S>
116where
117    K: Eq + std::hash::Hash,
118    S: BuildHasher,
119{
120    fn combine_with(&mut self, mut other: Self) {
121        // `self` takes precedence over `other` but `extend` overrides existing values.
122        // Swap the hash maps so that `self` is the one that gets extended.
123        std::mem::swap(self, &mut other);
124        self.extend(other);
125    }
126}
127
128impl<K, V, S> Combine for OrderMap<K, V, S>
129where
130    K: Eq + std::hash::Hash,
131    S: BuildHasher,
132{
133    fn combine_with(&mut self, mut other: Self) {
134        // `self` takes precedence over `other` but values with higher precedence must be placed after.
135        // Swap the vectors so that `other` is the one that gets extended, so that the values of `self` come after.
136        std::mem::swap(self, &mut other);
137
138        for (k, v) in other {
139            // If there's an existing entry, remove it to ensure `k` (previously self)
140            // comes after any item in `self`.
141            self.remove(&k);
142
143            // Append `k` at the end.
144            self.insert(k, v);
145        }
146    }
147}
148
149/// Implements [`Combine`] for a value that always returns `self` when combined with another value.
150macro_rules! impl_noop_combine {
151    ($name:ident) => {
152        impl Combine for $name {
153            #[inline(always)]
154            fn combine_with(&mut self, _other: Self) {}
155
156            #[inline(always)]
157            fn combine(self, _other: Self) -> Self {
158                self
159            }
160        }
161    };
162}
163
164impl_noop_combine!(SystemPathBuf);
165impl_noop_combine!(PythonVersion);
166
167// std types
168impl_noop_combine!(bool);
169impl_noop_combine!(usize);
170impl_noop_combine!(u8);
171impl_noop_combine!(u16);
172impl_noop_combine!(u32);
173impl_noop_combine!(u64);
174impl_noop_combine!(u128);
175impl_noop_combine!(isize);
176impl_noop_combine!(i8);
177impl_noop_combine!(i16);
178impl_noop_combine!(i32);
179impl_noop_combine!(i64);
180impl_noop_combine!(i128);
181impl_noop_combine!(String);
182
183#[cfg(test)]
184mod tests {
185    use ordermap::OrderMap;
186    use std::collections::HashMap;
187
188    use super::Combine;
189
190    #[test]
191    fn combine_option() {
192        assert_eq!(Some(1).combine(Some(2)), Some(1));
193        assert_eq!(None.combine(Some(2)), Some(2));
194        assert_eq!(Some(1).combine(None), Some(1));
195    }
196
197    #[test]
198    fn combine_vec() {
199        assert_eq!(None.combine(Some(vec![1, 2, 3])), Some(vec![1, 2, 3]));
200        assert_eq!(Some(vec![1, 2, 3]).combine(None), Some(vec![1, 2, 3]));
201        assert_eq!(
202            Some(vec![1, 2, 3]).combine(Some(vec![4, 5, 6])),
203            Some(vec![4, 5, 6, 1, 2, 3])
204        );
205    }
206
207    #[test]
208    fn combine_map() {
209        let a: HashMap<u32, _> = HashMap::from_iter([(1, "a"), (2, "a"), (3, "a")]);
210        let b: HashMap<u32, _> = HashMap::from_iter([(0, "b"), (2, "b"), (5, "b")]);
211
212        assert_eq!(None.combine(Some(b.clone())), Some(b.clone()));
213        assert_eq!(Some(a.clone()).combine(None), Some(a.clone()));
214        assert_eq!(
215            Some(a).combine(Some(b)),
216            Some(HashMap::from_iter([
217                (0, "b"),
218                // The value from `a` takes precedence
219                (1, "a"),
220                (2, "a"),
221                (3, "a"),
222                (5, "b")
223            ]))
224        );
225    }
226
227    #[test]
228    fn combine_order_map() {
229        let a: OrderMap<_, _> = OrderMap::from_iter([(1, "a"), (2, "a"), (3, "a")]);
230        let b: OrderMap<_, _> = OrderMap::from_iter([(0, "b"), (2, "b"), (5, "b")]);
231
232        assert_eq!(None.combine(Some(b.clone())), Some(b.clone()));
233        assert_eq!(Some(a.clone()).combine(None), Some(a.clone()));
234        assert_eq!(
235            a.combine(b),
236            // The value from `a` takes precedence
237            OrderMap::<_, _>::from_iter([(0, "b"), (5, "b"), (1, "a"), (2, "a"), (3, "a"),])
238        );
239    }
240}