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