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