Skip to main content

vivacity_resolver/
policy.rs

1//! Port of `Composer\DependencyResolver\DefaultPolicy`: the choice of
2//! preferred versions (stability, highest/lowest, root alias, replacement,
3//! same vendor, pool id).
4
5use crate::constraint::{Constraint, Op};
6use crate::package::Package;
7use crate::pool::Pool;
8use crate::version::stability_rank;
9use std::cmp::Ordering;
10use std::collections::HashMap;
11
12pub struct DefaultPolicy {
13    pub prefer_stable: bool,
14    pub prefer_lowest: bool,
15    /// `COMPOSER_PREFER_DEV_OVER_PRERELEASE`.
16    pub prefer_dev_over_prerelease: bool,
17    /// `--minimal-changes`: name -> preferred version.
18    pub preferred_versions: Option<HashMap<String, String>>,
19    /// `preferredPackageResultCachePerPool` / `sortingCachePerPool`:
20    /// (pool identity, key).
21    result_cache: HashMap<(u64, String), Vec<i64>>,
22    sorting_cache: HashMap<(u64, String), Ordering>,
23}
24
25impl DefaultPolicy {
26    pub fn new(
27        prefer_stable: bool,
28        prefer_lowest: bool,
29        preferred_versions: Option<HashMap<String, String>>,
30    ) -> DefaultPolicy {
31        let prefer_dev = std::env::var("COMPOSER_PREFER_DEV_OVER_PRERELEASE")
32            .map(|v| !(v.is_empty() || v == "0"))
33            .unwrap_or(false);
34        DefaultPolicy {
35            prefer_stable,
36            prefer_lowest,
37            prefer_dev_over_prerelease: prefer_dev,
38            preferred_versions,
39            result_cache: HashMap::new(),
40            sorting_cache: HashMap::new(),
41        }
42    }
43
44    /// `versionCompare($a, $b, $operator)`.
45    pub fn version_compare(&self, a: &Package, b: &Package, operator: Op) -> bool {
46        if self.prefer_stable && a.stability != b.stability {
47            let (mut stab_a, mut stab_b) = (a.stability, b.stability);
48            if self.prefer_lowest
49                && self.prefer_dev_over_prerelease
50                && stab_a != "stable"
51                && stab_b != "stable"
52            {
53                if stab_a == "dev" {
54                    stab_a = "stable";
55                }
56                if stab_b == "dev" {
57                    stab_b = "stable";
58                }
59            }
60            return stability_rank(stab_a) < stability_rank(stab_b);
61        }
62        if (a.is_dev() && a.version.starts_with("dev-"))
63            || (b.is_dev() && b.version.starts_with("dev-"))
64        {
65            let constraint = Constraint::new(operator, b.version.clone());
66            let version = Constraint::new(Op::Eq, a.version.clone());
67            return constraint.match_specific(&version, true);
68        }
69        Constraint::new(operator, b.version.clone()).matches_version(&a.version)
70    }
71
72    /// `selectPreferredPackages($pool, $literals, $requiredPackage)`.
73    pub fn select_preferred_packages(
74        &mut self,
75        pool: &Pool,
76        arena: &[Package],
77        literals: &[i64],
78        required_package: Option<&str>,
79    ) -> Vec<i64> {
80        let mut literals = literals.to_vec();
81        literals.sort_unstable();
82        let key = format!(
83            "{}{}",
84            literals
85                .iter()
86                .map(|l| l.to_string())
87                .collect::<Vec<_>>()
88                .join(","),
89            required_package.unwrap_or("")
90        );
91        let key = (pool.identity, key);
92        if let Some(cached) = self.result_cache.get(&key) {
93            return cached.clone();
94        }
95        // groupLiteralsByName (order of first appearance).
96        let mut groups: Vec<(String, Vec<i64>)> = Vec::new();
97        for &literal in &literals {
98            let name = &arena[pool.literal_to_package(literal)].name;
99            match groups.iter_mut().find(|(n, _)| n == name) {
100                Some((_, g)) => g.push(literal),
101                None => groups.push((name.clone(), vec![literal])),
102            }
103        }
104        for (_, group) in groups.iter_mut() {
105            let mut sorted = group.clone();
106            sorted.sort_by(|&a, &b| {
107                self.cached_compare(pool, arena, a, b, required_package, true, "i")
108            });
109            *group = self.prune_to_best_version(pool, arena, &sorted);
110            *group = Self::prune_remote_aliases(pool, arena, group);
111        }
112        let mut selected: Vec<i64> = groups.into_iter().flat_map(|(_, g)| g).collect();
113        selected
114            .sort_by(|&a, &b| self.cached_compare(pool, arena, a, b, required_package, false, ""));
115        self.result_cache.insert(key, selected.clone());
116        selected
117    }
118
119    #[allow(clippy::too_many_arguments)]
120    fn cached_compare(
121        &mut self,
122        pool: &Pool,
123        arena: &[Package],
124        a: i64,
125        b: i64,
126        required_package: Option<&str>,
127        ignore_replace: bool,
128        prefix: &str,
129    ) -> Ordering {
130        let key = (
131            pool.identity,
132            format!("{prefix}{a}.{b}{}", required_package.unwrap_or("")),
133        );
134        if let Some(o) = self.sorting_cache.get(&key) {
135            return *o;
136        }
137        let o = Self::compare_by_priority(pool, arena, a, b, required_package, ignore_replace);
138        self.sorting_cache.insert(key, o);
139        o
140    }
141
142    /// `compareByPriority` on two (positive) pool literals.
143    pub fn compare_by_priority(
144        pool: &Pool,
145        arena: &[Package],
146        la: i64,
147        lb: i64,
148        required_package: Option<&str>,
149        ignore_replace: bool,
150    ) -> Ordering {
151        let a = &arena[pool.literal_to_package(la)];
152        let b = &arena[pool.literal_to_package(lb)];
153        if a.name == b.name {
154            let (a_alias, b_alias) = (a.is_alias(), b.is_alias());
155            if a_alias && !b_alias {
156                return Ordering::Less;
157            }
158            if !a_alias && b_alias {
159                return Ordering::Greater;
160            }
161        }
162        if !ignore_replace {
163            if Self::replaces(a, b) {
164                return Ordering::Greater;
165            }
166            if Self::replaces(b, a) {
167                return Ordering::Less;
168            }
169            if let Some(req) = required_package {
170                if let Some(pos) = req.find('/') {
171                    let vendor = &req[..pos];
172                    let a_same = a.name.starts_with(vendor);
173                    let b_same = b.name.starts_with(vendor);
174                    if a_same != b_same {
175                        return if a_same {
176                            Ordering::Less
177                        } else {
178                            Ordering::Greater
179                        };
180                    }
181                }
182            }
183        }
184        la.abs().cmp(&lb.abs())
185    }
186
187    fn replaces(source: &Package, target: &Package) -> bool {
188        source.replaces.iter().any(|l| l.target == target.name)
189    }
190
191    /// `pruneToBestVersion`.
192    fn prune_to_best_version(&self, pool: &Pool, arena: &[Package], literals: &[i64]) -> Vec<i64> {
193        if let Some(preferred) = &self.preferred_versions {
194            let name = &arena[pool.literal_to_package(literals[0])].name;
195            if let Some(version) = preferred.get(name) {
196                let best: Vec<i64> = literals
197                    .iter()
198                    .copied()
199                    .filter(|&l| arena[pool.literal_to_package(l)].version == *version)
200                    .collect();
201                if !best.is_empty() {
202                    return best;
203                }
204            }
205        }
206        let operator = if self.prefer_lowest { Op::Lt } else { Op::Gt };
207        let mut best_literals = vec![literals[0]];
208        let mut best = pool.literal_to_package(literals[0]);
209        for &literal in &literals[1..] {
210            let package = pool.literal_to_package(literal);
211            if self.version_compare(&arena[package], &arena[best], operator) {
212                best = package;
213                best_literals = vec![literal];
214            } else if self.version_compare(&arena[package], &arena[best], Op::Eq) {
215                best_literals.push(literal);
216            }
217        }
218        best_literals
219    }
220
221    /// `pruneRemoteAliases`.
222    fn prune_remote_aliases(pool: &Pool, arena: &[Package], literals: &[i64]) -> Vec<i64> {
223        let is_root_alias = |l: i64| {
224            let p = &arena[pool.literal_to_package(l)];
225            p.is_alias() && p.root_package_alias
226        };
227        if !literals.iter().any(|&l| is_root_alias(l)) {
228            return literals.to_vec();
229        }
230        literals
231            .iter()
232            .copied()
233            .filter(|&l| is_root_alias(l))
234            .collect()
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241    use crate::package::Origin;
242
243    /// Caches are per pool (`spl_object_id($pool)`): the same policy serves
244    /// the optimizer (full pool) and then the solver (reduced pool,
245    /// renumbered ids).
246    #[test]
247    fn caches_are_scoped_to_the_pool() {
248        let mut arena = vec![Package::new(
249            "a/a",
250            "1.0.0.0",
251            "1.0.0",
252            Origin::Repository(0),
253        )];
254        let alias = arena[0].alias(0, "9999999-dev", "dev-main");
255        arena.push(alias);
256        arena.push(Package::new(
257            "b/b",
258            "1.0.0.0",
259            "1.0.0",
260            Origin::Repository(0),
261        ));
262        arena.push(Package::new(
263            "c/c",
264            "1.0.0.0",
265            "1.0.0",
266            Origin::Repository(0),
267        ));
268        let pool_a = Pool::new(vec![0, 1], Vec::new(), &arena);
269        let pool_b = Pool::new(vec![2, 3], Vec::new(), &arena);
270        let mut fresh = DefaultPolicy::new(true, false, None);
271        let expected = fresh.select_preferred_packages(&pool_b, &arena, &[1, 2], None);
272        let mut shared = DefaultPolicy::new(true, false, None);
273        shared.select_preferred_packages(&pool_a, &arena, &[1, 2], None);
274        assert_eq!(
275            shared.select_preferred_packages(&pool_b, &arena, &[1, 2], None),
276            expected
277        );
278        assert_eq!(expected, vec![1, 2]);
279    }
280}