Skip to main content

uv_resolver/
error.rs

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