Skip to main content

uv_resolver/
error.rs

1use std::collections::{BTreeMap, BTreeSet, Bound};
2use std::fmt::Formatter;
3use std::sync::{Arc, OnceLock};
4
5use indexmap::IndexSet;
6use itertools::Itertools;
7use owo_colors::OwoColorize;
8use pubgrub::{
9    DefaultStringReporter, DerivationTree, Derived, External, Range, Ranges, Reporter, Term,
10};
11use rustc_hash::FxHashMap;
12use tracing::trace;
13
14use uv_distribution_types::{
15    DerivationChain, DistErrorKind, IndexCapabilities, IndexLocations, IndexUrl, RequestedDist,
16};
17use uv_normalize::{ExtraName, InvalidNameError, PackageName};
18use uv_pep440::{LocalVersionSlice, LowerBound, Version};
19use uv_pep508::MarkerEnvironment;
20use uv_platform_tags::Tags;
21use uv_pypi_types::ParsedUrl;
22use uv_redacted::DisplaySafeUrl;
23use uv_static::EnvVars;
24
25use crate::candidate_selector::CandidateSelector;
26use crate::dependency_provider::UvDependencyProvider;
27use crate::fork_indexes::ForkIndexes;
28use crate::fork_urls::ForkUrls;
29use crate::prerelease::AllowPrerelease;
30use crate::pubgrub::{PubGrubHint, PubGrubPackage, PubGrubPackageInner, PubGrubReportFormatter};
31use crate::python_requirement::PythonRequirement;
32use crate::resolution::ConflictingDistributionError;
33use crate::resolver::{
34    MetadataUnavailable, ResolverEnvironment, UnavailablePackage, UnavailableReason,
35};
36use crate::{InMemoryIndex, Options};
37
38#[derive(Debug, thiserror::Error)]
39pub enum ResolveError {
40    #[error("Failed to resolve dependencies for package `{1}=={2}`")]
41    Dependencies(#[source] Box<Self>, PackageName, Version, DerivationChain),
42
43    #[error(transparent)]
44    Client(#[from] uv_client::Error),
45
46    #[error(transparent)]
47    Distribution(#[from] uv_distribution::Error),
48
49    #[error("The channel closed unexpectedly")]
50    ChannelClosed,
51
52    #[error("Attempted to wait on an unregistered task: `{_0}`")]
53    UnregisteredTask(String),
54
55    #[error(
56        "Requirements contain conflicting URLs for package `{package_name}`{}:\n- {}",
57        if env.marker_environment().is_some() {
58            String::new()
59        } else {
60            format!(" in {env}")
61        },
62        urls.iter()
63            .map(|url| format!("{}{}", DisplaySafeUrl::from(url.clone()), if url.is_editable() { " (editable)" } else { "" }))
64            .collect::<Vec<_>>()
65            .join("\n- ")
66    )]
67    ConflictingUrls {
68        package_name: PackageName,
69        urls: Vec<ParsedUrl>,
70        env: ResolverEnvironment,
71    },
72
73    #[error(
74        "Requirements contain conflicting indexes for package `{package_name}`{}:\n- {}",
75        if env.marker_environment().is_some() {
76            String::new()
77        } else {
78            format!(" in {env}")
79        },
80        indexes.iter()
81            .map(std::string::ToString::to_string)
82            .collect::<Vec<_>>()
83            .join("\n- ")
84    )]
85    ConflictingIndexesForEnvironment {
86        package_name: PackageName,
87        indexes: Vec<IndexUrl>,
88        env: ResolverEnvironment,
89    },
90
91    #[error("Requirements contain conflicting indexes for package `{0}`: `{1}` vs. `{2}`")]
92    ConflictingIndexes(PackageName, String, String),
93
94    #[error(
95        "Package `{name}` was included as a URL dependency. URL dependencies must be expressed as direct requirements or constraints. Consider adding `{requirement}` to your dependencies or constraints file.",
96        name = name.cyan(),
97        requirement = format!("{name} @ {url}").cyan(),
98    )]
99    DisallowedUrl { name: PackageName, url: String },
100
101    #[error(transparent)]
102    DistributionType(#[from] uv_distribution_types::Error),
103
104    #[error("{0} `{1}`")]
105    Dist(
106        DistErrorKind,
107        Box<RequestedDist>,
108        DerivationChain,
109        #[source] Arc<uv_distribution::Error>,
110    ),
111
112    #[error(transparent)]
113    NoSolution(#[from] Box<NoSolutionError>),
114
115    #[error("Attempted to construct an invalid version specifier")]
116    InvalidVersion(#[from] uv_pep440::VersionSpecifierBuildError),
117
118    #[error(
119        "In `--require-hashes` mode, all requirements must be pinned upfront with `==`, but found: `{0}`"
120    )]
121    UnhashedPackage(PackageName),
122
123    #[error("found conflicting distribution in resolution: {0}")]
124    ConflictingDistribution(ConflictingDistributionError),
125
126    #[error("Package `{0}` is unavailable")]
127    PackageUnavailable(PackageName),
128
129    #[error("Invalid extra value in conflict marker: {reason}: {raw_extra}")]
130    InvalidExtraInConflictMarker {
131        reason: String,
132        raw_extra: ExtraName,
133    },
134
135    #[error("Invalid {kind} value in conflict marker: {name_error}")]
136    InvalidValueInConflictMarker {
137        kind: &'static str,
138        #[source]
139        name_error: InvalidNameError,
140    },
141    #[error(
142        "The index returned metadata for the wrong package: expected {request} for {expected}, got {request} for {actual}"
143    )]
144    MismatchedPackageName {
145        request: &'static str,
146        expected: PackageName,
147        actual: PackageName,
148    },
149}
150
151impl uv_errors::Hint for ResolveError {
152    fn hints(&self) -> uv_errors::Hints<'_> {
153        match self {
154            Self::NoSolution(no_solution) => uv_errors::Hint::hints(no_solution.as_ref()),
155            _ => uv_errors::Hints::none(),
156        }
157    }
158}
159
160impl<T> From<tokio::sync::mpsc::error::SendError<T>> for ResolveError {
161    /// Drop the value we want to send to not leak the private type we're sending.
162    /// The tokio error only says "channel closed", so we don't lose information.
163    fn from(_value: tokio::sync::mpsc::error::SendError<T>) -> Self {
164        Self::ChannelClosed
165    }
166}
167
168pub type ErrorTree = DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>;
169
170/// A wrapper around [`pubgrub::error::NoSolutionError`] that displays a resolution failure report.
171pub struct NoSolutionError {
172    error: pubgrub::NoSolutionError<UvDependencyProvider>,
173    index: InMemoryIndex,
174    /// The versions that were available for each package after `exclude-newer` filtering.
175    ///
176    /// For versions available before filtering, see [`NoSolutionError::available_versions`].
177    included_versions: FxHashMap<PackageName, BTreeSet<Version>>,
178    /// The versions available for each package.
179    ///
180    /// These version sets are not filtered by `exclude-newer`. See
181    /// [`NoSolutionError::included_versions`] instead if filtered versions are needed.
182    ///
183    /// These versions are filtered by [`EnvVars::UV_TEST_AVAILABLE_VERSION_CUTOFF`] for
184    /// deterministic output in tests.
185    available_versions: FxHashMap<PackageName, BTreeSet<Version>>,
186    available_indexes: FxHashMap<PackageName, BTreeSet<IndexUrl>>,
187    selector: CandidateSelector,
188    python_requirement: PythonRequirement,
189    index_locations: IndexLocations,
190    index_capabilities: IndexCapabilities,
191    unavailable_packages: FxHashMap<PackageName, UnavailablePackage>,
192    incomplete_packages: FxHashMap<PackageName, BTreeMap<Version, MetadataUnavailable>>,
193    fork_urls: ForkUrls,
194    fork_indexes: ForkIndexes,
195    env: ResolverEnvironment,
196    current_environment: MarkerEnvironment,
197    tags: Option<Tags>,
198    workspace_members: BTreeSet<PackageName>,
199    options: Options,
200    /// Cached report and hints, computed once on first access.
201    cached: OnceLock<(String, IndexSet<PubGrubHint>)>,
202}
203
204impl NoSolutionError {
205    /// Create a new [`NoSolutionError`] from a [`pubgrub::NoSolutionError`].
206    pub(crate) fn new(
207        error: pubgrub::NoSolutionError<UvDependencyProvider>,
208        index: InMemoryIndex,
209        included_versions: FxHashMap<PackageName, BTreeSet<Version>>,
210        available_versions: FxHashMap<PackageName, BTreeSet<Version>>,
211        available_indexes: FxHashMap<PackageName, BTreeSet<IndexUrl>>,
212        selector: CandidateSelector,
213        python_requirement: PythonRequirement,
214        index_locations: IndexLocations,
215        index_capabilities: IndexCapabilities,
216        unavailable_packages: FxHashMap<PackageName, UnavailablePackage>,
217        incomplete_packages: FxHashMap<PackageName, BTreeMap<Version, MetadataUnavailable>>,
218        fork_urls: ForkUrls,
219        fork_indexes: ForkIndexes,
220        env: ResolverEnvironment,
221        current_environment: MarkerEnvironment,
222        tags: Option<Tags>,
223        workspace_members: BTreeSet<PackageName>,
224        options: Options,
225    ) -> Self {
226        Self {
227            error,
228            index,
229            included_versions,
230            available_versions,
231            available_indexes,
232            selector,
233            python_requirement,
234            index_locations,
235            index_capabilities,
236            unavailable_packages,
237            incomplete_packages,
238            fork_urls,
239            fork_indexes,
240            env,
241            current_environment,
242            tags,
243            workspace_members,
244            options,
245            cached: OnceLock::new(),
246        }
247    }
248
249    /// Get the cached report and hints, computing them on first access.
250    fn cached(&self) -> &(String, IndexSet<PubGrubHint>) {
251        self.cached.get_or_init(|| self.compute_report_and_hints())
252    }
253
254    /// Given a [`DerivationTree`], collapse any [`External::FromDependencyOf`] incompatibilities
255    /// wrap an [`PubGrubPackageInner::Extra`] package.
256    pub(crate) fn collapse_proxies(derivation_tree: ErrorTree) -> ErrorTree {
257        fn collapse(derivation_tree: ErrorTree) -> Option<ErrorTree> {
258            match derivation_tree {
259                DerivationTree::Derived(derived) => {
260                    match (&*derived.cause1, &*derived.cause2) {
261                        (
262                            DerivationTree::External(External::FromDependencyOf(package1, ..)),
263                            DerivationTree::External(External::FromDependencyOf(package2, ..)),
264                        ) if package1.is_proxy() && package2.is_proxy() => None,
265                        (
266                            DerivationTree::External(External::FromDependencyOf(package, ..)),
267                            cause,
268                        ) if package.is_proxy() => collapse(cause.clone()),
269                        (
270                            cause,
271                            DerivationTree::External(External::FromDependencyOf(package, ..)),
272                        ) if package.is_proxy() => collapse(cause.clone()),
273                        (cause1, cause2) => {
274                            let cause1 = collapse(cause1.clone());
275                            let cause2 = collapse(cause2.clone());
276                            match (cause1, cause2) {
277                                (Some(cause1), Some(cause2)) => {
278                                    Some(DerivationTree::Derived(Derived {
279                                        cause1: Arc::new(cause1),
280                                        cause2: Arc::new(cause2),
281                                        ..derived
282                                    }))
283                                }
284                                (Some(cause), None) | (None, Some(cause)) => Some(cause),
285                                _ => None,
286                            }
287                        }
288                    }
289                }
290                DerivationTree::External(_) => Some(derivation_tree),
291            }
292        }
293
294        collapse(derivation_tree)
295            .expect("derivation tree should contain at least one external term")
296    }
297
298    /// Simplifies the version ranges on any incompatibilities to remove the `[max]` sentinel.
299    ///
300    /// The `[max]` sentinel is used to represent the maximum local version of a package, to
301    /// implement PEP 440 semantics for local version equality. For example, `1.0.0+foo` needs to
302    /// satisfy `==1.0.0`.
303    pub(crate) fn collapse_local_version_segments(derivation_tree: ErrorTree) -> ErrorTree {
304        fn strip(derivation_tree: ErrorTree) -> Option<ErrorTree> {
305            match derivation_tree {
306                DerivationTree::External(External::NotRoot(_, _)) => Some(derivation_tree),
307                DerivationTree::External(External::NoVersions(package, versions)) => {
308                    if SentinelRange::from(&versions).is_complement() {
309                        return None;
310                    }
311
312                    let versions = SentinelRange::from(&versions).strip();
313                    Some(DerivationTree::External(External::NoVersions(
314                        package, versions,
315                    )))
316                }
317                DerivationTree::External(External::FromDependencyOf(
318                    package1,
319                    versions1,
320                    package2,
321                    versions2,
322                )) => {
323                    let versions1 = SentinelRange::from(&versions1).strip();
324                    let versions2 = SentinelRange::from(&versions2).strip();
325                    Some(DerivationTree::External(External::FromDependencyOf(
326                        package1, versions1, package2, versions2,
327                    )))
328                }
329                DerivationTree::External(External::Custom(package, versions, reason)) => {
330                    let versions = SentinelRange::from(&versions).strip();
331                    Some(DerivationTree::External(External::Custom(
332                        package, versions, reason,
333                    )))
334                }
335                DerivationTree::Derived(mut derived) => {
336                    let cause1 = strip((*derived.cause1).clone());
337                    let cause2 = strip((*derived.cause2).clone());
338                    match (cause1, cause2) {
339                        (Some(cause1), Some(cause2)) => Some(DerivationTree::Derived(Derived {
340                            cause1: Arc::new(cause1),
341                            cause2: Arc::new(cause2),
342                            terms: std::mem::take(&mut derived.terms)
343                                .into_iter()
344                                .map(|(pkg, term)| {
345                                    let term = match term {
346                                        Term::Positive(versions) => {
347                                            Term::Positive(SentinelRange::from(&versions).strip())
348                                        }
349                                        Term::Negative(versions) => {
350                                            Term::Negative(SentinelRange::from(&versions).strip())
351                                        }
352                                    };
353                                    (pkg, term)
354                                })
355                                .collect(),
356                            shared_id: derived.shared_id,
357                        })),
358                        (Some(cause), None) | (None, Some(cause)) => Some(cause),
359                        _ => None,
360                    }
361                }
362            }
363        }
364
365        strip(derivation_tree).expect("derivation tree should contain at least one term")
366    }
367
368    /// Given a [`DerivationTree`], identify the largest required Python version that is missing.
369    pub fn find_requires_python(&self) -> LowerBound {
370        fn find(derivation_tree: &ErrorTree, minimum: &mut LowerBound) {
371            match derivation_tree {
372                DerivationTree::Derived(derived) => {
373                    find(derived.cause1.as_ref(), minimum);
374                    find(derived.cause2.as_ref(), minimum);
375                }
376                DerivationTree::External(External::FromDependencyOf(.., package, version)) => {
377                    if let PubGrubPackageInner::Python(_) = &**package {
378                        if let Some((lower, ..)) = version.bounding_range() {
379                            let lower = LowerBound::new(lower.cloned());
380                            if lower > *minimum {
381                                *minimum = lower;
382                            }
383                        }
384                    }
385                }
386                DerivationTree::External(_) => {}
387            }
388        }
389
390        let mut minimum = LowerBound::default();
391        find(&self.error, &mut minimum);
392        minimum
393    }
394
395    /// Initialize a [`NoSolutionHeader`] for this error.
396    pub fn header(&self) -> NoSolutionHeader {
397        NoSolutionHeader::new(self.env.clone())
398    }
399
400    /// Get the packages that are involved in this error.
401    pub fn packages(&self) -> impl Iterator<Item = &PackageName> {
402        self.error
403            .packages()
404            .into_iter()
405            .filter_map(|p| p.name())
406            .unique()
407    }
408
409    /// Generate the report and hints for this resolution failure.
410    ///
411    /// Returns the formatted report string and structured [`PubGrubHint`] values.
412    /// The result is cached so repeated calls (e.g., from both `Display` and
413    /// explicit hint collection) don't recompute the derivation tree.
414    /// Return the formatted report string.
415    pub fn report(&self) -> &str {
416        &self.cached().0
417    }
418
419    /// Return the computed PubGrub hints.
420    pub fn pubgrub_hints(&self) -> &IndexSet<PubGrubHint> {
421        &self.cached().1
422    }
423
424    /// Compute the reduced derivation tree, formatted report string, and hints.
425    fn compute_report_and_hints(&self) -> (String, IndexSet<PubGrubHint>) {
426        let formatter = PubGrubReportFormatter {
427            included_versions: &self.included_versions,
428            available_versions: &self.available_versions,
429            python_requirement: &self.python_requirement,
430            workspace_members: &self.workspace_members,
431            tags: self.tags.as_ref(),
432        };
433
434        // Transform the error tree for reporting
435        let mut tree = self.error.clone();
436        simplify_derivation_tree_markers(&self.python_requirement, &mut tree);
437        let should_display_tree = std::env::var_os(EnvVars::UV_INTERNAL__SHOW_DERIVATION_TREE)
438            .is_some()
439            || tracing::enabled!(tracing::Level::TRACE);
440
441        if should_display_tree {
442            display_tree(&tree, "Resolver derivation tree before reduction");
443        }
444
445        collapse_no_versions_of_workspace_members(&mut tree, &self.workspace_members);
446
447        if self.workspace_members.len() == 1 {
448            let project = self.workspace_members.iter().next().unwrap();
449            drop_root_dependency_on_project(&mut tree, project);
450        }
451
452        collapse_unavailable_versions(&mut tree);
453        collapse_redundant_depends_on_no_versions(&mut tree);
454
455        simplify_derivation_tree_ranges(
456            &mut tree,
457            &self.included_versions,
458            &self.selector,
459            &self.env,
460        );
461
462        // This needs to be applied _after_ simplification of the ranges
463        collapse_redundant_no_versions(&mut tree);
464
465        while collapse_redundant_no_versions_tree(&mut tree) {
466            // Continue collapsing until no more redundant nodes are found
467        }
468
469        if should_display_tree {
470            display_tree(&tree, "Resolver derivation tree after reduction");
471        }
472
473        let report = DefaultStringReporter::report_with_formatter(&tree, &formatter);
474
475        let inherited_exclude_newer_ranges = FxHashMap::default();
476        let mut hints = IndexSet::default();
477        formatter.generate_hints(
478            &tree,
479            &self.index,
480            &self.selector,
481            &self.index_locations,
482            &self.index_capabilities,
483            &self.available_indexes,
484            &self.unavailable_packages,
485            &self.incomplete_packages,
486            &self.fork_urls,
487            &self.fork_indexes,
488            &self.env,
489            &self.current_environment,
490            self.tags.as_ref(),
491            &self.workspace_members,
492            &self.options,
493            &inherited_exclude_newer_ranges,
494            &mut hints,
495        );
496
497        (report, hints)
498    }
499}
500
501impl std::fmt::Debug for NoSolutionError {
502    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
503        // Include every field except `index` (no Debug) and `cached` (derived).
504        let Self {
505            error,
506            index: _,
507            included_versions,
508            available_versions,
509            available_indexes,
510            selector,
511            python_requirement,
512            index_locations,
513            index_capabilities,
514            unavailable_packages,
515            incomplete_packages,
516            fork_urls,
517            fork_indexes,
518            env,
519            current_environment,
520            tags,
521            workspace_members,
522            options,
523            cached: _,
524        } = self;
525        f.debug_struct("NoSolutionError")
526            .field("error", error)
527            .field("included_versions", included_versions)
528            .field("available_versions", available_versions)
529            .field("available_indexes", available_indexes)
530            .field("selector", selector)
531            .field("python_requirement", python_requirement)
532            .field("index_locations", index_locations)
533            .field("index_capabilities", index_capabilities)
534            .field("unavailable_packages", unavailable_packages)
535            .field("incomplete_packages", incomplete_packages)
536            .field("fork_urls", fork_urls)
537            .field("fork_indexes", fork_indexes)
538            .field("env", env)
539            .field("current_environment", current_environment)
540            .field("tags", tags)
541            .field("workspace_members", workspace_members)
542            .field("options", options)
543            .finish()
544    }
545}
546
547impl std::error::Error for NoSolutionError {}
548
549impl uv_errors::Hint for NoSolutionError {
550    fn hints(&self) -> uv_errors::Hints<'_> {
551        self.pubgrub_hints()
552            .iter()
553            .map(ToString::to_string)
554            .collect()
555    }
556}
557
558impl uv_errors::Hint for Box<NoSolutionError> {
559    fn hints(&self) -> uv_errors::Hints<'_> {
560        self.as_ref().hints()
561    }
562}
563
564impl std::fmt::Display for NoSolutionError {
565    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
566        // Write only the derivation report. Hints are available separately
567        // via `hints()` and rendered by the caller.
568        write!(f, "{}", self.report())
569    }
570}
571
572#[expect(clippy::print_stderr)]
573fn display_tree(
574    error: &DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
575    name: &str,
576) {
577    let mut lines = Vec::new();
578    display_tree_inner(error, &mut lines, 0);
579    lines.reverse();
580
581    if std::env::var_os(EnvVars::UV_INTERNAL__SHOW_DERIVATION_TREE).is_some() {
582        eprintln!("{name}\n{}", lines.join("\n"));
583    } else {
584        trace!("{name}\n{}", lines.join("\n"));
585    }
586}
587
588fn display_tree_inner(
589    error: &DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
590    lines: &mut Vec<String>,
591    depth: usize,
592) {
593    let prefix = "  ".repeat(depth);
594    match error {
595        DerivationTree::Derived(derived) => {
596            display_tree_inner(&derived.cause1, lines, depth + 1);
597            display_tree_inner(&derived.cause2, lines, depth + 1);
598            for (package, term) in &derived.terms {
599                match term {
600                    Term::Positive(versions) => {
601                        lines.push(format!("{prefix}term {package}{versions}"));
602                    }
603                    Term::Negative(versions) => {
604                        lines.push(format!("{prefix}term not {package}{versions}"));
605                    }
606                }
607            }
608        }
609        DerivationTree::External(external) => match external {
610            External::FromDependencyOf(package, version, dependency, dependency_version) => {
611                lines.push(format!(
612                    "{prefix}{package}{version} depends on {dependency}{dependency_version}"
613                ));
614            }
615            External::Custom(package, versions, reason) => match reason {
616                UnavailableReason::Package(_) => {
617                    lines.push(format!("{prefix}{package} {reason}"));
618                }
619                UnavailableReason::Version(_) => {
620                    lines.push(format!("{prefix}{package}{versions} {reason}"));
621                }
622            },
623            External::NoVersions(package, versions) => {
624                lines.push(format!("{prefix}no versions of {package}{versions}"));
625            }
626            External::NotRoot(package, versions) => {
627                lines.push(format!("{prefix}not root {package}{versions}"));
628            }
629        },
630    }
631}
632
633fn collapse_redundant_no_versions(
634    tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
635) {
636    match tree {
637        DerivationTree::External(_) => {}
638        DerivationTree::Derived(derived) => {
639            match (
640                Arc::make_mut(&mut derived.cause1),
641                Arc::make_mut(&mut derived.cause2),
642            ) {
643                // If we have a node for a package with no versions...
644                (
645                    DerivationTree::External(External::NoVersions(package, versions)),
646                    ref mut other,
647                )
648                | (
649                    ref mut other,
650                    DerivationTree::External(External::NoVersions(package, versions)),
651                ) => {
652                    // First, always recursively visit the other side of the tree
653                    collapse_redundant_no_versions(other);
654
655                    // Retrieve the nearest terms, either alongside this node or from the parent.
656                    let package_terms = if let DerivationTree::Derived(derived) = other {
657                        derived.terms.get(package)
658                    } else {
659                        derived.terms.get(package)
660                    };
661
662                    let Some(Term::Positive(term)) = package_terms else {
663                        return;
664                    };
665
666                    let versions = versions.complement();
667
668                    // If we're disqualifying a single version, this is important to retain, e.g,
669                    // for `only foo==1.0.0 is available`
670                    if versions.as_singleton().is_some() {
671                        return;
672                    }
673
674                    // If the range in the conclusion (terms) matches the range of no versions,
675                    // then we'll drop this node. If the range is "all versions", then there's no
676                    // also no need to enumerate the available versions.
677                    if *term != Range::full() && *term != versions {
678                        return;
679                    }
680
681                    *tree = other.clone();
682                }
683                // If not, just recurse
684                _ => {
685                    collapse_redundant_no_versions(Arc::make_mut(&mut derived.cause1));
686                    collapse_redundant_no_versions(Arc::make_mut(&mut derived.cause2));
687                }
688            }
689        }
690    }
691}
692
693/// Given a [`DerivationTree`], collapse any derived trees with two `NoVersions` nodes for the same
694/// package. For example, if we have a tree like:
695///
696/// ```text
697/// term Python>=3.7.9
698///   no versions of Python>=3.7.9, <3.8
699///   no versions of Python>=3.8
700/// ```
701///
702/// We can simplify this to:
703///
704/// ```text
705/// no versions of Python>=3.7.9
706/// ```
707///
708/// This function returns a `bool` indicating if a change was made. This allows for repeated calls,
709/// e.g., the following tree contains nested redundant trees:
710///
711/// ```text
712/// term Python>=3.10
713///   no versions of Python>=3.11, <3.12
714///   term Python>=3.10, <3.11 | >=3.12
715///     no versions of Python>=3.12
716///     no versions of Python>=3.10, <3.11
717/// ```
718///
719/// We can simplify this to:
720///
721/// ```text
722/// no versions of Python>=3.10
723/// ```
724///
725/// This appears to be common with the way the resolver currently models Python version
726/// incompatibilities.
727fn collapse_redundant_no_versions_tree(
728    tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
729) -> bool {
730    match tree {
731        DerivationTree::External(_) => false,
732        DerivationTree::Derived(derived) => {
733            match (
734                Arc::make_mut(&mut derived.cause1),
735                Arc::make_mut(&mut derived.cause2),
736            ) {
737                // If we have a tree with two `NoVersions` nodes for the same package...
738                (
739                    DerivationTree::External(External::NoVersions(package, versions)),
740                    DerivationTree::External(External::NoVersions(other_package, other_versions)),
741                ) if package == other_package => {
742                    // Retrieve the terms from the parent.
743                    let Some(Term::Positive(term)) = derived.terms.get(package) else {
744                        return false;
745                    };
746
747                    // If they're both subsets of the term, then drop this node in favor of the term
748                    if versions.subset_of(term) && other_versions.subset_of(term) {
749                        *tree = DerivationTree::External(External::NoVersions(
750                            package.clone(),
751                            term.clone(),
752                        ));
753                        return true;
754                    }
755
756                    false
757                }
758                // If not, just recurse
759                _ => {
760                    collapse_redundant_no_versions_tree(Arc::make_mut(&mut derived.cause1))
761                        || collapse_redundant_no_versions_tree(Arc::make_mut(&mut derived.cause2))
762                }
763            }
764        }
765    }
766}
767
768/// Given a [`DerivationTree`], collapse any `NoVersion` incompatibilities for workspace members
769/// to avoid saying things like "only <workspace-member>==0.1.0 is available".
770fn collapse_no_versions_of_workspace_members(
771    tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
772    workspace_members: &BTreeSet<PackageName>,
773) {
774    match tree {
775        DerivationTree::External(_) => {}
776        DerivationTree::Derived(derived) => {
777            match (
778                Arc::make_mut(&mut derived.cause1),
779                Arc::make_mut(&mut derived.cause2),
780            ) {
781                // If we have a node for a package with no versions...
782                (DerivationTree::External(External::NoVersions(package, _)), ref mut other)
783                | (ref mut other, DerivationTree::External(External::NoVersions(package, _))) => {
784                    // First, always recursively visit the other side of the tree
785                    collapse_no_versions_of_workspace_members(other, workspace_members);
786
787                    // Then, if the package is a workspace member...
788                    let (PubGrubPackageInner::Package { name, .. }
789                    | PubGrubPackageInner::Extra { name, .. }
790                    | PubGrubPackageInner::Group { name, .. }) = &**package
791                    else {
792                        return;
793                    };
794                    if !workspace_members.contains(name) {
795                        return;
796                    }
797
798                    // Replace this node with the other tree
799                    *tree = other.clone();
800                }
801                // If not, just recurse
802                _ => {
803                    collapse_no_versions_of_workspace_members(
804                        Arc::make_mut(&mut derived.cause1),
805                        workspace_members,
806                    );
807                    collapse_no_versions_of_workspace_members(
808                        Arc::make_mut(&mut derived.cause2),
809                        workspace_members,
810                    );
811                }
812            }
813        }
814    }
815}
816
817/// Given a [`DerivationTree`], collapse `NoVersions` incompatibilities that are redundant children
818/// of a dependency. For example, if we have a tree like:
819///
820/// ```text
821/// A>=1,<2 depends on B
822///     A has no versions >1,<2
823///     C depends on A>=1,<2
824/// ```
825///
826/// We can simplify this to `C depends A>=1 and A>=1 depends on B so C depends on B` without
827/// explaining that there are no other versions of A. This is dependent on range of A in "A depends
828/// on" being a subset of range of A in "depends on A". For example, in a tree like:
829///
830/// ```text
831/// A>=1,<3 depends on B
832///     A has no versions >2,<3
833///     C depends on A>=2,<3
834/// ```
835///
836/// We cannot say `C depends on A>=2 and A>=1 depends on B so C depends on B` because there is a
837/// hole in the range — `A>=1,<3` is not a subset of `A>=2,<3`.
838fn collapse_redundant_depends_on_no_versions(
839    tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
840) {
841    match tree {
842        DerivationTree::External(_) => {}
843        DerivationTree::Derived(derived) => {
844            // If one node is a dependency incompatibility...
845            match (
846                Arc::make_mut(&mut derived.cause1),
847                Arc::make_mut(&mut derived.cause2),
848            ) {
849                (
850                    DerivationTree::External(External::FromDependencyOf(package, versions, _, _)),
851                    ref mut other,
852                )
853                | (
854                    ref mut other,
855                    DerivationTree::External(External::FromDependencyOf(package, versions, _, _)),
856                ) => {
857                    // Check if the other node is the relevant form of subtree...
858                    collapse_redundant_depends_on_no_versions_inner(other, package, versions);
859                }
860                // If not, just recurse
861                _ => {
862                    collapse_redundant_depends_on_no_versions(Arc::make_mut(&mut derived.cause1));
863                    collapse_redundant_depends_on_no_versions(Arc::make_mut(&mut derived.cause2));
864                }
865            }
866        }
867    }
868}
869
870/// Helper for [`collapse_redundant_depends_on_no_versions`].
871fn collapse_redundant_depends_on_no_versions_inner(
872    tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
873    package: &PubGrubPackage,
874    versions: &Range<Version>,
875) {
876    match tree {
877        DerivationTree::External(_) => {}
878        DerivationTree::Derived(derived) => {
879            // If we're a subtree with dependency and no versions incompatibilities...
880            match (&*derived.cause1, &*derived.cause2) {
881                (
882                    DerivationTree::External(External::NoVersions(no_versions_package, _)),
883                    dependency_clause @ DerivationTree::External(External::FromDependencyOf(
884                        _,
885                        _,
886                        dependency_package,
887                        dependency_versions,
888                    )),
889                )
890                | (
891                    dependency_clause @ DerivationTree::External(External::FromDependencyOf(
892                        _,
893                        _,
894                        dependency_package,
895                        dependency_versions,
896                    )),
897                    DerivationTree::External(External::NoVersions(no_versions_package, _)),
898                )
899                // And these incompatibilities (and the parent incompatibility) all are referring to
900                // the same package...
901                if no_versions_package == dependency_package
902                    && package == no_versions_package
903                // And parent dependency versions are a subset of the versions in this tree...
904                    && versions.subset_of(dependency_versions) =>
905                {
906                    // Enumerating the available versions will be redundant and we can drop the no
907                    // versions clause entirely in favor of the dependency clause.
908                    *tree = dependency_clause.clone();
909
910                    // Note we are at a leaf of the tree so there's no further recursion to do
911                }
912                // If not, just recurse
913                _ => {
914                    collapse_redundant_depends_on_no_versions(Arc::make_mut(&mut derived.cause1));
915                    collapse_redundant_depends_on_no_versions(Arc::make_mut(&mut derived.cause2));
916                }
917            }
918        }
919    }
920}
921
922/// Simplifies the markers on pubgrub packages in the given derivation tree
923/// according to the given Python requirement.
924///
925/// For example, when there's a dependency like `foo ; python_version >=
926/// '3.11'` and `requires-python = '>=3.11'`, this simplification will remove
927/// the `python_version >= '3.11'` marker since it's implied to be true by
928/// the `requires-python` setting. This simplifies error messages by reducing
929/// noise.
930fn simplify_derivation_tree_markers(
931    python_requirement: &PythonRequirement,
932    tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
933) {
934    match tree {
935        DerivationTree::External(External::NotRoot(pkg, _)) => {
936            pkg.simplify_markers(python_requirement);
937        }
938        DerivationTree::External(External::NoVersions(pkg, _)) => {
939            pkg.simplify_markers(python_requirement);
940        }
941        DerivationTree::External(External::FromDependencyOf(pkg1, _, pkg2, _)) => {
942            pkg1.simplify_markers(python_requirement);
943            pkg2.simplify_markers(python_requirement);
944        }
945        DerivationTree::External(External::Custom(pkg, _, _)) => {
946            pkg.simplify_markers(python_requirement);
947        }
948        DerivationTree::Derived(derived) => {
949            derived.terms = std::mem::take(&mut derived.terms)
950                .into_iter()
951                .map(|(mut pkg, term)| {
952                    pkg.simplify_markers(python_requirement);
953                    (pkg, term)
954                })
955                .collect();
956            simplify_derivation_tree_markers(
957                python_requirement,
958                Arc::make_mut(&mut derived.cause1),
959            );
960            simplify_derivation_tree_markers(
961                python_requirement,
962                Arc::make_mut(&mut derived.cause2),
963            );
964        }
965    }
966}
967
968/// Given a [`DerivationTree`], collapse incompatibilities for versions of a package that are
969/// unavailable for the same reason to avoid repeating the same message for every unavailable
970/// version.
971fn collapse_unavailable_versions(
972    tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
973) {
974    match tree {
975        DerivationTree::External(_) => {}
976        DerivationTree::Derived(derived) => {
977            match (
978                Arc::make_mut(&mut derived.cause1),
979                Arc::make_mut(&mut derived.cause2),
980            ) {
981                // If we have a node for unavailable package versions
982                (
983                    DerivationTree::External(External::Custom(package, versions, reason)),
984                    ref mut other,
985                )
986                | (
987                    ref mut other,
988                    DerivationTree::External(External::Custom(package, versions, reason)),
989                ) => {
990                    // First, recursively collapse the other side of the tree
991                    collapse_unavailable_versions(other);
992
993                    // If it's not a derived tree, nothing to do.
994                    let DerivationTree::Derived(Derived {
995                        terms,
996                        shared_id,
997                        cause1,
998                        cause2,
999                    }) = other
1000                    else {
1001                        return;
1002                    };
1003
1004                    // If the other tree has an unavailable package...
1005                    match (&**cause1, &**cause2) {
1006                        // Note the following cases are the same, but we need two matches to retain
1007                        // the ordering of the causes
1008                        (
1009                            _,
1010                            DerivationTree::External(External::Custom(
1011                                other_package,
1012                                other_versions,
1013                                other_reason,
1014                            )),
1015                        ) if package == other_package && reason == other_reason => {
1016                            // Collapse both into a new node, with a union of their ranges
1017                            let versions = other_versions.union(versions);
1018                            let mut terms = terms.clone();
1019                            if let Some(Term::Positive(range)) = terms.get_mut(package) {
1020                                *range = versions.clone();
1021                            }
1022                            *tree = DerivationTree::Derived(Derived {
1023                                terms,
1024                                shared_id: *shared_id,
1025                                cause1: cause1.clone(),
1026                                cause2: Arc::new(DerivationTree::External(External::Custom(
1027                                    package.clone(),
1028                                    versions,
1029                                    reason.clone(),
1030                                ))),
1031                            });
1032                        }
1033                        (
1034                            DerivationTree::External(External::Custom(
1035                                other_package,
1036                                other_versions,
1037                                other_reason,
1038                            )),
1039                            _,
1040                        ) if package == other_package && reason == other_reason => {
1041                            // Collapse both into a new node, with a union of their ranges
1042                            let versions = other_versions.union(versions);
1043                            let mut terms = terms.clone();
1044                            if let Some(Term::Positive(range)) = terms.get_mut(package) {
1045                                *range = versions.clone();
1046                            }
1047                            *tree = DerivationTree::Derived(Derived {
1048                                terms,
1049                                shared_id: *shared_id,
1050                                cause1: Arc::new(DerivationTree::External(External::Custom(
1051                                    package.clone(),
1052                                    versions,
1053                                    reason.clone(),
1054                                ))),
1055                                cause2: cause2.clone(),
1056                            });
1057                        }
1058                        _ => {}
1059                    }
1060                }
1061                // If not, just recurse
1062                _ => {
1063                    collapse_unavailable_versions(Arc::make_mut(&mut derived.cause1));
1064                    collapse_unavailable_versions(Arc::make_mut(&mut derived.cause2));
1065                }
1066            }
1067        }
1068    }
1069}
1070
1071/// Given a [`DerivationTree`], drop dependency incompatibilities from the root
1072/// to the project.
1073///
1074/// Intended to effectively change the root to a workspace member in single project
1075/// workspaces, avoiding a level of indirection like "And because your project
1076/// requires your project, we can conclude that your project's requirements are
1077/// unsatisfiable."
1078fn drop_root_dependency_on_project(
1079    tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
1080    project: &PackageName,
1081) {
1082    match tree {
1083        DerivationTree::External(_) => {}
1084        DerivationTree::Derived(derived) => {
1085            match (
1086                Arc::make_mut(&mut derived.cause1),
1087                Arc::make_mut(&mut derived.cause2),
1088            ) {
1089                // If one node is a dependency incompatibility...
1090                (
1091                    DerivationTree::External(External::FromDependencyOf(package, _, dependency, _)),
1092                    ref mut other,
1093                )
1094                | (
1095                    ref mut other,
1096                    DerivationTree::External(External::FromDependencyOf(package, _, dependency, _)),
1097                ) => {
1098                    // And the parent is the root package...
1099                    if !matches!(&**package, PubGrubPackageInner::Root(_)) {
1100                        return;
1101                    }
1102
1103                    // And the dependency is the project...
1104                    let PubGrubPackageInner::Package { name, .. } = &**dependency else {
1105                        return;
1106                    };
1107                    if name != project {
1108                        return;
1109                    }
1110
1111                    // Recursively collapse the other side of the tree
1112                    drop_root_dependency_on_project(other, project);
1113
1114                    // Then, replace this node with the other tree
1115                    *tree = other.clone();
1116                }
1117                // If not, just recurse
1118                _ => {
1119                    drop_root_dependency_on_project(Arc::make_mut(&mut derived.cause1), project);
1120                    drop_root_dependency_on_project(Arc::make_mut(&mut derived.cause2), project);
1121                }
1122            }
1123        }
1124    }
1125}
1126
1127/// A version range that may include local version sentinels (`+[max]`).
1128#[derive(Debug)]
1129pub struct SentinelRange<'range>(&'range Range<Version>);
1130
1131impl<'range> From<&'range Range<Version>> for SentinelRange<'range> {
1132    fn from(range: &'range Range<Version>) -> Self {
1133        Self(range)
1134    }
1135}
1136
1137impl SentinelRange<'_> {
1138    /// Returns `true` if the range appears to be, e.g., `>=1.0.0, <1.0.0+[max]`.
1139    pub(crate) fn is_sentinel(&self) -> bool {
1140        self.0.iter().all(|(lower, upper)| {
1141            let (Bound::Included(lower), Bound::Excluded(upper)) = (lower, upper) else {
1142                return false;
1143            };
1144            if !lower.local().is_empty() {
1145                return false;
1146            }
1147            if upper.local() != LocalVersionSlice::Max {
1148                return false;
1149            }
1150            *lower == upper.clone().without_local()
1151        })
1152    }
1153
1154    /// Returns `true` if the range appears to be, e.g., `>1.0.0, <1.0.0+[max]` (i.e., a sentinel
1155    /// range with the non-local version removed).
1156    pub fn is_complement(&self) -> bool {
1157        self.0.iter().all(|(lower, upper)| {
1158            let (Bound::Excluded(lower), Bound::Excluded(upper)) = (lower, upper) else {
1159                return false;
1160            };
1161            if !lower.local().is_empty() {
1162                return false;
1163            }
1164            if upper.local() != LocalVersionSlice::Max {
1165                return false;
1166            }
1167            *lower == upper.clone().without_local()
1168        })
1169    }
1170
1171    /// Remove local versions sentinels (`+[max]`) from the version ranges.
1172    pub fn strip(&self) -> Ranges<Version> {
1173        self.0
1174            .iter()
1175            .map(|(lower, upper)| Self::strip_sentinel(lower.clone(), upper.clone()))
1176            .collect()
1177    }
1178
1179    /// Remove local versions sentinels (`+[max]`) from the interval.
1180    fn strip_sentinel(
1181        mut lower: Bound<Version>,
1182        mut upper: Bound<Version>,
1183    ) -> (Bound<Version>, Bound<Version>) {
1184        match (&lower, &upper) {
1185            (Bound::Unbounded, Bound::Unbounded) => {}
1186            (Bound::Unbounded, Bound::Included(v)) => {
1187                // `<=1.0.0+[max]` is equivalent to `<=1.0.0`
1188                if v.local() == LocalVersionSlice::Max {
1189                    upper = Bound::Included(v.clone().without_local());
1190                }
1191            }
1192            (Bound::Unbounded, Bound::Excluded(v)) => {
1193                // `<1.0.0+[max]` is equivalent to `<1.0.0`
1194                if v.local() == LocalVersionSlice::Max {
1195                    upper = Bound::Excluded(v.clone().without_local());
1196                }
1197            }
1198            (Bound::Included(v), Bound::Unbounded) => {
1199                // `>=1.0.0+[max]` is equivalent to `>1.0.0`
1200                if v.local() == LocalVersionSlice::Max {
1201                    lower = Bound::Excluded(v.clone().without_local());
1202                }
1203            }
1204            (Bound::Included(v), Bound::Included(b)) => {
1205                // `>=1.0.0+[max]` is equivalent to `>1.0.0`
1206                if v.local() == LocalVersionSlice::Max {
1207                    lower = Bound::Excluded(v.clone().without_local());
1208                }
1209                // `<=1.0.0+[max]` is equivalent to `<=1.0.0`
1210                if b.local() == LocalVersionSlice::Max {
1211                    upper = Bound::Included(b.clone().without_local());
1212                }
1213            }
1214            (Bound::Included(v), Bound::Excluded(b)) => {
1215                // `>=1.0.0+[max]` is equivalent to `>1.0.0`
1216                if v.local() == LocalVersionSlice::Max {
1217                    lower = Bound::Excluded(v.clone().without_local());
1218                }
1219                // `<1.0.0+[max]` is equivalent to `<1.0.0`
1220                if b.local() == LocalVersionSlice::Max {
1221                    upper = Bound::Included(b.clone().without_local());
1222                }
1223            }
1224            (Bound::Excluded(v), Bound::Unbounded) => {
1225                // `>1.0.0+[max]` is equivalent to `>1.0.0`
1226                if v.local() == LocalVersionSlice::Max {
1227                    lower = Bound::Excluded(v.clone().without_local());
1228                }
1229            }
1230            (Bound::Excluded(v), Bound::Included(b)) => {
1231                // `>1.0.0+[max]` is equivalent to `>1.0.0`
1232                if v.local() == LocalVersionSlice::Max {
1233                    lower = Bound::Excluded(v.clone().without_local());
1234                }
1235                // `<=1.0.0+[max]` is equivalent to `<=1.0.0`
1236                if b.local() == LocalVersionSlice::Max {
1237                    upper = Bound::Included(b.clone().without_local());
1238                }
1239            }
1240            (Bound::Excluded(v), Bound::Excluded(b)) => {
1241                // `>1.0.0+[max]` is equivalent to `>1.0.0`
1242                if v.local() == LocalVersionSlice::Max {
1243                    lower = Bound::Excluded(v.clone().without_local());
1244                }
1245                // `<1.0.0+[max]` is equivalent to `<1.0.0`
1246                if b.local() == LocalVersionSlice::Max {
1247                    upper = Bound::Excluded(b.clone().without_local());
1248                }
1249            }
1250        }
1251        (lower, upper)
1252    }
1253}
1254
1255/// A prefix match, e.g., `==2.4.*`, which is desugared to a range like `>=2.4.dev0,<2.5.dev0`.
1256#[derive(Debug, Clone, PartialEq, Eq)]
1257pub(crate) struct PrefixMatch<'a> {
1258    version: &'a Version,
1259}
1260
1261impl<'a> PrefixMatch<'a> {
1262    /// Determine whether a given range is equivalent to a prefix match (e.g., `==2.4.*`).
1263    ///
1264    /// Prefix matches are desugared to (e.g.) `>=2.4.dev0,<2.5.dev0`, but we want to render them
1265    /// as `==2.4.*` in error messages.
1266    pub(crate) fn from_range(lower: &'a Bound<Version>, upper: &'a Bound<Version>) -> Option<Self> {
1267        let Bound::Included(lower) = lower else {
1268            return None;
1269        };
1270        let Bound::Excluded(upper) = upper else {
1271            return None;
1272        };
1273        if lower.is_pre() || lower.is_post() || lower.is_local() {
1274            return None;
1275        }
1276        if upper.is_pre() || upper.is_post() || upper.is_local() {
1277            return None;
1278        }
1279        if lower.dev() != Some(0) {
1280            return None;
1281        }
1282        if upper.dev() != Some(0) {
1283            return None;
1284        }
1285        if lower.release().len() != upper.release().len() {
1286            return None;
1287        }
1288
1289        // All segments should be the same, except the last one, which should be incremented.
1290        let num_segments = lower.release().len();
1291        for (i, (lower, upper)) in lower
1292            .release()
1293            .iter()
1294            .zip(upper.release().iter())
1295            .enumerate()
1296        {
1297            if i == num_segments - 1 {
1298                if lower + 1 != *upper {
1299                    return None;
1300                }
1301            } else {
1302                if lower != upper {
1303                    return None;
1304                }
1305            }
1306        }
1307
1308        Some(PrefixMatch { version: lower })
1309    }
1310}
1311
1312impl std::fmt::Display for PrefixMatch<'_> {
1313    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1314        write!(f, "=={}.*", self.version.only_release())
1315    }
1316}
1317
1318#[derive(Debug)]
1319pub struct NoSolutionHeader {
1320    /// The [`ResolverEnvironment`] that caused the failure.
1321    env: ResolverEnvironment,
1322    /// The additional context for the resolution failure.
1323    context: Option<&'static str>,
1324}
1325
1326impl NoSolutionHeader {
1327    /// Create a new [`NoSolutionHeader`] with the given [`ResolverEnvironment`].
1328    pub fn new(env: ResolverEnvironment) -> Self {
1329        Self { env, context: None }
1330    }
1331
1332    /// Set the context for the resolution failure.
1333    #[must_use]
1334    pub fn with_context(mut self, context: &'static str) -> Self {
1335        self.context = Some(context);
1336        self
1337    }
1338}
1339
1340impl std::fmt::Display for NoSolutionHeader {
1341    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1342        match (self.context, self.env.end_user_fork_display()) {
1343            (None, None) => write!(f, "No solution found when resolving dependencies:"),
1344            (Some(context), None) => write!(
1345                f,
1346                "No solution found when resolving {context} dependencies:"
1347            ),
1348            (None, Some(split)) => write!(
1349                f,
1350                "No solution found when resolving dependencies for {split}:"
1351            ),
1352            (Some(context), Some(split)) => write!(
1353                f,
1354                "No solution found when resolving {context} dependencies for {split}:"
1355            ),
1356        }
1357    }
1358}
1359
1360/// Given a [`DerivationTree`], simplify version ranges using the included versions for each
1361/// package.
1362fn simplify_derivation_tree_ranges(
1363    tree: &mut DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
1364    included_versions: &FxHashMap<PackageName, BTreeSet<Version>>,
1365    candidate_selector: &CandidateSelector,
1366    resolver_environment: &ResolverEnvironment,
1367) {
1368    match tree {
1369        DerivationTree::External(external) => match external {
1370            External::FromDependencyOf(package1, versions1, package2, versions2) => {
1371                if let Some(simplified) = simplify_range(
1372                    versions1,
1373                    package1,
1374                    included_versions,
1375                    candidate_selector,
1376                    resolver_environment,
1377                ) {
1378                    *versions1 = simplified;
1379                }
1380                if let Some(simplified) = simplify_range(
1381                    versions2,
1382                    package2,
1383                    included_versions,
1384                    candidate_selector,
1385                    resolver_environment,
1386                ) {
1387                    *versions2 = simplified;
1388                }
1389            }
1390            External::NoVersions(package, versions) => {
1391                if let Some(simplified) = simplify_range(
1392                    versions,
1393                    package,
1394                    included_versions,
1395                    candidate_selector,
1396                    resolver_environment,
1397                ) {
1398                    *versions = simplified;
1399                }
1400            }
1401            External::Custom(package, versions, _) => {
1402                if let Some(simplified) = simplify_range(
1403                    versions,
1404                    package,
1405                    included_versions,
1406                    candidate_selector,
1407                    resolver_environment,
1408                ) {
1409                    *versions = simplified;
1410                }
1411            }
1412            External::NotRoot(..) => (),
1413        },
1414        DerivationTree::Derived(derived) => {
1415            // Recursively simplify both sides of the tree
1416            simplify_derivation_tree_ranges(
1417                Arc::make_mut(&mut derived.cause1),
1418                included_versions,
1419                candidate_selector,
1420                resolver_environment,
1421            );
1422            simplify_derivation_tree_ranges(
1423                Arc::make_mut(&mut derived.cause2),
1424                included_versions,
1425                candidate_selector,
1426                resolver_environment,
1427            );
1428
1429            // Simplify the terms
1430            derived.terms = std::mem::take(&mut derived.terms)
1431                .into_iter()
1432                .map(|(package, term)| {
1433                    let term = match term {
1434                        Term::Positive(versions) => Term::Positive(
1435                            simplify_range(
1436                                &versions,
1437                                &package,
1438                                included_versions,
1439                                candidate_selector,
1440                                resolver_environment,
1441                            )
1442                            .unwrap_or(versions),
1443                        ),
1444                        Term::Negative(versions) => Term::Negative(
1445                            simplify_range(
1446                                &versions,
1447                                &package,
1448                                included_versions,
1449                                candidate_selector,
1450                                resolver_environment,
1451                            )
1452                            .unwrap_or(versions),
1453                        ),
1454                    };
1455                    (package, term)
1456                })
1457                .collect();
1458        }
1459    }
1460}
1461
1462/// Helper function to simplify a version range using included versions for a package.
1463///
1464/// If the range cannot be simplified, `None` is returned.
1465fn simplify_range(
1466    range: &Range<Version>,
1467    package: &PubGrubPackage,
1468    included_versions: &FxHashMap<PackageName, BTreeSet<Version>>,
1469    candidate_selector: &CandidateSelector,
1470    resolver_environment: &ResolverEnvironment,
1471) -> Option<Range<Version>> {
1472    // If there's not a package name or included versions, we can't simplify anything
1473    let name = package.name()?;
1474    let versions = included_versions.get(name)?;
1475
1476    // If this is a full range, there's nothing to simplify
1477    if range == &Range::full() {
1478        return None;
1479    }
1480
1481    // If there's only one version available and it's in the range, return just that version
1482    if let Some(version) = versions.iter().next() {
1483        if versions.len() == 1 && range.contains(version) {
1484            return Some(Range::singleton(version.clone()));
1485        }
1486    }
1487
1488    // Check if pre-releases are allowed
1489    let prereleases_not_allowed = candidate_selector
1490        .prerelease_strategy()
1491        .allows(name, resolver_environment)
1492        != AllowPrerelease::Yes;
1493
1494    let any_prerelease = range.iter().any(|(start, end)| {
1495        let is_pre1 = match start {
1496            Bound::Included(version) => version.any_prerelease(),
1497            Bound::Excluded(version) => version.any_prerelease(),
1498            Bound::Unbounded => false,
1499        };
1500        let is_pre2 = match end {
1501            Bound::Included(version) => version.any_prerelease(),
1502            Bound::Excluded(version) => version.any_prerelease(),
1503            Bound::Unbounded => false,
1504        };
1505        is_pre1 || is_pre2
1506    });
1507
1508    // Simplify the range, as implemented in PubGrub
1509    Some(range.simplify(versions.iter().filter(|version| {
1510        // If there are pre-releases in the range segments, we need to include pre-releases
1511        if any_prerelease {
1512            return true;
1513        }
1514
1515        // If pre-releases are not allowed, filter out pre-releases
1516        if prereleases_not_allowed && version.any_prerelease() {
1517            return false;
1518        }
1519
1520        // Otherwise, include the version
1521        true
1522    })))
1523}