Skip to main content

uv_resolver/
error.rs

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