Skip to main content

uv_resolver/
error.rs

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