Skip to main content

uv_resolver/
error.rs

1use std::collections::{BTreeMap, BTreeSet, Bound};
2use std::fmt::{Debug, Formatter};
3use std::ops::Deref;
4use std::sync::{Arc, OnceLock};
5
6use indexmap::IndexSet;
7use itertools::Itertools;
8use owo_colors::OwoColorize;
9use pubgrub::{DerivationTree, Derived, External, Map, Ranges, Term};
10use rustc_hash::{FxHashMap, FxHashSet};
11use tracing::trace;
12
13use uv_distribution_types::{
14    DerivationChain, DistErrorKind, IndexCapabilities, IndexLocations, IndexUrl, RequestedDist,
15};
16use uv_normalize::PackageName;
17use uv_pep440::{LowerBound, Version};
18use uv_pep508::MarkerEnvironment;
19use uv_platform_tags::Tags;
20use uv_pypi_types::ParsedUrl;
21use uv_redacted::DisplaySafeUrl;
22use uv_static::EnvVars;
23
24use crate::candidate_selector::CandidateSelector;
25use crate::dependency_provider::UvDependencyProvider;
26use crate::fork_indexes::ForkIndexes;
27use crate::fork_urls::ForkUrls;
28use crate::prerelease::PrereleaseSelection;
29use crate::pubgrub::{
30    PubGrubHint, PubGrubPackage, PubGrubPackageInner, PubGrubReportFormatter, Range,
31    report_derivation_tree,
32};
33use crate::python_requirement::PythonRequirement;
34use crate::resolution::ConflictingDistributionError;
35use crate::resolver::{
36    MetadataUnavailable, ResolverEnvironment, UnavailablePackage, UnavailableReason,
37};
38use crate::{InMemoryIndex, Options};
39
40#[derive(Debug, thiserror::Error)]
41pub enum ResolveError {
42    #[error("Failed to resolve dependencies for package `{1}=={2}`")]
43    Dependencies(#[source] Box<Self>, PackageName, Version, DerivationChain),
44
45    #[error(transparent)]
46    Client(#[from] uv_client::Error),
47
48    #[error(transparent)]
49    Distribution(#[from] uv_distribution::Error),
50
51    #[error("The channel closed unexpectedly")]
52    ChannelClosed,
53
54    #[error("Attempted to wait on an unregistered task: `{_0}`")]
55    UnregisteredTask(String),
56
57    #[error(
58        "Requirements contain conflicting URLs for package `{package_name}`{}:\n- {}",
59        if env.marker_environment().is_some() {
60            String::new()
61        } else {
62            format!(" in {env}")
63        },
64        urls.iter()
65            .map(|url| format!("{}{}", DisplaySafeUrl::from(url.clone()), if url.is_editable() { " (editable)" } else { "" }))
66            .collect::<Vec<_>>()
67            .join("\n- ")
68    )]
69    ConflictingUrls {
70        package_name: PackageName,
71        urls: Vec<ParsedUrl>,
72        env: ResolverEnvironment,
73    },
74
75    #[error(
76        "Requirements contain conflicting indexes for package `{package_name}`{}:\n- {}",
77        if env.marker_environment().is_some() {
78            String::new()
79        } else {
80            format!(" in {env}")
81        },
82        indexes.iter()
83            .map(std::string::ToString::to_string)
84            .collect::<Vec<_>>()
85            .join("\n- ")
86    )]
87    ConflictingIndexesForEnvironment {
88        package_name: PackageName,
89        indexes: Vec<IndexUrl>,
90        env: ResolverEnvironment,
91    },
92
93    #[error("Requirements contain conflicting indexes for package `{0}`: `{1}` vs. `{2}`")]
94    ConflictingIndexes(PackageName, String, String),
95
96    #[error(
97        "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.",
98        name = name.cyan(),
99        requirement = format!("{name} @ {url}").cyan(),
100    )]
101    DisallowedUrl { name: PackageName, url: String },
102
103    #[error(transparent)]
104    DistributionType(#[from] uv_distribution_types::Error),
105
106    #[error("{0} `{1}`")]
107    Dist(
108        DistErrorKind,
109        Box<RequestedDist>,
110        DerivationChain,
111        #[source] Arc<uv_distribution::Error>,
112    ),
113
114    #[error(transparent)]
115    NoSolution(#[from] Box<NoSolutionError>),
116
117    #[error("Attempted to construct an invalid version specifier")]
118    InvalidVersion(#[from] uv_pep440::VersionSpecifierBuildError),
119
120    #[error("Hash-checking is required, but no trusted hash was provided for: `{0}`")]
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(transparent)]
130    ConflictMarker(#[from] uv_resolver_types::ConflictMarkerError),
131    #[error(
132        "The index returned metadata for the wrong package: expected {request} for {expected}, got {request} for {actual}"
133    )]
134    MismatchedPackageName {
135        request: &'static str,
136        expected: PackageName,
137        actual: PackageName,
138    },
139}
140
141impl ResolveError {
142    /// Return whether this is an expected user-facing failure.
143    pub fn is_user_failure(&self) -> bool {
144        match self {
145            Self::Dependencies(error, ..) => error.is_user_failure(),
146            Self::Distribution(error) => error.is_user_failure(),
147            Self::ConflictingUrls { .. }
148            | Self::ConflictingIndexesForEnvironment { .. }
149            | Self::ConflictingIndexes(..)
150            | Self::DisallowedUrl { .. }
151            | Self::DistributionType(_)
152            | Self::NoSolution(_)
153            | Self::UnhashedPackage(_)
154            | Self::PackageUnavailable(_)
155            | Self::ConflictMarker(_)
156            | Self::MismatchedPackageName { .. } => true,
157            Self::Dist(_, _, _, error) => error.is_user_failure(),
158            Self::Client(error) => error.is_user_failure(),
159            Self::ChannelClosed
160            | Self::UnregisteredTask(_)
161            | Self::InvalidVersion(_)
162            | Self::ConflictingDistribution(_) => false,
163        }
164    }
165}
166
167impl uv_errors::Hinted for ResolveError {
168    fn hints(&self) -> uv_errors::Hints<'_> {
169        match self {
170            Self::NoSolution(no_solution) => uv_errors::Hinted::hints(no_solution.as_ref()),
171            Self::Client(error) => uv_errors::Hinted::hints(error),
172            Self::Distribution(error) => uv_errors::Hinted::hints(error),
173            Self::Dependencies(error, ..) => uv_errors::Hinted::hints(error.as_ref()),
174            _ => uv_errors::Hints::none(),
175        }
176    }
177}
178
179impl<T> From<tokio::sync::mpsc::error::SendError<T>> for ResolveError {
180    /// Drop the value we want to send to not leak the private type we're sending.
181    /// The tokio error only says "channel closed", so we don't lose information.
182    fn from(_value: tokio::sync::mpsc::error::SendError<T>) -> Self {
183        Self::ChannelClosed
184    }
185}
186
187pub type ErrorTree = DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>;
188type ErrorExternal = External<PubGrubPackage, Range<Version>, UnavailableReason>;
189type ErrorDerived = Derived<PubGrubPackage, Range<Version>, UnavailableReason>;
190type ErrorTerms = Map<PubGrubPackage, Term<Range<Version>>>;
191
192/// Visit each distinct package in a derivation tree without recursive calls.
193///
194/// The iteration order is unspecified, matching the set semantics of
195/// [`DerivationTree::packages`].
196pub(crate) fn derivation_tree_packages(
197    derivation_tree: &ErrorTree,
198) -> impl Iterator<Item = &PubGrubPackage> {
199    let mut packages = FxHashSet::default();
200    let mut trees = vec![derivation_tree];
201
202    while let Some(tree) = trees.pop() {
203        match tree {
204            DerivationTree::External(external) => match external {
205                External::FromDependencyOf(package, _, dependency, _) => {
206                    packages.insert(package);
207                    packages.insert(dependency);
208                }
209                External::NoVersions(package, _)
210                | External::NotRoot(package, _)
211                | External::Custom(package, _, _) => {
212                    packages.insert(package);
213                }
214            },
215            DerivationTree::Derived(derived) => {
216                packages.extend(derived.terms.keys());
217                trees.push(&derived.cause1);
218                trees.push(&derived.cause2);
219            }
220        }
221    }
222
223    packages.into_iter()
224}
225
226/// Drop an exclusively owned derivation tree without recursing through its children.
227///
228/// Shared [`Arc`] children are left for their remaining owners; once the last owner is processed,
229/// [`Arc::try_unwrap`] exposes the child for iterative destruction.
230pub(crate) fn drop_derivation_tree(derivation_tree: ErrorTree) {
231    let mut trees = vec![derivation_tree];
232
233    while let Some(tree) = trees.pop() {
234        if let DerivationTree::Derived(derived) = tree {
235            if let Ok(cause1) = Arc::try_unwrap(derived.cause1) {
236                trees.push(cause1);
237            }
238            if let Ok(cause2) = Arc::try_unwrap(derived.cause2) {
239                trees.push(cause2);
240            }
241        }
242    }
243}
244
245/// Own a derivation tree whose destruction must not recurse through the process stack.
246///
247/// The `Option` allows [`Drop`] to take ownership of the tree and applies the same iterative
248/// teardown during normal returns and unwinding.
249#[derive(Clone)]
250struct StackSafeErrorTree(Option<ErrorTree>);
251
252impl StackSafeErrorTree {
253    fn new(derivation_tree: ErrorTree) -> Self {
254        Self(Some(derivation_tree))
255    }
256
257    fn into_inner(mut self) -> ErrorTree {
258        self.0.take().expect("derivation tree is only taken once")
259    }
260}
261
262impl Deref for StackSafeErrorTree {
263    type Target = ErrorTree;
264
265    fn deref(&self) -> &Self::Target {
266        self.0
267            .as_ref()
268            .expect("derivation tree is only taken during drop")
269    }
270}
271
272impl Drop for StackSafeErrorTree {
273    fn drop(&mut self) {
274        if let Some(derivation_tree) = self.0.take() {
275            drop_derivation_tree(derivation_tree);
276        }
277    }
278}
279
280impl Debug for StackSafeErrorTree {
281    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
282        debug_derivation_tree(self, f)
283    }
284}
285
286/// Preserve PubGrub's [`Debug`] representation without recursive formatting.
287fn debug_derivation_tree(
288    derivation_tree: &ErrorTree,
289    formatter: &mut Formatter<'_>,
290) -> std::fmt::Result {
291    enum Frame<'a> {
292        Tree(&'a ErrorTree),
293        Text(&'static str),
294    }
295
296    let mut frames = vec![Frame::Tree(derivation_tree)];
297
298    while let Some(frame) = frames.pop() {
299        match frame {
300            Frame::Tree(DerivationTree::External(external)) => {
301                write!(formatter, "External({external:?})")?;
302            }
303            Frame::Tree(DerivationTree::Derived(derived)) => {
304                write!(
305                    formatter,
306                    "Derived(Derived {{ terms: {:?}, shared_id: {:?}, cause1: ",
307                    derived.terms, derived.shared_id
308                )?;
309                frames.push(Frame::Text(" })"));
310                frames.push(Frame::Tree(&derived.cause2));
311                frames.push(Frame::Text(", cause2: "));
312                frames.push(Frame::Tree(&derived.cause1));
313            }
314            Frame::Text(text) => formatter.write_str(text)?,
315        }
316    }
317
318    Ok(())
319}
320
321struct DerivedMetadata {
322    terms: ErrorTerms,
323    shared_id: Option<usize>,
324}
325
326enum TreeTask {
327    Visit(StackSafeErrorTree),
328    Rebuild(DerivedMetadata),
329}
330
331fn schedule_derived(tasks: &mut Vec<TreeTask>, derived: ErrorDerived) {
332    let Derived {
333        terms,
334        shared_id,
335        cause1,
336        cause2,
337    } = derived;
338    tasks.push(TreeTask::Rebuild(DerivedMetadata { terms, shared_id }));
339    tasks.push(TreeTask::Visit(StackSafeErrorTree::new(
340        Arc::unwrap_or_clone(cause2),
341    )));
342    tasks.push(TreeTask::Visit(StackSafeErrorTree::new(
343        Arc::unwrap_or_clone(cause1),
344    )));
345}
346
347fn transform_derivation_tree(
348    derivation_tree: ErrorTree,
349    mut transform_external: impl FnMut(ErrorExternal) -> Option<ErrorTree>,
350    mut transform_derived: impl FnMut(
351        DerivedMetadata,
352        Option<ErrorTree>,
353        Option<ErrorTree>,
354    ) -> Option<ErrorTree>,
355) -> Option<ErrorTree> {
356    let mut tasks = vec![TreeTask::Visit(StackSafeErrorTree::new(derivation_tree))];
357    let mut results: Vec<Option<StackSafeErrorTree>> = Vec::new();
358
359    while let Some(task) = tasks.pop() {
360        match task {
361            TreeTask::Visit(tree) => match tree.into_inner() {
362                DerivationTree::External(external) => {
363                    results.push(transform_external(external).map(StackSafeErrorTree::new));
364                }
365                DerivationTree::Derived(derived) => schedule_derived(&mut tasks, derived),
366            },
367            TreeTask::Rebuild(metadata) => {
368                let cause2 = results
369                    .pop()
370                    .expect("every derived tree has a second transformed cause")
371                    .map(StackSafeErrorTree::into_inner);
372                let cause1 = results
373                    .pop()
374                    .expect("every derived tree has a first transformed cause")
375                    .map(StackSafeErrorTree::into_inner);
376                results
377                    .push(transform_derived(metadata, cause1, cause2).map(StackSafeErrorTree::new));
378            }
379        }
380    }
381
382    results
383        .pop()
384        .expect("the root derivation tree produces one transformed result")
385        .map(StackSafeErrorTree::into_inner)
386}
387
388fn map_derivation_tree(
389    derivation_tree: ErrorTree,
390    mut transform_external: impl FnMut(ErrorExternal) -> ErrorTree,
391    mut transform_derived: impl FnMut(DerivedMetadata, ErrorTree, ErrorTree) -> ErrorTree,
392) -> ErrorTree {
393    transform_derivation_tree(
394        derivation_tree,
395        |external| Some(transform_external(external)),
396        |metadata, cause1, cause2| {
397            Some(transform_derived(
398                metadata,
399                cause1.expect("map transformations retain the first cause"),
400                cause2.expect("map transformations retain the second cause"),
401            ))
402        },
403    )
404    .expect("map transformations retain the root")
405}
406
407fn derived_tree(metadata: DerivedMetadata, cause1: ErrorTree, cause2: ErrorTree) -> ErrorTree {
408    DerivationTree::Derived(Derived {
409        terms: metadata.terms,
410        shared_id: metadata.shared_id,
411        cause1: Arc::new(cause1),
412        cause2: Arc::new(cause2),
413    })
414}
415
416/// Narrow a version set onto a non-empty list of known versions.
417fn narrow_to_known(set: &Range<Version>, versions: &[Version]) -> Range<Version> {
418    if versions.iter().all(|version| set.contains(version)) {
419        Range::full()
420    } else {
421        set.narrow_versions(versions)
422    }
423}
424
425/// A wrapper around [`pubgrub::error::NoSolutionError`] that displays a resolution failure report.
426pub struct NoSolutionError {
427    error: StackSafeErrorTree,
428    index: InMemoryIndex,
429    /// The versions that were available for each package after `exclude-newer` filtering.
430    ///
431    /// For versions available before filtering, see [`NoSolutionError::available_versions`].
432    included_versions: FxHashMap<PackageName, BTreeSet<Version>>,
433    /// The versions available for each package.
434    ///
435    /// These version sets are not filtered by `exclude-newer`. See
436    /// [`NoSolutionError::included_versions`] instead if filtered versions are needed.
437    ///
438    /// These versions are filtered by [`EnvVars::UV_INTERNAL__TEST_AVAILABLE_VERSION_CUTOFF`] for
439    /// deterministic output in tests.
440    available_versions: FxHashMap<PackageName, BTreeSet<Version>>,
441    available_indexes: FxHashMap<PackageName, BTreeSet<IndexUrl>>,
442    selector: CandidateSelector,
443    python_requirement: PythonRequirement,
444    index_locations: IndexLocations,
445    index_capabilities: IndexCapabilities,
446    unavailable_packages: FxHashMap<PackageName, UnavailablePackage>,
447    incomplete_packages: FxHashMap<PackageName, BTreeMap<Version, MetadataUnavailable>>,
448    fork_urls: ForkUrls,
449    fork_indexes: ForkIndexes,
450    env: ResolverEnvironment,
451    current_environment: MarkerEnvironment,
452    tags: Option<Tags>,
453    workspace_members: BTreeSet<PackageName>,
454    options: Options,
455    /// Cached report and hints, computed once on first access.
456    cached: OnceLock<(String, IndexSet<PubGrubHint>)>,
457}
458
459impl NoSolutionError {
460    /// Create a new [`NoSolutionError`] from a [`pubgrub::NoSolutionError`].
461    pub(crate) fn new(
462        error: pubgrub::NoSolutionError<UvDependencyProvider>,
463        index: InMemoryIndex,
464        included_versions: FxHashMap<PackageName, BTreeSet<Version>>,
465        available_versions: FxHashMap<PackageName, BTreeSet<Version>>,
466        available_indexes: FxHashMap<PackageName, BTreeSet<IndexUrl>>,
467        selector: CandidateSelector,
468        python_requirement: PythonRequirement,
469        index_locations: IndexLocations,
470        index_capabilities: IndexCapabilities,
471        unavailable_packages: FxHashMap<PackageName, UnavailablePackage>,
472        incomplete_packages: FxHashMap<PackageName, BTreeMap<Version, MetadataUnavailable>>,
473        fork_urls: ForkUrls,
474        fork_indexes: ForkIndexes,
475        env: ResolverEnvironment,
476        current_environment: MarkerEnvironment,
477        tags: Option<Tags>,
478        workspace_members: BTreeSet<PackageName>,
479        options: Options,
480    ) -> Self {
481        Self {
482            error: StackSafeErrorTree::new(error),
483            index,
484            included_versions,
485            available_versions,
486            available_indexes,
487            selector,
488            python_requirement,
489            index_locations,
490            index_capabilities,
491            unavailable_packages,
492            incomplete_packages,
493            fork_urls,
494            fork_indexes,
495            env,
496            current_environment,
497            tags,
498            workspace_members,
499            options,
500            cached: OnceLock::new(),
501        }
502    }
503
504    /// Get the cached report and hints, computing them on first access.
505    fn cached(&self) -> &(String, IndexSet<PubGrubHint>) {
506        self.cached.get_or_init(|| self.compute_report_and_hints())
507    }
508
509    /// Given a [`DerivationTree`], collapse any [`External::FromDependencyOf`] incompatibilities
510    /// wrap an [`PubGrubPackageInner::Extra`] package.
511    pub(crate) fn collapse_proxies(derivation_tree: ErrorTree) -> ErrorTree {
512        fn is_proxy(tree: &ErrorTree) -> bool {
513            matches!(
514                tree,
515                DerivationTree::External(External::FromDependencyOf(package, ..))
516                    if package.is_proxy()
517            )
518        }
519
520        transform_derivation_tree(
521            derivation_tree,
522            |external| Some(DerivationTree::External(external)),
523            |metadata, cause1, cause2| match (cause1, cause2) {
524                (Some(cause1), Some(cause2)) if is_proxy(&cause1) && is_proxy(&cause2) => None,
525                (Some(cause), other) if is_proxy(&cause) => other,
526                (other, Some(cause)) if is_proxy(&cause) => other,
527                (Some(cause1), Some(cause2)) => Some(derived_tree(metadata, cause1, cause2)),
528                (Some(cause), None) | (None, Some(cause)) => Some(cause),
529                (None, None) => None,
530            },
531        )
532        .expect("derivation tree should contain at least one external term")
533    }
534
535    /// Simplifies the version ranges on any incompatibilities to remove the `[max]` sentinel.
536    ///
537    /// The `[max]` sentinel is used to represent the maximum local version of a package, to
538    /// implement PEP 440 semantics for local version equality. For example, `1.0.0+foo` needs to
539    /// satisfy `==1.0.0`.
540    pub(crate) fn collapse_local_version_segments(derivation_tree: ErrorTree) -> ErrorTree {
541        transform_derivation_tree(
542            derivation_tree,
543            |external| match external {
544                external @ External::NotRoot(_, _) => Some(DerivationTree::External(external)),
545                External::NoVersions(package, versions) => {
546                    if versions.is_local_version_complement() {
547                        return None;
548                    }
549
550                    let versions = versions.without_local_version_sentinels();
551                    Some(DerivationTree::External(External::NoVersions(
552                        package, versions,
553                    )))
554                }
555                External::FromDependencyOf(package1, versions1, package2, versions2) => {
556                    let versions1 = versions1.without_local_version_sentinels();
557                    let versions2 = versions2.without_local_version_sentinels();
558                    Some(DerivationTree::External(External::FromDependencyOf(
559                        package1, versions1, package2, versions2,
560                    )))
561                }
562                External::Custom(package, versions, reason) => {
563                    let versions = versions.without_local_version_sentinels();
564                    Some(DerivationTree::External(External::Custom(
565                        package, versions, reason,
566                    )))
567                }
568            },
569            |mut metadata, cause1, cause2| {
570                metadata.terms = metadata
571                    .terms
572                    .into_iter()
573                    .map(|(package, term)| {
574                        let term = match term {
575                            Term::Positive(versions) => {
576                                Term::Positive(versions.without_local_version_sentinels())
577                            }
578                            Term::Negative(versions) => {
579                                Term::Negative(versions.without_local_version_sentinels())
580                            }
581                        };
582                        (package, term)
583                    })
584                    .collect();
585
586                match (cause1, cause2) {
587                    (Some(cause1), Some(cause2)) => Some(derived_tree(metadata, cause1, cause2)),
588                    (Some(cause), None) | (None, Some(cause)) => Some(cause),
589                    (None, None) => None,
590                }
591            },
592        )
593        .expect("derivation tree should contain at least one term")
594    }
595
596    /// Shrinks widened version sets in the derivation tree back onto the known versions.
597    ///
598    /// The resolver widens version sets to the largest interval containing the same known
599    /// versions ([`Ranges::widen_versions`]), both on the depending side of a dependency
600    /// incompatibility and for a version that cannot be used, keeping version sets small during
601    /// resolution. The widened bounds are misleading in error messages, e.g., `a>1.5.2,<2.0.0`
602    /// when `a 1.5.3` is the only version in that interval. Shrink the depending side, the
603    /// unavailable set, and positive terms back: a set containing all known versions of a package
604    /// becomes the full range ("all versions of a"); otherwise, bounded ends are narrowed to
605    /// inclusive bounds on the known versions they contain while unbounded ends are preserved,
606    /// except on an unavailable set, which never claims a version outside the listing. Where a
607    /// package has one version, a set that rules it out is reported as that version rather than as
608    /// all of them. Dependency requests (the depended-on side and negative terms) are shown as
609    /// requested.
610    pub(crate) fn narrow_widened_sets(
611        derivation_tree: ErrorTree,
612        known_versions: &FxHashMap<PackageName, Arc<[Version]>>,
613    ) -> ErrorTree {
614        // The known versions of a package, with the lowest and the highest. An empty list carries
615        // no information, so it is treated as absent.
616        let listed = |package: &PubGrubPackage| -> Option<(&[Version], &Version, &Version)> {
617            let versions = package
618                .name_no_root()
619                .and_then(|name| known_versions.get(name))?;
620            Some((versions, versions.first()?, versions.last()?))
621        };
622
623        let narrow = |package: &PubGrubPackage, set: Range<Version>| -> Range<Version> {
624            let Some((versions, _, _)) = listed(package) else {
625                return set;
626            };
627            narrow_to_known(&set, versions)
628        };
629
630        // A single version's unavailability says nothing about versions outside the listing, so
631        // an unbounded end of its widened set must not be reported as a claim about them.
632        let narrow_unavailable =
633            |package: &PubGrubPackage, set: Range<Version>| -> Range<Version> {
634                let Some((versions, lowest, highest)) = listed(package) else {
635                    return set;
636                };
637                // A rejection of the one version a package has reports that version.
638                if let [version] = versions
639                    && set.contains(version)
640                {
641                    return Range::singleton(version.clone());
642                }
643                let narrowed = narrow_to_known(&set, versions);
644                // A claim about every known version is reported as such, not as its bounds.
645                if narrowed == Range::full() {
646                    return narrowed;
647                }
648                let envelope = Range::from_range_bounds(lowest.clone()..=highest.clone());
649                let clamped = narrowed.intersection(&envelope);
650                // A rejected version outside the listing has no envelope to report it in.
651                if clamped == Range::empty() {
652                    narrowed
653                } else {
654                    clamped
655                }
656            };
657
658        // A conclusion about a package with one version reports that version where its causes do,
659        // so that the step does not reach past them.
660        let narrow_conclusion = |package: &PubGrubPackage,
661                                 set: Range<Version>,
662                                 cause1: &ErrorTree,
663                                 cause2: &ErrorTree|
664         -> Range<Version> {
665            let Some(([version], _, _)) = listed(package) else {
666                return narrow(package, set);
667            };
668            let single = Range::singleton(version.clone());
669            if set.contains(version)
670                && reported_versions(package, cause1).union(&reported_versions(package, cause2))
671                    == single
672            {
673                return single;
674            }
675            narrow(package, set)
676        };
677
678        map_derivation_tree(
679            derivation_tree,
680            |external| match external {
681                External::FromDependencyOf(package1, versions1, package2, versions2) => {
682                    // A rejected version is recorded as a dependency on Python, so the widened
683                    // side is an exclusion and stops at the listing like any other.
684                    let versions1 = if matches!(&*package2, PubGrubPackageInner::Python(_)) {
685                        narrow_unavailable(&package1, versions1)
686                    } else {
687                        narrow(&package1, versions1)
688                    };
689                    DerivationTree::External(External::FromDependencyOf(
690                        package1, versions1, package2, versions2,
691                    ))
692                }
693                External::Custom(package, versions, reason) => {
694                    let versions = match &reason {
695                        UnavailableReason::Version(_) => narrow_unavailable(&package, versions),
696                        UnavailableReason::Package(_) => narrow(&package, versions),
697                    };
698                    DerivationTree::External(External::Custom(package, versions, reason))
699                }
700                external => DerivationTree::External(external),
701            },
702            |mut metadata, cause1, cause2| {
703                metadata.terms = metadata
704                    .terms
705                    .into_iter()
706                    .map(|(package, term)| {
707                        let term = match term {
708                            Term::Positive(versions) => Term::Positive(narrow_conclusion(
709                                &package, versions, &cause1, &cause2,
710                            )),
711                            term @ Term::Negative(_) => term,
712                        };
713                        (package, term)
714                    })
715                    .collect();
716
717                derived_tree(metadata, cause1, cause2)
718            },
719        )
720    }
721
722    /// Given a [`DerivationTree`], identify the largest required Python version that is missing.
723    pub fn find_requires_python(&self) -> LowerBound {
724        let mut minimum = LowerBound::default();
725        let mut trees = vec![&*self.error];
726
727        while let Some(derivation_tree) = trees.pop() {
728            match derivation_tree {
729                DerivationTree::Derived(derived) => {
730                    trees.push(&derived.cause2);
731                    trees.push(&derived.cause1);
732                }
733                DerivationTree::External(External::FromDependencyOf(.., package, version)) => {
734                    if let PubGrubPackageInner::Python(_) = &**package {
735                        if let Some((lower, ..)) = version.bounding_range() {
736                            let lower = LowerBound::new(lower.cloned());
737                            if lower > minimum {
738                                minimum = lower;
739                            }
740                        }
741                    }
742                }
743                DerivationTree::External(_) => {}
744            }
745        }
746
747        minimum
748    }
749
750    /// Return the [`ResolverEnvironment`] that caused the failure.
751    pub fn environment(&self) -> &ResolverEnvironment {
752        &self.env
753    }
754
755    /// Get the packages that are involved in this error.
756    pub fn packages(&self) -> impl Iterator<Item = &PackageName> {
757        derivation_tree_packages(&self.error)
758            .filter_map(|p| p.name())
759            .unique()
760    }
761
762    /// Return the formatted report string.
763    fn report(&self) -> &str {
764        &self.cached().0
765    }
766
767    /// Return the computed PubGrub hints.
768    fn pubgrub_hints(&self) -> &IndexSet<PubGrubHint> {
769        &self.cached().1
770    }
771
772    /// Compute the reduced derivation tree, formatted report string, and hints.
773    fn compute_report_and_hints(&self) -> (String, IndexSet<PubGrubHint>) {
774        let formatter = PubGrubReportFormatter {
775            included_versions: &self.included_versions,
776            available_versions: &self.available_versions,
777            python_requirement: &self.python_requirement,
778            workspace_members: &self.workspace_members,
779            tags: self.tags.as_ref(),
780        };
781
782        // Transform the error tree for reporting
783        let mut tree = simplify_derivation_tree_markers(
784            self.error.clone().into_inner(),
785            &self.python_requirement,
786        );
787        let should_display_tree = std::env::var_os(EnvVars::UV_INTERNAL__SHOW_DERIVATION_TREE)
788            .is_some()
789            || tracing::enabled!(tracing::Level::TRACE);
790
791        if should_display_tree {
792            display_tree(&tree, "Resolver derivation tree before reduction");
793        }
794
795        tree = collapse_no_versions_of_workspace_members(tree, &self.workspace_members);
796
797        if self.workspace_members.len() == 1 {
798            let project = self.workspace_members.iter().next().unwrap();
799            tree = drop_root_dependency_on_project(tree, project);
800        }
801
802        tree = collapse_unavailable_versions(tree);
803        tree = collapse_redundant_depends_on_no_versions(tree);
804
805        tree = simplify_derivation_tree_ranges(
806            tree,
807            &self.included_versions,
808            &self.selector,
809            &self.env,
810        );
811
812        // These need to be applied _after_ simplification of the ranges
813        tree = restate_available_versions(tree, &self.included_versions);
814        tree = collapse_redundant_no_versions(tree);
815
816        loop {
817            let (collapsed, changed) = collapse_redundant_no_versions_tree(tree);
818            tree = collapsed;
819            if !changed {
820                break;
821            }
822        }
823
824        if should_display_tree {
825            display_tree(&tree, "Resolver derivation tree after reduction");
826        }
827
828        let report = report_derivation_tree(&tree, &formatter);
829
830        let inherited_exclude_newer_ranges = FxHashMap::default();
831        let mut hints = IndexSet::default();
832        formatter.generate_hints(
833            &tree,
834            &self.index,
835            &self.selector,
836            &self.index_locations,
837            &self.index_capabilities,
838            &self.available_indexes,
839            &self.unavailable_packages,
840            &self.incomplete_packages,
841            &self.fork_urls,
842            &self.fork_indexes,
843            &self.env,
844            &self.current_environment,
845            self.tags.as_ref(),
846            &self.workspace_members,
847            &self.options,
848            &inherited_exclude_newer_ranges,
849            &mut hints,
850        );
851
852        (report, hints)
853    }
854}
855
856impl std::fmt::Debug for NoSolutionError {
857    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
858        // Include every field except `index` (no Debug) and `cached` (derived).
859        let Self {
860            error,
861            index: _,
862            included_versions,
863            available_versions,
864            available_indexes,
865            selector,
866            python_requirement,
867            index_locations,
868            index_capabilities,
869            unavailable_packages,
870            incomplete_packages,
871            fork_urls,
872            fork_indexes,
873            env,
874            current_environment,
875            tags,
876            workspace_members,
877            options,
878            cached: _,
879        } = self;
880        f.debug_struct("NoSolutionError")
881            .field("error", error)
882            .field("included_versions", included_versions)
883            .field("available_versions", available_versions)
884            .field("available_indexes", available_indexes)
885            .field("selector", selector)
886            .field("python_requirement", python_requirement)
887            .field("index_locations", index_locations)
888            .field("index_capabilities", index_capabilities)
889            .field("unavailable_packages", unavailable_packages)
890            .field("incomplete_packages", incomplete_packages)
891            .field("fork_urls", fork_urls)
892            .field("fork_indexes", fork_indexes)
893            .field("env", env)
894            .field("current_environment", current_environment)
895            .field("tags", tags)
896            .field("workspace_members", workspace_members)
897            .field("options", options)
898            .finish()
899    }
900}
901
902impl std::error::Error for NoSolutionError {}
903
904impl uv_errors::Hinted for NoSolutionError {
905    fn hints(&self) -> uv_errors::Hints<'_> {
906        self.pubgrub_hints()
907            .iter()
908            .map(ToString::to_string)
909            .collect()
910    }
911}
912
913impl uv_errors::Hinted for Box<NoSolutionError> {
914    fn hints(&self) -> uv_errors::Hints<'_> {
915        self.as_ref().hints()
916    }
917}
918
919impl std::fmt::Display for NoSolutionError {
920    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
921        // Write only the derivation report. Hints are available separately
922        // via `hints()` and rendered by the caller.
923        write!(f, "{}", self.report())
924    }
925}
926
927#[expect(clippy::print_stderr)]
928fn display_tree(
929    error: &DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
930    name: &str,
931) {
932    let mut lines = Vec::new();
933    display_tree_inner(error, &mut lines);
934    lines.reverse();
935
936    if std::env::var_os(EnvVars::UV_INTERNAL__SHOW_DERIVATION_TREE).is_some() {
937        eprintln!("{name}\n{}", lines.join("\n"));
938    } else {
939        trace!("{name}\n{}", lines.join("\n"));
940    }
941}
942
943fn display_tree_inner(
944    error: &DerivationTree<PubGrubPackage, Range<Version>, UnavailableReason>,
945    lines: &mut Vec<String>,
946) {
947    enum Frame<'a> {
948        Tree(&'a ErrorTree, usize),
949        Terms(&'a ErrorTerms, usize),
950    }
951
952    let mut frames = vec![Frame::Tree(error, 0)];
953    while let Some(frame) = frames.pop() {
954        match frame {
955            Frame::Tree(DerivationTree::Derived(derived), depth) => {
956                frames.push(Frame::Terms(&derived.terms, depth));
957                frames.push(Frame::Tree(&derived.cause2, depth + 1));
958                frames.push(Frame::Tree(&derived.cause1, depth + 1));
959            }
960            Frame::Tree(DerivationTree::External(external), depth) => {
961                let prefix = "  ".repeat(depth);
962                match external {
963                    External::FromDependencyOf(
964                        package,
965                        version,
966                        dependency,
967                        dependency_version,
968                    ) => {
969                        lines.push(format!(
970                            "{prefix}{package}{version} depends on {dependency}{dependency_version}"
971                        ));
972                    }
973                    External::Custom(package, versions, reason) => match reason {
974                        UnavailableReason::Package(_) => {
975                            lines.push(format!("{prefix}{package} {reason}"));
976                        }
977                        UnavailableReason::Version(_) => {
978                            lines.push(format!("{prefix}{package}{versions} {reason}"));
979                        }
980                    },
981                    External::NoVersions(package, versions) => {
982                        lines.push(format!("{prefix}no versions of {package}{versions}"));
983                    }
984                    External::NotRoot(package, versions) => {
985                        lines.push(format!("{prefix}not root {package}{versions}"));
986                    }
987                }
988            }
989            Frame::Terms(terms, depth) => {
990                let prefix = "  ".repeat(depth);
991                for (package, term) in terms {
992                    match term {
993                        Term::Positive(versions) => {
994                            lines.push(format!("{prefix}term {package}{versions}"));
995                        }
996                        Term::Negative(versions) => {
997                            lines.push(format!("{prefix}term not {package}{versions}"));
998                        }
999                    }
1000                }
1001            }
1002        }
1003    }
1004}
1005
1006/// Given a [`DerivationTree`], restate the availability of a package where a derivation reaches
1007/// past the versions its causes rule out.
1008///
1009/// A derivation rules out the union of what its causes rule out, but the reported version sets are
1010/// simplified onto the versions that exist, which drops the versions that do not. The derivation
1011/// then reaches past its causes over a range that holds nothing, and the report has to say so, as it
1012/// does for the ranges the resolver itself finds empty. The version sets are left as they are; only
1013/// the statement that carries them is added, and only when every version it adds is one that the
1014/// package does not have.
1015fn restate_available_versions(
1016    tree: ErrorTree,
1017    included_versions: &FxHashMap<PackageName, BTreeSet<Version>>,
1018) -> ErrorTree {
1019    map_derivation_tree(
1020        tree,
1021        DerivationTree::External,
1022        |metadata, cause1, cause2| {
1023            let Some((package, unlisted)) =
1024                unlisted_versions(&metadata.terms, &cause1, &cause2, included_versions)
1025            else {
1026                return derived_tree(metadata, cause1, cause2);
1027            };
1028
1029            let statement = || {
1030                DerivationTree::External(External::NoVersions(package.clone(), unlisted.clone()))
1031            };
1032            let carry = |cause: ErrorTree| {
1033                let carried = ruled_out_versions(&package, &cause).union(&unlisted);
1034                DerivationTree::Derived(Derived {
1035                    terms: Map::from_iter([(package.clone(), Term::Positive(carried))]),
1036                    shared_id: None,
1037                    cause1: Arc::new(cause),
1038                    cause2: Arc::new(statement()),
1039                })
1040            };
1041
1042            // The statement carries a step that rules the package out, so it goes on the cause that
1043            // does the ruling out. Where neither does, the step reaches past its causes on its own
1044            // and the statement carries the step itself.
1045            let first = ruled_out_versions(&package, &cause1);
1046            let second = ruled_out_versions(&package, &cause2);
1047            if first.is_empty() && second.is_empty() {
1048                let concluded = metadata
1049                    .terms
1050                    .get(&package)
1051                    .cloned()
1052                    .unwrap_or_else(|| Term::Positive(unlisted.clone()));
1053                let mut inner = metadata;
1054                let shared_id = inner.shared_id.take();
1055                inner.terms = Map::from_iter([(
1056                    package.clone(),
1057                    Term::Positive(
1058                        reported_versions(&package, &cause1)
1059                            .union(&reported_versions(&package, &cause2)),
1060                    ),
1061                )]);
1062                return DerivationTree::Derived(Derived {
1063                    terms: Map::from_iter([(package.clone(), concluded)]),
1064                    shared_id,
1065                    cause1: Arc::new(derived_tree(inner, cause1, cause2)),
1066                    cause2: Arc::new(statement()),
1067                });
1068            }
1069            if starts_lower(&first, &second) {
1070                derived_tree(metadata, carry(cause1), cause2)
1071            } else {
1072                derived_tree(metadata, cause1, carry(cause2))
1073            }
1074        },
1075    )
1076}
1077
1078/// The versions a derivation rules out without either cause mentioning them, when they are versions
1079/// the package does not have.
1080///
1081/// A derivation reaches past its causes in two places: the conclusion it draws about a package, and
1082/// the requirement it resolves for a package it drops from its terms.
1083fn unlisted_versions(
1084    terms: &ErrorTerms,
1085    cause1: &ErrorTree,
1086    cause2: &ErrorTree,
1087    included_versions: &FxHashMap<PackageName, BTreeSet<Version>>,
1088) -> Option<(PubGrubPackage, Range<Version>)> {
1089    // What the causes establish about the package carries its conclusion, but only what they rule
1090    // out can carry a requirement: a dependency of the package is not a statement that it cannot be
1091    // used.
1092    let concluded = concluded_package(terms).map(|(package, concluded)| {
1093        let reported =
1094            reported_versions(package, cause1).union(&reported_versions(package, cause2));
1095        (package, concluded.clone(), reported)
1096    });
1097    let resolved = cause_packages(cause1)
1098        .into_iter()
1099        .chain(cause_packages(cause2))
1100        .filter(|package| !terms.contains_key(*package))
1101        .map(|package| {
1102            let required =
1103                required_versions(package, cause1).union(&required_versions(package, cause2));
1104            let ruled_out =
1105                ruled_out_versions(package, cause1).union(&ruled_out_versions(package, cause2));
1106            (package, required, ruled_out)
1107        });
1108
1109    for (package, covered, reported) in concluded.into_iter().chain(resolved) {
1110        // A requirement that no cause rules out any of is unavailable outright rather than across a
1111        // range, which [`collapse_redundant_depends_on_no_versions`] reports without the listing.
1112        if reported.is_empty() {
1113            continue;
1114        }
1115        let unlisted = covered.intersection(&reported.complement());
1116        if unlisted.is_empty() {
1117            continue;
1118        }
1119        // Only a version the package does not have can go unmentioned; anything else is a
1120        // derivation its causes really do not support.
1121        let Some(versions) = package
1122            .name_no_root()
1123            .and_then(|name| included_versions.get(name))
1124        else {
1125            continue;
1126        };
1127        if versions.iter().any(|version| unlisted.contains(version)) {
1128            continue;
1129        }
1130        return Some((package.clone(), unlisted));
1131    }
1132    None
1133}
1134
1135/// Whether the first version set starts below the second, treating an empty set as starting above
1136/// every other.
1137fn starts_lower(first: &Range<Version>, second: &Range<Version>) -> bool {
1138    let lowest = |versions: &Range<Version>| {
1139        versions.bounding_range().map(|(lower, _)| match lower {
1140            Bound::Unbounded => None,
1141            Bound::Included(version) | Bound::Excluded(version) => Some(version.clone()),
1142        })
1143    };
1144    match (lowest(first), lowest(second)) {
1145        // An unbounded start sorts below every version.
1146        (Some(first), Some(second)) => first <= second,
1147        (Some(_), None) => true,
1148        (None, _) => false,
1149    }
1150}
1151
1152/// The package a derivation concludes about, when it concludes about one alone.
1153fn concluded_package(terms: &ErrorTerms) -> Option<(&PubGrubPackage, &Range<Version>)> {
1154    let mut terms = terms.iter();
1155    let (package, Term::Positive(versions)) = terms.next()? else {
1156        return None;
1157    };
1158    terms.next().is_none().then_some((package, versions))
1159}
1160
1161/// The packages a cause states something about.
1162fn cause_packages(cause: &ErrorTree) -> Vec<&PubGrubPackage> {
1163    match cause {
1164        DerivationTree::Derived(derived) => derived.terms.keys().collect(),
1165        DerivationTree::External(
1166            External::Custom(package, ..)
1167            | External::NoVersions(package, ..)
1168            | External::NotRoot(package, ..),
1169        ) => vec![package],
1170        DerivationTree::External(External::FromDependencyOf(package, _, dependency, _)) => {
1171            vec![package, dependency]
1172        }
1173    }
1174}
1175
1176/// The versions of `package` that a cause requires to remain usable.
1177fn required_versions(package: &PubGrubPackage, cause: &ErrorTree) -> Range<Version> {
1178    match cause {
1179        DerivationTree::Derived(derived) => match derived.terms.get(package) {
1180            Some(Term::Negative(versions)) => versions.clone(),
1181            _ => Range::empty(),
1182        },
1183        DerivationTree::External(External::FromDependencyOf(_, _, required, versions))
1184            if required == package =>
1185        {
1186            versions.clone()
1187        }
1188        DerivationTree::External(_) => Range::empty(),
1189    }
1190}
1191
1192/// The versions of `package` that a cause states cannot be used.
1193fn ruled_out_versions(package: &PubGrubPackage, cause: &ErrorTree) -> Range<Version> {
1194    match cause {
1195        DerivationTree::Derived(derived) => match concluded_package(&derived.terms) {
1196            Some((concluded, versions)) if concluded == package => versions.clone(),
1197            _ => Range::empty(),
1198        },
1199        DerivationTree::External(External::Custom(ruled_out, versions, _))
1200            if ruled_out == package =>
1201        {
1202            versions.clone()
1203        }
1204        DerivationTree::External(_) => Range::empty(),
1205    }
1206}
1207
1208/// The versions of `package` that a cause is reported as establishing something about.
1209fn reported_versions(package: &PubGrubPackage, cause: &ErrorTree) -> Range<Version> {
1210    match cause {
1211        DerivationTree::Derived(derived) => match derived.terms.get(package) {
1212            Some(Term::Positive(versions)) => versions.clone(),
1213            _ => Range::empty(),
1214        },
1215        DerivationTree::External(
1216            External::Custom(reported, versions, _)
1217            | External::NoVersions(reported, versions)
1218            | External::FromDependencyOf(reported, versions, ..),
1219        ) if reported == package => versions.clone(),
1220        DerivationTree::External(_) => Range::empty(),
1221    }
1222}
1223
1224fn can_drop_no_versions(
1225    package: &PubGrubPackage,
1226    versions: &Range<Version>,
1227    other: &ErrorTree,
1228    parent_terms: &ErrorTerms,
1229) -> bool {
1230    let package_terms = if let DerivationTree::Derived(derived) = other {
1231        derived.terms.get(package)
1232    } else {
1233        parent_terms.get(package)
1234    };
1235    let Some(Term::Positive(term)) = package_terms else {
1236        return false;
1237    };
1238    let versions = versions.complement();
1239
1240    // Retain exclusions of a single version because they produce useful messages like
1241    // "only foo==1.0.0 is available". Otherwise, the clause is redundant when the conclusion
1242    // covers either all versions or exactly the remaining range.
1243    versions.as_singleton().is_none() && (*term == Range::full() || *term == versions)
1244}
1245
1246fn collapse_redundant_no_versions(tree: ErrorTree) -> ErrorTree {
1247    map_derivation_tree(
1248        tree,
1249        DerivationTree::External,
1250        |metadata, cause1, cause2| {
1251            if let DerivationTree::External(External::NoVersions(package, versions)) = &cause1
1252                && can_drop_no_versions(package, versions, &cause2, &metadata.terms)
1253            {
1254                return cause2;
1255            }
1256
1257            if let DerivationTree::External(External::NoVersions(package, versions)) = &cause2
1258                && can_drop_no_versions(package, versions, &cause1, &metadata.terms)
1259            {
1260                return cause1;
1261            }
1262
1263            derived_tree(metadata, cause1, cause2)
1264        },
1265    )
1266}
1267
1268/// Given a [`DerivationTree`], collapse any derived trees with two `NoVersions` nodes for the same
1269/// package. For example, if we have a tree like:
1270///
1271/// ```text
1272/// term Python>=3.7.9
1273///   no versions of Python>=3.7.9, <3.8
1274///   no versions of Python>=3.8
1275/// ```
1276///
1277/// We can simplify this to:
1278///
1279/// ```text
1280/// no versions of Python>=3.7.9
1281/// ```
1282///
1283/// This function returns a `bool` indicating if a change was made. This allows for repeated calls,
1284/// e.g., the following tree contains nested redundant trees:
1285///
1286/// ```text
1287/// term Python>=3.10
1288///   no versions of Python>=3.11, <3.12
1289///   term Python>=3.10, <3.11 | >=3.12
1290///     no versions of Python>=3.12
1291///     no versions of Python>=3.10, <3.11
1292/// ```
1293///
1294/// We can simplify this to:
1295///
1296/// ```text
1297/// no versions of Python>=3.10
1298/// ```
1299///
1300/// This appears to be common with the way the resolver currently models Python version
1301/// incompatibilities.
1302fn collapse_redundant_no_versions_tree(tree: ErrorTree) -> (ErrorTree, bool) {
1303    let mut changed = false;
1304    let tree = map_derivation_tree(
1305        tree,
1306        DerivationTree::External,
1307        |metadata, cause1, cause2| {
1308            if let (
1309                DerivationTree::External(External::NoVersions(package, versions)),
1310                DerivationTree::External(External::NoVersions(other_package, other_versions)),
1311            ) = (&cause1, &cause2)
1312                && package == other_package
1313                && let Some(Term::Positive(term)) = metadata.terms.get(package)
1314                && versions.subset_of(term)
1315                && other_versions.subset_of(term)
1316            {
1317                changed = true;
1318                DerivationTree::External(External::NoVersions(package.clone(), term.clone()))
1319            } else {
1320                derived_tree(metadata, cause1, cause2)
1321            }
1322        },
1323    );
1324    (tree, changed)
1325}
1326
1327fn is_workspace_member(
1328    package: &PubGrubPackage,
1329    workspace_members: &BTreeSet<PackageName>,
1330) -> bool {
1331    let (PubGrubPackageInner::Package { name, .. }
1332    | PubGrubPackageInner::Extra { name, .. }
1333    | PubGrubPackageInner::Group { name, .. }) = &**package
1334    else {
1335        return false;
1336    };
1337    workspace_members.contains(name)
1338}
1339
1340/// Given a [`DerivationTree`], collapse any `NoVersion` incompatibilities for workspace members
1341/// to avoid saying things like "only <workspace-member>==0.1.0 is available".
1342fn collapse_no_versions_of_workspace_members(
1343    tree: ErrorTree,
1344    workspace_members: &BTreeSet<PackageName>,
1345) -> ErrorTree {
1346    map_derivation_tree(
1347        tree,
1348        DerivationTree::External,
1349        |metadata, cause1, cause2| {
1350            if let DerivationTree::External(External::NoVersions(package, _)) = &cause1
1351                && is_workspace_member(package, workspace_members)
1352            {
1353                return cause2;
1354            }
1355            if let DerivationTree::External(External::NoVersions(package, _)) = &cause2
1356                && is_workspace_member(package, workspace_members)
1357            {
1358                return cause1;
1359            }
1360            derived_tree(metadata, cause1, cause2)
1361        },
1362    )
1363}
1364
1365fn collapse_redundant_dependency_child(
1366    tree: ErrorTree,
1367    package: &PubGrubPackage,
1368    versions: &Range<Version>,
1369) -> ErrorTree {
1370    let DerivationTree::Derived(derived) = &tree else {
1371        return tree;
1372    };
1373    let dependency_clause = match (&*derived.cause1, &*derived.cause2) {
1374        (
1375            DerivationTree::External(External::NoVersions(no_versions_package, _)),
1376            dependency_clause @ DerivationTree::External(External::FromDependencyOf(
1377                _,
1378                _,
1379                dependency_package,
1380                dependency_versions,
1381            )),
1382        )
1383        | (
1384            dependency_clause @ DerivationTree::External(External::FromDependencyOf(
1385                _,
1386                _,
1387                dependency_package,
1388                dependency_versions,
1389            )),
1390            DerivationTree::External(External::NoVersions(no_versions_package, _)),
1391        ) if no_versions_package == dependency_package
1392            && package == no_versions_package
1393            && versions.subset_of(dependency_versions) =>
1394        {
1395            Some(dependency_clause.clone())
1396        }
1397        _ => None,
1398    };
1399
1400    dependency_clause.unwrap_or(tree)
1401}
1402
1403/// Given a [`DerivationTree`], collapse `NoVersions` incompatibilities that are redundant children
1404/// of a dependency. For example, if we have a tree like:
1405///
1406/// ```text
1407/// A>=1,<2 depends on B
1408///     A has no versions >1,<2
1409///     C depends on A>=1,<2
1410/// ```
1411///
1412/// We can simplify this to `C depends on A>=1 and A>=1 depends on B so C depends on B` without
1413/// explaining that there are no other versions of A. This requires the range of A in "A depends
1414/// on B" to be a subset of the range in "C depends on A". For example, in a tree like:
1415///
1416/// ```text
1417/// A>=1,<3 depends on B
1418///     A has no versions >2,<3
1419///     C depends on A>=2,<3
1420/// ```
1421///
1422/// We cannot apply the same simplification because `A>=1,<3` is not a subset of `A>=2,<3`.
1423fn collapse_redundant_depends_on_no_versions(tree: ErrorTree) -> ErrorTree {
1424    map_derivation_tree(
1425        tree,
1426        DerivationTree::External,
1427        |metadata, cause1, cause2| {
1428            if let DerivationTree::External(External::FromDependencyOf(package, versions, _, _)) =
1429                &cause1
1430            {
1431                let cause2 = collapse_redundant_dependency_child(cause2, package, versions);
1432                return derived_tree(metadata, cause1, cause2);
1433            }
1434            if let DerivationTree::External(External::FromDependencyOf(package, versions, _, _)) =
1435                &cause2
1436            {
1437                let cause1 = collapse_redundant_dependency_child(cause1, package, versions);
1438                return derived_tree(metadata, cause1, cause2);
1439            }
1440            derived_tree(metadata, cause1, cause2)
1441        },
1442    )
1443}
1444
1445/// Simplifies the markers on pubgrub packages in the given derivation tree
1446/// according to the given Python requirement.
1447///
1448/// For example, when there's a dependency like `foo ; python_version >= '3.11'` and
1449/// `requires-python = '>=3.11'`, this removes the redundant `python_version >= '3.11'` marker from
1450/// the error message.
1451fn simplify_derivation_tree_markers(
1452    tree: ErrorTree,
1453    python_requirement: &PythonRequirement,
1454) -> ErrorTree {
1455    map_derivation_tree(
1456        tree,
1457        |mut external| {
1458            match &mut external {
1459                External::NotRoot(package, _) | External::NoVersions(package, _) => {
1460                    package.simplify_markers(python_requirement);
1461                }
1462                External::FromDependencyOf(package1, _, package2, _) => {
1463                    package1.simplify_markers(python_requirement);
1464                    package2.simplify_markers(python_requirement);
1465                }
1466                External::Custom(package, _, _) => package.simplify_markers(python_requirement),
1467            }
1468            DerivationTree::External(external)
1469        },
1470        |mut metadata, cause1, cause2| {
1471            metadata.terms = metadata
1472                .terms
1473                .into_iter()
1474                .map(|(mut package, term)| {
1475                    package.simplify_markers(python_requirement);
1476                    (package, term)
1477                })
1478                .collect();
1479            derived_tree(metadata, cause1, cause2)
1480        },
1481    )
1482}
1483
1484fn merge_unavailable_versions(
1485    package: &PubGrubPackage,
1486    versions: &Range<Version>,
1487    reason: &UnavailableReason,
1488    other: &ErrorTree,
1489) -> Option<ErrorTree> {
1490    let DerivationTree::Derived(derived) = other else {
1491        return None;
1492    };
1493    let merge = |cause: &ErrorTree| {
1494        let DerivationTree::External(External::Custom(other_package, other_versions, other_reason)) =
1495            cause
1496        else {
1497            return None;
1498        };
1499        (package == other_package && reason == other_reason).then(|| other_versions.union(versions))
1500    };
1501
1502    // Keep the two cases separate to preserve the ordering of the causes.
1503    let (unchanged_cause, merged_versions, merged_is_cause2) =
1504        if let Some(merged_versions) = merge(&derived.cause2) {
1505            (derived.cause1.clone(), merged_versions, true)
1506        } else {
1507            let merged_versions = merge(&derived.cause1)?;
1508            (derived.cause2.clone(), merged_versions, false)
1509        };
1510
1511    let merged_cause = Arc::new(DerivationTree::External(External::Custom(
1512        package.clone(),
1513        merged_versions.clone(),
1514        reason.clone(),
1515    )));
1516    let (cause1, cause2) = if merged_is_cause2 {
1517        (unchanged_cause, merged_cause)
1518    } else {
1519        (merged_cause, unchanged_cause)
1520    };
1521
1522    let mut terms = derived.terms.clone();
1523    if let Some(Term::Positive(range)) = terms.get_mut(package) {
1524        *range = merged_versions;
1525    }
1526    Some(DerivationTree::Derived(Derived {
1527        terms,
1528        shared_id: derived.shared_id,
1529        cause1,
1530        cause2,
1531    }))
1532}
1533
1534/// Combine two unavailabilities of one package that are the causes of the same derivation.
1535///
1536/// A derivation whose two causes both say that a package cannot be used concludes their union. If
1537/// they say it for the same reason, they are one statement about a larger set and the derivation
1538/// collapses into it, as [`merge_unavailable_versions`] does for a cause nested one level deeper.
1539/// Otherwise the two statements stay separate and only the conclusion is widened to cover them
1540/// both.
1541///
1542/// Two unavailabilities are siblings rather than nested when their version sets are contiguous.
1543fn merge_unavailable_siblings(derived: &ErrorDerived) -> Option<ErrorTree> {
1544    let DerivationTree::External(External::Custom(package, versions, reason)) = &*derived.cause1
1545    else {
1546        return None;
1547    };
1548    let DerivationTree::External(External::Custom(other_package, other_versions, other_reason)) =
1549        &*derived.cause2
1550    else {
1551        return None;
1552    };
1553    if package != other_package {
1554        return None;
1555    }
1556
1557    // Only rewrite a derivation that concludes about this package alone.
1558    let mut terms = derived.terms.iter();
1559    let Some((term_package, Term::Positive(term_versions))) = terms.next() else {
1560        return None;
1561    };
1562    if terms.next().is_some() || term_package != package {
1563        return None;
1564    }
1565
1566    let versions = versions.union(other_versions);
1567    if reason == other_reason {
1568        return Some(DerivationTree::External(External::Custom(
1569            package.clone(),
1570            versions,
1571            reason.clone(),
1572        )));
1573    }
1574    if versions.subset_of(term_versions) {
1575        return None;
1576    }
1577    Some(DerivationTree::Derived(Derived {
1578        terms: Map::from_iter([(
1579            package.clone(),
1580            Term::Positive(term_versions.union(&versions)),
1581        )]),
1582        shared_id: derived.shared_id,
1583        cause1: derived.cause1.clone(),
1584        cause2: derived.cause2.clone(),
1585    }))
1586}
1587
1588/// Given a [`DerivationTree`], collapse incompatibilities for versions of a package that are
1589/// unavailable for the same reason to avoid repeating the same message for every unavailable
1590/// version.
1591fn collapse_unavailable_versions(tree: ErrorTree) -> ErrorTree {
1592    map_derivation_tree(
1593        tree,
1594        DerivationTree::External,
1595        |metadata, cause1, cause2| {
1596            let tree = if let DerivationTree::External(External::Custom(package, versions, reason)) =
1597                &cause1
1598                && let Some(tree) = merge_unavailable_versions(package, versions, reason, &cause2)
1599            {
1600                tree
1601            } else if let DerivationTree::External(External::Custom(package, versions, reason)) =
1602                &cause2
1603                && let Some(tree) = merge_unavailable_versions(package, versions, reason, &cause1)
1604            {
1605                tree
1606            } else {
1607                derived_tree(metadata, cause1, cause2)
1608            };
1609
1610            // Merging a cause can also leave two unavailabilities of one package side by side.
1611            if let DerivationTree::Derived(derived) = &tree
1612                && let Some(merged) = merge_unavailable_siblings(derived)
1613            {
1614                return merged;
1615            }
1616            tree
1617        },
1618    )
1619}
1620
1621fn is_root_dependency_on_project(external: &ErrorExternal, project: &PackageName) -> bool {
1622    let External::FromDependencyOf(package, _, dependency, _) = external else {
1623        return false;
1624    };
1625    if !matches!(&**package, PubGrubPackageInner::Root(_)) {
1626        return false;
1627    }
1628    matches!(&**dependency, PubGrubPackageInner::Package { name, .. } if name == project)
1629}
1630
1631/// Given a [`DerivationTree`], drop dependency incompatibilities from the root to the project.
1632///
1633/// This effectively changes the root to the workspace member in a single-project workspace,
1634/// avoiding an extra level of indirection like "your project requires your project".
1635///
1636/// A direct dependency incompatibility is also a traversal boundary: if it is not the
1637/// root-to-project edge, leave that subtree unchanged. After removing a matching edge, continue
1638/// through only the opposite cause.
1639fn drop_root_dependency_on_project(tree: ErrorTree, project: &PackageName) -> ErrorTree {
1640    let mut tasks = vec![TreeTask::Visit(StackSafeErrorTree::new(tree))];
1641    let mut results = Vec::new();
1642
1643    while let Some(task) = tasks.pop() {
1644        match task {
1645            TreeTask::Visit(tree) => match tree.into_inner() {
1646                DerivationTree::External(external) => {
1647                    results.push(StackSafeErrorTree::new(DerivationTree::External(external)));
1648                }
1649                DerivationTree::Derived(derived) => {
1650                    let first_dependency = match derived.cause1.as_ref() {
1651                        DerivationTree::External(external @ External::FromDependencyOf(..)) => {
1652                            Some((external, true))
1653                        }
1654                        _ => match derived.cause2.as_ref() {
1655                            DerivationTree::External(external @ External::FromDependencyOf(..)) => {
1656                                Some((external, false))
1657                            }
1658                            _ => None,
1659                        },
1660                    };
1661
1662                    if let Some((external, dependency_is_cause1)) = first_dependency {
1663                        if is_root_dependency_on_project(external, project) {
1664                            let other = if dependency_is_cause1 {
1665                                Arc::unwrap_or_clone(derived.cause2)
1666                            } else {
1667                                Arc::unwrap_or_clone(derived.cause1)
1668                            };
1669                            tasks.push(TreeTask::Visit(StackSafeErrorTree::new(other)));
1670                        } else {
1671                            results.push(StackSafeErrorTree::new(DerivationTree::Derived(derived)));
1672                        }
1673                    } else {
1674                        schedule_derived(&mut tasks, derived);
1675                    }
1676                }
1677            },
1678            TreeTask::Rebuild(metadata) => {
1679                let cause2 = results
1680                    .pop()
1681                    .expect("every derived tree has a second reduced cause")
1682                    .into_inner();
1683                let cause1 = results
1684                    .pop()
1685                    .expect("every derived tree has a first reduced cause")
1686                    .into_inner();
1687                results.push(StackSafeErrorTree::new(derived_tree(
1688                    metadata, cause1, cause2,
1689                )));
1690            }
1691        }
1692    }
1693
1694    results
1695        .pop()
1696        .expect("the root derivation tree produces one reduced result")
1697        .into_inner()
1698}
1699
1700/// A prefix match, e.g., `==2.4.*`, which is desugared to a range like `>=2.4.dev0,<2.5.dev0`.
1701#[derive(Debug, Clone, PartialEq, Eq)]
1702pub(crate) struct PrefixMatch<'a> {
1703    version: &'a Version,
1704}
1705
1706impl<'a> PrefixMatch<'a> {
1707    /// Determine whether a given range is equivalent to a prefix match (e.g., `==2.4.*`).
1708    ///
1709    /// Prefix matches are desugared to (e.g.) `>=2.4.dev0,<2.5.dev0`, but we want to render them
1710    /// as `==2.4.*` in error messages.
1711    pub(crate) fn from_range(lower: Bound<&'a Version>, upper: Bound<&'a Version>) -> Option<Self> {
1712        let Bound::Included(lower) = lower else {
1713            return None;
1714        };
1715        let Bound::Excluded(upper) = upper else {
1716            return None;
1717        };
1718        if lower.is_pre() || lower.is_post() || lower.is_local() {
1719            return None;
1720        }
1721        if upper.is_pre() || upper.is_post() || upper.is_local() {
1722            return None;
1723        }
1724        if lower.dev() != Some(0) {
1725            return None;
1726        }
1727        if upper.dev() != Some(0) {
1728            return None;
1729        }
1730        if lower.release().len() != upper.release().len() {
1731            return None;
1732        }
1733
1734        // All segments should be the same, except the last one, which should be incremented.
1735        let num_segments = lower.release().len();
1736        for (i, (lower, upper)) in lower
1737            .release()
1738            .iter()
1739            .zip(upper.release().iter())
1740            .enumerate()
1741        {
1742            if i == num_segments - 1 {
1743                if lower + 1 != *upper {
1744                    return None;
1745                }
1746            } else {
1747                if lower != upper {
1748                    return None;
1749                }
1750            }
1751        }
1752
1753        Some(PrefixMatch { version: lower })
1754    }
1755}
1756
1757impl std::fmt::Display for PrefixMatch<'_> {
1758    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1759        write!(f, "=={}.*", self.version.only_release())
1760    }
1761}
1762
1763#[derive(Debug)]
1764pub struct NoSolutionHeader {
1765    /// The [`ResolverEnvironment`] that caused the failure.
1766    env: ResolverEnvironment,
1767    /// The additional context for the resolution failure.
1768    context: Option<&'static str>,
1769}
1770
1771impl NoSolutionHeader {
1772    /// Create a new [`NoSolutionHeader`] with the given [`ResolverEnvironment`].
1773    pub fn new(env: ResolverEnvironment) -> Self {
1774        Self { env, context: None }
1775    }
1776
1777    /// Set the context for the resolution failure.
1778    #[must_use]
1779    pub fn with_context(mut self, context: &'static str) -> Self {
1780        self.context = Some(context);
1781        self
1782    }
1783}
1784
1785impl std::fmt::Display for NoSolutionHeader {
1786    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1787        match (self.context, self.env.end_user_fork_display()) {
1788            (None, None) => write!(f, "No solution found when resolving dependencies"),
1789            (Some(context), None) => {
1790                write!(f, "No solution found when resolving {context} dependencies")
1791            }
1792            (None, Some(split)) => write!(
1793                f,
1794                "No solution found when resolving dependencies for {split}"
1795            ),
1796            (Some(context), Some(split)) => write!(
1797                f,
1798                "No solution found when resolving {context} dependencies for {split}"
1799            ),
1800        }
1801    }
1802}
1803
1804/// Given a [`DerivationTree`], simplify version ranges using the included versions for each
1805/// package.
1806fn simplify_derivation_tree_ranges(
1807    tree: ErrorTree,
1808    included_versions: &FxHashMap<PackageName, BTreeSet<Version>>,
1809    candidate_selector: &CandidateSelector,
1810    resolver_environment: &ResolverEnvironment,
1811) -> ErrorTree {
1812    map_derivation_tree(
1813        tree,
1814        |mut external| {
1815            match &mut external {
1816                External::FromDependencyOf(package1, versions1, package2, versions2) => {
1817                    if let Some(simplified) = simplify_range(
1818                        versions1,
1819                        package1,
1820                        included_versions,
1821                        candidate_selector,
1822                        resolver_environment,
1823                    ) {
1824                        *versions1 = simplified;
1825                    }
1826                    if let Some(simplified) = simplify_range(
1827                        versions2,
1828                        package2,
1829                        included_versions,
1830                        candidate_selector,
1831                        resolver_environment,
1832                    ) {
1833                        *versions2 = simplified;
1834                    }
1835                }
1836                External::NoVersions(package, versions) => {
1837                    if let Some(simplified) = simplify_range(
1838                        versions,
1839                        package,
1840                        included_versions,
1841                        candidate_selector,
1842                        resolver_environment,
1843                    ) {
1844                        *versions = simplified;
1845                    }
1846                }
1847                External::Custom(package, versions, _) => {
1848                    if let Some(simplified) = simplify_range(
1849                        versions,
1850                        package,
1851                        included_versions,
1852                        candidate_selector,
1853                        resolver_environment,
1854                    ) {
1855                        *versions = simplified;
1856                    }
1857                }
1858                External::NotRoot(..) => {}
1859            }
1860            DerivationTree::External(external)
1861        },
1862        |mut metadata, cause1, cause2| {
1863            metadata.terms = metadata
1864                .terms
1865                .into_iter()
1866                .map(|(package, term)| {
1867                    let term = match term {
1868                        Term::Positive(versions) => Term::Positive(
1869                            simplify_range(
1870                                &versions,
1871                                &package,
1872                                included_versions,
1873                                candidate_selector,
1874                                resolver_environment,
1875                            )
1876                            .unwrap_or(versions),
1877                        ),
1878                        Term::Negative(versions) => Term::Negative(
1879                            simplify_range(
1880                                &versions,
1881                                &package,
1882                                included_versions,
1883                                candidate_selector,
1884                                resolver_environment,
1885                            )
1886                            .unwrap_or(versions),
1887                        ),
1888                    };
1889                    (package, term)
1890                })
1891                .collect();
1892            derived_tree(metadata, cause1, cause2)
1893        },
1894    )
1895}
1896
1897/// Helper function to simplify a version range using included versions for a package.
1898///
1899/// If the range cannot be simplified, `None` is returned.
1900fn simplify_range(
1901    range: &Range<Version>,
1902    package: &PubGrubPackage,
1903    included_versions: &FxHashMap<PackageName, BTreeSet<Version>>,
1904    candidate_selector: &CandidateSelector,
1905    resolver_environment: &ResolverEnvironment,
1906) -> Option<Range<Version>> {
1907    // If there's not a package name or included versions, we can't simplify anything
1908    let name = package.name()?;
1909    let versions = included_versions.get(name)?;
1910
1911    // If this is a full range, there's nothing to simplify
1912    if range.encoded_versions() == &Ranges::full() {
1913        return None;
1914    }
1915
1916    // If there's only one version available and it's in the range, return just that version
1917    if let Some(version) = versions.iter().next() {
1918        if versions.len() == 1 && range.contains(version) {
1919            return Some(Range::singleton(version.clone()));
1920        }
1921    }
1922
1923    // Check if pre-releases are allowed
1924    let prereleases_not_allowed = candidate_selector
1925        .prerelease_strategy()
1926        .selection(name, resolver_environment)
1927        == PrereleaseSelection::Disallow;
1928
1929    let any_prerelease = range.iter().any(|(start, end)| {
1930        let is_pre1 = match start {
1931            Bound::Included(version) => version.any_prerelease(),
1932            Bound::Excluded(version) => version.any_prerelease(),
1933            Bound::Unbounded => false,
1934        };
1935        let is_pre2 = match end {
1936            Bound::Included(version) => version.any_prerelease(),
1937            Bound::Excluded(version) => version.any_prerelease(),
1938            Bound::Unbounded => false,
1939        };
1940        is_pre1 || is_pre2
1941    });
1942
1943    // Simplify the range, as implemented in PubGrub
1944    Some(Range::from_versions(range.simplify(
1945        versions.iter().filter(|version| {
1946            // If there are pre-releases in the range segments, we need to include pre-releases
1947            if any_prerelease {
1948                return true;
1949            }
1950
1951            // If pre-releases are not allowed, filter out pre-releases
1952            if prereleases_not_allowed && version.any_prerelease() {
1953                return false;
1954            }
1955
1956            // Otherwise, include the version
1957            true
1958        }),
1959    )))
1960}
1961
1962#[cfg(test)]
1963mod tests {
1964    use std::assert_matches;
1965    use uv_resolver_types::PackageNodeKind;
1966
1967    use super::*;
1968    use crate::resolver::UnavailableVersion;
1969
1970    fn deep_derivation_tree() -> ErrorTree {
1971        let package = PubGrubPackage::from(PubGrubPackageInner::Root(None));
1972        let leaf = ErrorTree::External(External::NotRoot(package, Version::new([1_u64])));
1973        let mut tree = leaf.clone();
1974
1975        for _ in 0..100_000 {
1976            tree = ErrorTree::Derived(Derived {
1977                terms: pubgrub::Map::default(),
1978                shared_id: None,
1979                cause1: Arc::new(tree),
1980                cause2: Arc::new(leaf.clone()),
1981            });
1982        }
1983
1984        tree
1985    }
1986
1987    fn pubgrub_package(name: &str) -> PubGrubPackage {
1988        PubGrubPackage::from(PubGrubPackageInner::Package {
1989            name: package_name(name),
1990            kind: PackageNodeKind::Base,
1991            marker: uv_pep508::MarkerTree::TRUE,
1992        })
1993    }
1994
1995    fn package_name(name: &str) -> PackageName {
1996        name.parse().expect("valid package name")
1997    }
1998
1999    fn version(version: &str) -> Version {
2000        version.parse().expect("valid version")
2001    }
2002
2003    fn known_versions(name: &str, versions: &[&str]) -> FxHashMap<PackageName, Arc<[Version]>> {
2004        let versions: Arc<[Version]> = versions.iter().copied().map(version).collect();
2005        FxHashMap::from_iter([(package_name(name), versions)])
2006    }
2007
2008    fn unavailable(package: &PubGrubPackage, versions: Range<Version>) -> ErrorTree {
2009        ErrorTree::External(External::Custom(
2010            package.clone(),
2011            versions,
2012            UnavailableReason::Version(UnavailableVersion::InvalidMetadata),
2013        ))
2014    }
2015
2016    fn narrow_unavailable(
2017        package: &PubGrubPackage,
2018        versions: Range<Version>,
2019        known_versions: &FxHashMap<PackageName, Arc<[Version]>>,
2020    ) -> Range<Version> {
2021        let narrowed =
2022            NoSolutionError::narrow_widened_sets(unavailable(package, versions), known_versions);
2023        let ErrorTree::External(External::Custom(_, versions, _)) = narrowed else {
2024            panic!("expected a custom incompatibility");
2025        };
2026        versions
2027    }
2028
2029    /// A widened unavailable set is narrowed back onto the known versions.
2030    #[test]
2031    fn narrows_widened_unavailable_versions() {
2032        let package = pubgrub_package("numpy");
2033        let known_versions = known_versions("numpy", &["1.0", "2.0", "3.0"]);
2034
2035        // A gap-widened rejection reports the single version it excludes.
2036        let widened = Range::singleton(version("2.0")).widen_versions(&[
2037            version("1.0"),
2038            version("2.0"),
2039            version("3.0"),
2040        ]);
2041        assert_eq!(widened.to_string(), ">1.0, <3.0");
2042        assert_eq!(
2043            narrow_unavailable(&package, widened, &known_versions),
2044            Range::singleton(version("2.0"))
2045        );
2046
2047        // At the top of a listing the widened set is unbounded, but the rejection says nothing
2048        // about versions that are not published yet.
2049        let widened = Range::from_range_bounds(version("2.0")..);
2050        assert_eq!(
2051            narrow_unavailable(&package, widened, &known_versions),
2052            Range::from_range_bounds(version("2.0")..=version("3.0"))
2053        );
2054
2055        // Merged rejections covering every known version report as the full range.
2056        let merged = Range::from_range_bounds(version("0")..);
2057        assert_eq!(
2058            narrow_unavailable(&package, merged, &known_versions),
2059            Range::full()
2060        );
2061
2062        // A rejected version outside the known versions has no envelope to report it in.
2063        let widened = Range::from_range_bounds(version("4.0")..version("5.0"));
2064        assert_eq!(
2065            narrow_unavailable(&package, widened.clone(), &known_versions),
2066            widened
2067        );
2068
2069        // A package without a known version list is reported as recorded.
2070        let widened = Range::from_range_bounds(version("1.0")..version("3.0"));
2071        assert_eq!(
2072            narrow_unavailable(&pubgrub_package("scipy"), widened.clone(), &known_versions),
2073            widened
2074        );
2075    }
2076
2077    /// A rejection of the one version a package has reports that version.
2078    #[test]
2079    fn narrows_widened_unavailable_version_of_one_version() {
2080        let package = pubgrub_package("numpy");
2081        let known_versions = known_versions("numpy", &["2.0"]);
2082
2083        assert_eq!(
2084            narrow_unavailable(&package, Range::full(), &known_versions),
2085            Range::singleton(version("2.0"))
2086        );
2087    }
2088
2089    /// A conclusion about a package with one version reports that version where its causes do.
2090    #[test]
2091    fn narrows_widened_conclusion_of_one_version() {
2092        let package = pubgrub_package("numpy");
2093        let known_versions = known_versions("numpy", &["2.0"]);
2094        let concluded = |cause: ErrorTree| {
2095            let tree = ErrorTree::Derived(Derived {
2096                terms: pubgrub::Map::from_iter([(package.clone(), Term::Positive(Range::full()))]),
2097                shared_id: None,
2098                cause1: Arc::new(cause),
2099                cause2: Arc::new(ErrorTree::External(External::NotRoot(
2100                    PubGrubPackage::from(PubGrubPackageInner::Root(None)),
2101                    version("1.0"),
2102                ))),
2103            });
2104            let ErrorTree::Derived(narrowed) =
2105                NoSolutionError::narrow_widened_sets(tree, &known_versions)
2106            else {
2107                panic!("expected a derived incompatibility");
2108            };
2109            narrowed.terms.get(&package).cloned()
2110        };
2111
2112        // The cause reports the version, so the conclusion stops there too.
2113        assert_eq!(
2114            concluded(unavailable(&package, Range::full())),
2115            Some(Term::Positive(Range::singleton(version("2.0"))))
2116        );
2117
2118        // A cause that reports every version of the package leaves the conclusion as it is.
2119        assert_eq!(
2120            concluded(ErrorTree::External(External::NoVersions(
2121                package.clone(),
2122                Range::full(),
2123            ))),
2124            Some(Term::Positive(Range::full()))
2125        );
2126    }
2127
2128    /// Two rejections of the same package for the same reason read as one statement.
2129    #[test]
2130    fn collapses_sibling_unavailable_versions() {
2131        let package = pubgrub_package("numpy");
2132        let cause1 = unavailable(&package, Range::singleton(version("1.0")));
2133        let cause2 = unavailable(
2134            &package,
2135            Range::from_range_bounds(version("2.0")..=version("3.0")),
2136        );
2137        let terms = pubgrub::Map::from_iter([(
2138            package.clone(),
2139            Term::Positive(Range::from_range_bounds(version("2.0")..=version("3.0"))),
2140        )]);
2141        let tree = ErrorTree::Derived(Derived {
2142            terms,
2143            shared_id: None,
2144            cause1: Arc::new(cause1),
2145            cause2: Arc::new(cause2),
2146        });
2147
2148        let collapsed = collapse_unavailable_versions(tree);
2149        let ErrorTree::External(External::Custom(_, versions, _)) = collapsed else {
2150            panic!("expected a custom incompatibility");
2151        };
2152        assert_eq!(versions.to_string(), "==1.0 | >=2.0, <=3.0");
2153    }
2154
2155    /// A derivation that concludes about more than the unavailable package is left alone.
2156    #[test]
2157    fn keeps_sibling_unavailable_versions_concluding_about_another_package() {
2158        let package = pubgrub_package("numpy");
2159        let cause1 = unavailable(&package, Range::singleton(version("1.0")));
2160        let cause2 = unavailable(&package, Range::singleton(version("3.0")));
2161        let terms = pubgrub::Map::from_iter([
2162            (package.clone(), Term::Positive(Range::full())),
2163            (pubgrub_package("scipy"), Term::Positive(Range::full())),
2164        ]);
2165        let tree = ErrorTree::Derived(Derived {
2166            terms,
2167            shared_id: None,
2168            cause1: Arc::new(cause1),
2169            cause2: Arc::new(cause2),
2170        });
2171
2172        assert_matches!(collapse_unavailable_versions(tree), ErrorTree::Derived(_));
2173    }
2174
2175    #[test]
2176    fn drops_transformed_derivation_tree_without_recursion() -> std::io::Result<()> {
2177        let thread = std::thread::Builder::new()
2178            .stack_size(256 * 1024)
2179            .spawn(|| {
2180                let _tree = StackSafeErrorTree::new(deep_derivation_tree());
2181            })?;
2182
2183        assert!(thread.join().is_ok());
2184        Ok(())
2185    }
2186
2187    #[test]
2188    fn derivation_tree_packages_are_unique() {
2189        let tree = StackSafeErrorTree::new(deep_derivation_tree());
2190        assert_eq!(derivation_tree_packages(&tree).count(), 1);
2191    }
2192
2193    #[test]
2194    fn collapse_proxies_drops_source_tree_without_recursion() -> std::io::Result<()> {
2195        let thread = std::thread::Builder::new()
2196            .stack_size(256 * 1024)
2197            .spawn(|| {
2198                let tree = NoSolutionError::collapse_proxies(deep_derivation_tree());
2199                drop_derivation_tree(tree);
2200            })?;
2201
2202        assert!(thread.join().is_ok());
2203        Ok(())
2204    }
2205
2206    #[test]
2207    fn collapse_local_versions_drops_source_tree_without_recursion() -> std::io::Result<()> {
2208        let thread = std::thread::Builder::new()
2209            .stack_size(256 * 1024)
2210            .spawn(|| {
2211                let tree = NoSolutionError::collapse_local_version_segments(deep_derivation_tree());
2212                drop_derivation_tree(tree);
2213            })?;
2214
2215        assert!(thread.join().is_ok());
2216        Ok(())
2217    }
2218
2219    #[test]
2220    fn formats_debug_derivation_tree_without_recursion() -> std::io::Result<()> {
2221        let thread = std::thread::Builder::new()
2222            .stack_size(256 * 1024)
2223            .spawn(|| {
2224                let tree = StackSafeErrorTree::new(deep_derivation_tree());
2225                let _formatted = format!("{tree:?}");
2226            })?;
2227
2228        assert!(thread.join().is_ok());
2229        Ok(())
2230    }
2231
2232    #[test]
2233    fn iterative_debug_matches_pubgrub_debug() {
2234        let package = PubGrubPackage::from(PubGrubPackageInner::Root(None));
2235        let leaf = ErrorTree::External(External::NotRoot(package, Version::new([1_u64])));
2236        let tree = StackSafeErrorTree::new(ErrorTree::Derived(Derived {
2237            terms: pubgrub::Map::default(),
2238            shared_id: Some(1),
2239            cause1: Arc::new(leaf.clone()),
2240            cause2: Arc::new(leaf),
2241        }));
2242
2243        let inner = &*tree;
2244        assert_eq!(format!("{inner:?}"), format!("{tree:?}"));
2245    }
2246}