Skip to main content

uv_resolver/
error.rs

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