1use std::borrow::Cow;
2use std::collections::BTreeMap;
3use std::fmt::{Display, Formatter};
4use std::sync::Arc;
5
6use indexmap::IndexSet;
7use petgraph::{
8 Directed, Direction,
9 graph::{Graph, NodeIndex},
10};
11use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
12
13use uv_configuration::{Constraints, Overrides};
14use uv_distribution::Metadata;
15use uv_distribution_types::{
16 Dist, DistributionId, Edge, Identifier, IndexUrl, Name, Node, Requirement, RequiresPython,
17 ResolutionDiagnostic, ResolvedDist,
18};
19use uv_git::GitResolver;
20use uv_normalize::{ExtraName, GroupName, PackageName};
21use uv_pep440::{Version, VersionSpecifier};
22use uv_pep508::{MarkerEnvironment, MarkerTree, MarkerTreeKind};
23use uv_pypi_types::{Conflicts, HashDigests, ParsedUrlError, VerbatimParsedUrl, Yanked};
24
25use crate::graph_ops::{marker_reachability, simplify_conflict_markers};
26use crate::pins::FilePins;
27use crate::preferences::Preferences;
28use crate::redirect::url_to_precise;
29use crate::resolution::AnnotatedDist;
30use crate::resolution_mode::ResolutionStrategy;
31use crate::resolver::{Resolution, ResolutionDependencyEdge, ResolutionPackage};
32use crate::universal_marker::{ConflictMarker, UniversalMarker};
33use crate::{InMemoryIndex, MetadataResponse, Options, ResolveError, VersionsResponse};
34
35#[derive(Debug)]
40pub struct ResolverOutput {
41 pub(crate) graph: Graph<ResolutionGraphNode, UniversalMarker, Directed>,
43 pub(crate) requires_python: RequiresPython,
45 pub(crate) fork_markers: Vec<UniversalMarker>,
48 pub(crate) diagnostics: Vec<ResolutionDiagnostic>,
50 pub(crate) requirements: Vec<Requirement>,
52 pub(crate) constraints: Constraints,
54 pub(crate) overrides: Overrides,
56 pub(crate) options: Options,
58}
59
60#[derive(Debug, Clone)]
61#[expect(clippy::large_enum_variant)]
62pub(crate) enum ResolutionGraphNode {
63 Root,
64 Dist(AnnotatedDist),
65}
66
67impl ResolutionGraphNode {
68 pub(crate) fn marker(&self) -> &UniversalMarker {
69 match self {
70 Self::Root => &UniversalMarker::TRUE,
71 Self::Dist(dist) => &dist.marker,
72 }
73 }
74
75 pub(crate) fn package_extra_names(&self) -> Option<(&PackageName, &ExtraName)> {
76 match self {
77 Self::Root => None,
78 Self::Dist(dist) => {
79 let extra = dist.extra.as_ref()?;
80 Some((&dist.name, extra))
81 }
82 }
83 }
84
85 pub(crate) fn package_group_names(&self) -> Option<(&PackageName, &GroupName)> {
86 match self {
87 Self::Root => None,
88 Self::Dist(dist) => {
89 let group = dist.group.as_ref()?;
90 Some((&dist.name, group))
91 }
92 }
93 }
94
95 pub(crate) fn package_name(&self) -> Option<&PackageName> {
96 match self {
97 Self::Root => None,
98 Self::Dist(dist) => Some(&dist.name),
99 }
100 }
101}
102
103impl Display for ResolutionGraphNode {
104 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
105 match self {
106 Self::Root => f.write_str("root"),
107 Self::Dist(dist) => Display::fmt(dist, f),
108 }
109 }
110}
111
112#[derive(Debug, Eq, PartialEq, Hash)]
113struct PackageRef<'a> {
114 package_name: &'a PackageName,
115 version: &'a Version,
116 url: Option<&'a VerbatimParsedUrl>,
117 index: Option<&'a IndexUrl>,
118 extra: Option<&'a ExtraName>,
119 group: Option<&'a GroupName>,
120}
121
122impl ResolverOutput {
123 pub(crate) fn from_state(
125 resolutions: &[Resolution],
126 requirements: Vec<Requirement>,
127 constraints: Constraints,
128 overrides: Overrides,
129 preferences: &Preferences,
130 index: &InMemoryIndex,
131 git: &GitResolver,
132 requires_python: RequiresPython,
133 conflicts: &Conflicts,
134 resolution_strategy: &ResolutionStrategy,
135 options: Options,
136 ) -> Result<Self, ResolveError> {
137 let size_guess = resolutions[0].nodes.len();
138 let mut graph: Graph<ResolutionGraphNode, UniversalMarker, Directed> =
139 Graph::with_capacity(size_guess, size_guess);
140 let mut inverse: FxHashMap<PackageRef, NodeIndex<u32>> =
141 FxHashMap::with_capacity_and_hasher(size_guess, FxBuildHasher);
142 let mut diagnostics = Vec::new();
143
144 let root_index = graph.add_node(ResolutionGraphNode::Root);
146
147 let mut seen = FxHashSet::default();
148 for resolution in resolutions {
149 for (package, version) in &resolution.nodes {
151 if !seen.insert((package, version)) {
152 continue;
154 }
155 Self::add_version(
156 &mut graph,
157 &mut inverse,
158 &mut diagnostics,
159 preferences,
160 &resolution.pins,
161 index,
162 git,
163 package,
164 version,
165 )?;
166 }
167 }
168
169 let mut seen = FxHashSet::default();
170 for resolution in resolutions {
171 let marker = resolution.env.try_universal_markers().unwrap_or_default();
172
173 for edge in &resolution.edges {
176 if !seen.insert((edge, marker)) {
177 continue;
179 }
180
181 Self::add_edge(&mut graph, &mut inverse, root_index, edge, marker);
182 }
183 }
184
185 let fork_markers: Vec<UniversalMarker> = if let [resolution] = resolutions {
186 resolution
190 .env
191 .try_universal_markers()
192 .into_iter()
193 .filter(|marker| !marker.is_true())
194 .collect()
195 } else {
196 resolutions
197 .iter()
198 .map(|resolution| resolution.env.try_universal_markers().unwrap_or_default())
199 .collect()
200 };
201
202 let mut reachability = marker_reachability(&graph, &fork_markers);
204
205 let conflict_marker = ConflictMarker::from_conflicts(conflicts);
208 for index in graph.node_indices() {
209 if let ResolutionGraphNode::Dist(dist) = &mut graph[index] {
210 dist.marker = reachability.remove(&index).unwrap_or_default();
211 dist.marker.imbibe(conflict_marker);
212 }
213 }
214 for weight in graph.edge_weights_mut() {
215 weight.imbibe(conflict_marker);
216 }
217
218 simplify_conflict_markers(conflicts, &mut graph);
219
220 graph.retain_nodes(|graph, node| !graph[node].marker().is_false());
222
223 if matches!(resolution_strategy, ResolutionStrategy::Lowest) {
224 report_missing_lower_bounds(&graph, &mut diagnostics, &constraints, &overrides);
225 }
226
227 let output = Self {
228 graph,
229 requires_python,
230 fork_markers,
231 diagnostics,
232 requirements,
233 constraints,
234 overrides,
235 options,
236 };
237
238 if conflicts.is_empty() {
253 #[allow(unused_mut, reason = "Used in debug_assertions below")]
254 let mut conflicting = output.find_conflicting_distributions();
255 if !conflicting.is_empty() {
256 tracing::warn!(
257 "found {} conflicting distributions in resolution, \
258 please report this as a bug at \
259 https://github.com/astral-sh/uv/issues/new",
260 conflicting.len()
261 );
262 }
263 #[cfg(debug_assertions)]
272 if let Some(err) = conflicting.pop() {
273 return Err(ResolveError::ConflictingDistribution(err));
274 }
275 }
276 Ok(output)
277 }
278
279 fn add_edge(
280 graph: &mut Graph<ResolutionGraphNode, UniversalMarker>,
281 inverse: &mut FxHashMap<PackageRef<'_>, NodeIndex>,
282 root_index: NodeIndex,
283 edge: &ResolutionDependencyEdge,
284 marker: UniversalMarker,
285 ) {
286 let from_index = edge.from.as_ref().map_or(root_index, |from| {
287 inverse[&PackageRef {
288 package_name: from,
289 version: &edge.from_version,
290 url: edge.from_url.as_ref(),
291 index: edge.from_index.as_ref(),
292 extra: edge.from_extra.as_ref(),
293 group: edge.from_group.as_ref(),
294 }]
295 });
296 let to_index = inverse[&PackageRef {
297 package_name: &edge.to,
298 version: &edge.to_version,
299 url: edge.to_url.as_ref(),
300 index: edge.to_index.as_ref(),
301 extra: edge.to_extra.as_ref(),
302 group: edge.to_group.as_ref(),
303 }];
304
305 let edge_marker = {
306 let mut edge_marker = edge.universal_marker();
307 edge_marker.and(marker);
308 edge_marker
309 };
310
311 if let Some(weight) = graph
312 .find_edge(from_index, to_index)
313 .and_then(|edge| graph.edge_weight_mut(edge))
314 {
315 weight.or(edge_marker);
318 } else {
319 graph.update_edge(from_index, to_index, edge_marker);
320 }
321 }
322
323 fn add_version<'a>(
324 graph: &mut Graph<ResolutionGraphNode, UniversalMarker>,
325 inverse: &mut FxHashMap<PackageRef<'a>, NodeIndex>,
326 diagnostics: &mut Vec<ResolutionDiagnostic>,
327 preferences: &Preferences,
328 pins: &FilePins,
329 in_memory: &InMemoryIndex,
330 git: &GitResolver,
331 package: &'a ResolutionPackage,
332 version: &'a Version,
333 ) -> Result<(), ResolveError> {
334 let ResolutionPackage {
335 name,
336 extra,
337 dev: group,
338 url,
339 index,
340 } = &package;
341 let (dist, hashes, metadata) = Self::parse_dist(
343 name,
344 index.as_ref(),
345 url.as_ref(),
346 version,
347 pins,
348 diagnostics,
349 preferences,
350 in_memory,
351 git,
352 )?;
353
354 if let Some(metadata) = metadata.as_ref() {
355 if let Some(extra) = extra {
357 if !metadata.provides_extra.contains(extra) {
358 diagnostics.push(ResolutionDiagnostic::MissingExtra {
359 dist: dist.clone(),
360 extra: extra.clone(),
361 });
362 }
363 }
364
365 if let Some(dev) = group {
367 if !metadata.dependency_groups.contains_key(dev) {
368 diagnostics.push(ResolutionDiagnostic::MissingGroup {
369 dist: dist.clone(),
370 group: dev.clone(),
371 });
372 }
373 }
374 }
375
376 let node = graph.add_node(ResolutionGraphNode::Dist(AnnotatedDist {
378 dist,
379 name: name.clone(),
380 version: version.clone(),
381 extra: extra.clone(),
382 group: group.clone(),
383 hashes,
384 metadata,
385 marker: UniversalMarker::TRUE,
386 }));
387 inverse.insert(
388 PackageRef {
389 package_name: name,
390 version,
391 url: url.as_ref(),
392 index: index.as_ref(),
393 extra: extra.as_ref(),
394 group: group.as_ref(),
395 },
396 node,
397 );
398 Ok(())
399 }
400
401 fn parse_dist(
402 name: &PackageName,
403 index: Option<&IndexUrl>,
404 url: Option<&VerbatimParsedUrl>,
405 version: &Version,
406 pins: &FilePins,
407 diagnostics: &mut Vec<ResolutionDiagnostic>,
408 preferences: &Preferences,
409 in_memory: &InMemoryIndex,
410 git: &GitResolver,
411 ) -> Result<(ResolvedDist, HashDigests, Option<Metadata>), ResolveError> {
412 Ok(if let Some(url) = url {
413 let dist = Dist::from_url(name.clone(), url_to_precise(url.clone(), git))?;
416 let metadata_id = Dist::from_url(name.clone(), url.clone())?.distribution_id();
417
418 let hashes = Self::get_hashes(
420 name,
421 index,
422 Some(url),
423 &metadata_id,
424 version,
425 preferences,
426 in_memory,
427 );
428
429 let metadata = {
431 let response = in_memory
432 .distributions()
433 .get(&metadata_id)
434 .unwrap_or_else(|| {
435 panic!("Every URL distribution should have metadata: {metadata_id:?}")
436 });
437
438 let MetadataResponse::Found(archive) = &*response else {
439 panic!("Every URL distribution should have metadata: {metadata_id:?}")
440 };
441
442 archive.metadata.clone()
443 };
444
445 (
446 ResolvedDist::Installable {
447 dist: Arc::new(dist),
448 version: Some(version.clone()),
449 },
450 hashes,
451 Some(metadata),
452 )
453 } else {
454 let (dist, metadata_id) = pins
455 .dist_and_id(name, version)
456 .expect("Every package should be pinned");
457 let dist = dist.clone();
458 let hashes_id = dist.distribution_id();
459
460 match dist.yanked() {
462 None | Some(Yanked::Bool(false)) => {}
463 Some(Yanked::Bool(true)) => {
464 diagnostics.push(ResolutionDiagnostic::YankedVersion {
465 dist: dist.clone(),
466 reason: None,
467 });
468 }
469 Some(Yanked::Reason(reason)) => {
470 diagnostics.push(ResolutionDiagnostic::YankedVersion {
471 dist: dist.clone(),
472 reason: Some(reason.to_string()),
473 });
474 }
475 }
476
477 let hashes = Self::get_hashes(
479 name,
480 index,
481 None,
482 &hashes_id,
483 version,
484 preferences,
485 in_memory,
486 );
487
488 let metadata = {
490 in_memory
491 .distributions()
492 .get(metadata_id)
493 .and_then(|response| {
494 if let MetadataResponse::Found(archive) = &*response {
495 Some(archive.metadata.clone())
496 } else {
497 None
498 }
499 })
500 };
501
502 (dist, hashes, metadata)
503 })
504 }
505
506 fn get_hashes(
509 name: &PackageName,
510 index: Option<&IndexUrl>,
511 url: Option<&VerbatimParsedUrl>,
512 metadata_id: &DistributionId,
513 version: &Version,
514 preferences: &Preferences,
515 in_memory: &InMemoryIndex,
516 ) -> HashDigests {
517 if let Some(digests) = preferences.match_hashes(name, version) {
519 if !digests.is_empty() {
520 return HashDigests::from(digests);
521 }
522 }
523
524 if let Some(metadata_response) = in_memory.distributions().get(metadata_id) {
526 if let MetadataResponse::Found(ref archive) = *metadata_response {
527 let mut digests = archive.hashes.clone();
528 digests.sort_unstable();
529 if !digests.is_empty() {
530 return digests;
531 }
532 }
533 }
534
535 if url.is_none() {
537 let implicit_response = in_memory.implicit().get(name);
539 let mut explicit_response = None;
540
541 let hashes = implicit_response
543 .as_ref()
544 .and_then(|response| {
545 if let VersionsResponse::Found(version_maps) = &**response {
546 Some(version_maps)
547 } else {
548 None
549 }
550 })
551 .into_iter()
552 .flatten()
553 .filter(|version_map| version_map.index() == index)
554 .find_map(|version_map| version_map.hashes(version))
555 .or_else(|| {
556 explicit_response = index
558 .and_then(|index| in_memory.explicit().get(&(name.clone(), index.clone())));
559 explicit_response
560 .as_ref()
561 .and_then(|response| {
562 if let VersionsResponse::Found(version_maps) = &**response {
563 Some(version_maps)
564 } else {
565 None
566 }
567 })
568 .into_iter()
569 .flatten()
570 .filter(|version_map| version_map.index() == index)
571 .find_map(|version_map| version_map.hashes(version))
572 });
573
574 if let Some(hashes) = hashes {
575 let mut digests = HashDigests::from(hashes);
576 digests.sort_unstable();
577 if !digests.is_empty() {
578 return digests;
579 }
580 }
581 }
582
583 HashDigests::empty()
584 }
585
586 fn dists(&self) -> impl Iterator<Item = &AnnotatedDist> {
588 self.graph
589 .node_indices()
590 .filter_map(move |index| match &self.graph[index] {
591 ResolutionGraphNode::Root => None,
592 ResolutionGraphNode::Dist(dist) => Some(dist),
593 })
594 }
595
596 pub(crate) fn base_dists(&self) -> impl Iterator<Item = (NodeIndex, &AnnotatedDist)> {
598 self.graph
599 .node_indices()
600 .filter_map(move |node_index| match &self.graph[node_index] {
601 ResolutionGraphNode::Root => None,
602 ResolutionGraphNode::Dist(dist) => dist.is_base().then_some((node_index, dist)),
603 })
604 }
605
606 pub fn len(&self) -> usize {
608 self.base_dists().count()
609 }
610
611 pub fn is_empty(&self) -> bool {
613 self.base_dists().next().is_none()
614 }
615
616 pub fn contains(&self, name: &PackageName) -> bool {
618 self.dists().any(|dist| dist.name() == name)
619 }
620
621 pub fn diagnostics(&self) -> &[ResolutionDiagnostic] {
623 &self.diagnostics
624 }
625
626 pub fn marker_tree(
647 &self,
648 index: &InMemoryIndex,
649 marker_env: &MarkerEnvironment,
650 ) -> Result<MarkerTree, Box<ParsedUrlError>> {
651 use uv_pep508::{
652 CanonicalMarkerValueString, CanonicalMarkerValueVersion, MarkerExpression,
653 MarkerOperator, MarkerTree,
654 };
655
656 #[derive(Debug, Eq, Hash, PartialEq)]
662 enum MarkerParam {
663 Version(CanonicalMarkerValueVersion),
664 String(CanonicalMarkerValueString),
665 }
666
667 fn add_marker_params_from_tree(marker_tree: MarkerTree, set: &mut IndexSet<MarkerParam>) {
669 match marker_tree.kind() {
670 MarkerTreeKind::True => {}
671 MarkerTreeKind::False => {}
672 MarkerTreeKind::Version(marker) => {
673 set.insert(MarkerParam::Version(marker.key()));
674 for (_, tree) in marker.edges() {
675 add_marker_params_from_tree(tree, set);
676 }
677 }
678 MarkerTreeKind::String(marker) => {
679 set.insert(MarkerParam::String(marker.key()));
680 for (_, tree) in marker.children() {
681 add_marker_params_from_tree(tree, set);
682 }
683 }
684 MarkerTreeKind::In(marker) => {
685 set.insert(MarkerParam::String(marker.key()));
686 for (_, tree) in marker.children() {
687 add_marker_params_from_tree(tree, set);
688 }
689 }
690 MarkerTreeKind::Contains(marker) => {
691 set.insert(MarkerParam::String(marker.key()));
692 for (_, tree) in marker.children() {
693 add_marker_params_from_tree(tree, set);
694 }
695 }
696 MarkerTreeKind::Extra(marker) => {
702 for (_, tree) in marker.children() {
703 add_marker_params_from_tree(tree, set);
704 }
705 }
706 MarkerTreeKind::List(marker) => {
707 for (_, tree) in marker.children() {
708 add_marker_params_from_tree(tree, set);
709 }
710 }
711 }
712 }
713
714 let mut seen_marker_values = IndexSet::default();
715 for i in self.graph.node_indices() {
716 let ResolutionGraphNode::Dist(dist) = &self.graph[i] else {
717 continue;
718 };
719 let metadata_id = dist.dist.distribution_id();
720 let res = index
721 .distributions()
722 .get(&metadata_id)
723 .expect("every package in resolution graph has metadata");
724 let MetadataResponse::Found(archive, ..) = &*res else {
725 panic!("Every package should have metadata: {metadata_id:?}")
726 };
727 for req in self.constraints.apply(self.overrides.apply_for(
728 &dist.name,
729 &dist.version,
730 archive.metadata.requires_dist.iter(),
731 )) {
732 add_marker_params_from_tree(req.marker, &mut seen_marker_values);
733 }
734 }
735
736 for direct_req in self
738 .constraints
739 .apply(self.overrides.apply(self.requirements.iter()))
740 {
741 add_marker_params_from_tree(direct_req.marker, &mut seen_marker_values);
742 }
743
744 let mut conjunction = MarkerTree::TRUE;
747 for marker_param in seen_marker_values {
748 let expr = match marker_param {
749 MarkerParam::Version(value_version) => {
750 let from_env = marker_env.get_version(value_version);
751 MarkerExpression::Version {
752 key: value_version.into(),
753 specifier: VersionSpecifier::equals_version(from_env.clone()),
754 }
755 }
756 MarkerParam::String(value_string) => {
757 let from_env = marker_env.get_string(value_string);
758 MarkerExpression::String {
759 key: value_string.into(),
760 operator: MarkerOperator::Equal,
761 value: from_env.into(),
762 }
763 }
764 };
765 conjunction.and(MarkerTree::expression(expr));
766 }
767 Ok(conjunction)
768 }
769
770 fn find_conflicting_distributions(&self) -> Vec<ConflictingDistributionError> {
779 let mut name_to_markers: BTreeMap<&PackageName, Vec<(&Version, &UniversalMarker)>> =
780 BTreeMap::new();
781 for node in self.graph.node_weights() {
782 let annotated_dist = match node {
783 ResolutionGraphNode::Root => continue,
784 ResolutionGraphNode::Dist(annotated_dist) => annotated_dist,
785 };
786 name_to_markers
787 .entry(&annotated_dist.name)
788 .or_default()
789 .push((&annotated_dist.version, &annotated_dist.marker));
790 }
791 let mut dupes = vec![];
792 for (name, marker_trees) in name_to_markers {
793 for (i, (version1, marker1)) in marker_trees.iter().enumerate() {
794 for (version2, marker2) in &marker_trees[i + 1..] {
795 if version1 == version2 {
796 continue;
797 }
798 if !marker1.is_disjoint(**marker2) {
799 dupes.push(ConflictingDistributionError {
800 name: name.clone(),
801 version1: (*version1).clone(),
802 version2: (*version2).clone(),
803 marker1: **marker1,
804 marker2: **marker2,
805 });
806 }
807 }
808 }
809 }
810 dupes
811 }
812}
813
814#[derive(Debug)]
821pub struct ConflictingDistributionError {
822 name: PackageName,
823 version1: Version,
824 version2: Version,
825 marker1: UniversalMarker,
826 marker2: UniversalMarker,
827}
828
829impl std::error::Error for ConflictingDistributionError {}
830
831impl Display for ConflictingDistributionError {
832 fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
833 let Self {
834 ref name,
835 ref version1,
836 ref version2,
837 ref marker1,
838 ref marker2,
839 } = *self;
840 write!(
841 f,
842 "found conflicting versions for package `{name}`:
843 `{marker1:?}` (for version `{version1}`) is not disjoint with \
844 `{marker2:?}` (for version `{version2}`)",
845 )
846 }
847}
848
849impl From<ResolverOutput> for uv_distribution_types::Resolution {
861 fn from(output: ResolverOutput) -> Self {
862 let ResolverOutput {
863 graph,
864 diagnostics,
865 fork_markers,
866 ..
867 } = output;
868
869 assert!(
870 fork_markers.is_empty(),
871 "universal resolutions are not supported"
872 );
873
874 let mut transformed = Graph::with_capacity(graph.node_count(), graph.edge_count());
875 let mut inverse = FxHashMap::with_capacity_and_hasher(graph.node_count(), FxBuildHasher);
876
877 let root = transformed.add_node(Node::Root);
879
880 for index in graph.node_indices() {
882 let ResolutionGraphNode::Dist(dist) = &graph[index] else {
883 continue;
884 };
885 if dist.is_base() {
886 inverse.insert(
887 &dist.name,
888 transformed.add_node(Node::Dist {
889 dist: dist.dist.clone(),
890 hashes: dist.hashes.clone(),
891 install: true,
892 }),
893 );
894 }
895 }
896
897 for edge in graph.edge_indices() {
899 let (source, target) = graph.edge_endpoints(edge).unwrap();
900
901 match (&graph[source], &graph[target]) {
902 (ResolutionGraphNode::Root, ResolutionGraphNode::Dist(target_dist)) => {
903 let target = inverse[&target_dist.name()];
904 transformed.update_edge(root, target, Edge::Prod);
905 }
906 (
907 ResolutionGraphNode::Dist(source_dist),
908 ResolutionGraphNode::Dist(target_dist),
909 ) => {
910 let source = inverse[&source_dist.name()];
911 let target = inverse[&target_dist.name()];
912
913 let edge = if let Some(extra) = source_dist.extra.as_ref() {
914 Edge::Optional(extra.clone())
915 } else if let Some(group) = source_dist.group.as_ref() {
916 Edge::Dev(group.clone())
917 } else {
918 Edge::Prod
919 };
920
921 transformed.add_edge(source, target, edge);
922 }
923 _ => {
924 unreachable!("root should not contain incoming edges");
925 }
926 }
927 }
928
929 Self::new(transformed).with_diagnostics(diagnostics)
930 }
931}
932
933fn report_missing_lower_bounds(
935 graph: &Graph<ResolutionGraphNode, UniversalMarker>,
936 diagnostics: &mut Vec<ResolutionDiagnostic>,
937 constraints: &Constraints,
938 overrides: &Overrides,
939) {
940 for node_index in graph.node_indices() {
941 let ResolutionGraphNode::Dist(dist) = graph.node_weight(node_index).unwrap() else {
942 continue;
944 };
945 if !has_lower_bound(node_index, dist.name(), graph, constraints, overrides) {
946 diagnostics.push(ResolutionDiagnostic::MissingLowerBound {
947 package_name: dist.name().clone(),
948 });
949 }
950 }
951}
952
953fn has_lower_bound(
955 node_index: NodeIndex,
956 package_name: &PackageName,
957 graph: &Graph<ResolutionGraphNode, UniversalMarker>,
958 constraints: &Constraints,
959 overrides: &Overrides,
960) -> bool {
961 for neighbor_index in graph.neighbors_directed(node_index, Direction::Incoming) {
962 let neighbor_dist = match graph.node_weight(neighbor_index).unwrap() {
963 ResolutionGraphNode::Root => {
964 return true;
967 }
968 ResolutionGraphNode::Dist(neighbor_dist) => neighbor_dist,
969 };
970
971 if neighbor_dist.name() == package_name {
972 return true;
974 }
975
976 let Some(metadata) = neighbor_dist.metadata.as_ref() else {
977 return true;
979 };
980
981 for requirement in overrides
984 .apply_for(
985 neighbor_dist.name(),
986 &neighbor_dist.version,
987 metadata.requires_dist.iter(),
988 )
989 .chain(overrides.apply(metadata.dependency_groups.values().flatten()))
990 .chain(constraints.requirements().map(Cow::Borrowed))
992 {
993 if requirement.name != *package_name {
994 continue;
995 }
996 let Some(specifiers) = requirement.source.version_specifiers() else {
997 return true;
999 };
1000 if specifiers.iter().any(VersionSpecifier::has_lower_bound) {
1001 return true;
1002 }
1003 }
1004 }
1005 false
1006}