1use std::fmt;
2use std::str::FromStr;
3
4use rustc_hash::{FxHashMap, FxHashSet};
5use serde::de::{Error, MapAccess, Visitor, value::MapAccessDeserializer};
6use serde::{Deserialize, Deserializer};
7
8use uv_distribution_types::ResolutionRecorder;
9use uv_normalize::PackageName;
10use uv_pep440::Version;
11
12use crate::Overrides;
13
14#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
16#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
17#[serde(rename_all = "kebab-case", deny_unknown_fields)]
18pub struct PackageExclusion {
19 package: PackageExclusionTarget,
20 dependencies: Box<[PackageName]>,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
25#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
26#[serde(rename_all = "kebab-case", deny_unknown_fields)]
27pub struct PackageExclusionTarget {
28 name: PackageName,
29 #[cfg_attr(
30 feature = "schemars",
31 schemars(
32 with = "Option<String>",
33 description = "PEP 440-style package version, e.g., `1.2.3`"
34 )
35 )]
36 version: Option<Version>,
37}
38
39impl PackageExclusion {
40 pub fn package(&self) -> &PackageName {
42 &self.package.name
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
48#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema), schemars(untagged))]
49#[serde(untagged)]
50pub enum ExcludeDependency {
51 Package(PackageExclusion),
52 Dependency(PackageName),
53}
54
55struct ExcludeDependencyVisitor;
56
57impl<'de> Visitor<'de> for ExcludeDependencyVisitor {
58 type Value = ExcludeDependency;
59
60 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
61 formatter.write_str("a string or map")
62 }
63
64 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
65 where
66 E: Error,
67 {
68 PackageName::from_str(value)
69 .map(ExcludeDependency::Dependency)
70 .map_err(E::custom)
71 }
72
73 fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>
74 where
75 M: MapAccess<'de>,
76 {
77 PackageExclusion::deserialize(MapAccessDeserializer::new(map))
78 .map(ExcludeDependency::Package)
79 }
80}
81
82impl<'de> Deserialize<'de> for ExcludeDependency {
83 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
84 where
85 D: Deserializer<'de>,
86 {
87 deserializer.deserialize_any(ExcludeDependencyVisitor)
88 }
89}
90
91#[derive(Debug, Default, Clone)]
93pub struct Excludes {
94 recorder: Option<ResolutionRecorder>,
95 global: FxHashSet<PackageName>,
96 scoped: FxHashMap<PackageName, Vec<ScopedExclusions>>,
97}
98
99#[derive(Debug, Clone)]
100struct ScopedExclusions {
101 version: Option<Version>,
102 excludes: FxHashSet<PackageName>,
103}
104
105impl Excludes {
106 #[must_use]
108 pub(crate) fn with_recorder(mut self, recorder: Option<ResolutionRecorder>) -> Self {
109 self.recorder = recorder;
110 self
111 }
112
113 pub fn from_entries(entries: impl IntoIterator<Item = ExcludeDependency>) -> Self {
115 let mut excludes = Self::default();
116 for entry in entries {
117 match entry {
118 ExcludeDependency::Dependency(dependency) => {
119 excludes.global.insert(dependency);
120 }
121 ExcludeDependency::Package(package) => {
122 let packages = excludes.scoped.entry(package.package.name).or_default();
123 if let Some(entry) = packages
124 .iter_mut()
125 .find(|entry| entry.version == package.package.version)
126 {
127 entry.excludes.extend(package.dependencies);
128 } else {
129 packages.push(ScopedExclusions {
130 version: package.package.version,
131 excludes: package.dependencies.into_iter().collect(),
132 });
133 }
134 }
135 }
136 }
137 excludes
138 }
139
140 pub(crate) fn into_entries(self) -> Vec<ExcludeDependency> {
144 let mut entries = self
145 .global
146 .into_iter()
147 .map(ExcludeDependency::Dependency)
148 .collect::<Vec<_>>();
149 for (name, packages) in self.scoped {
150 for package in packages {
151 let mut dependencies = package.excludes.into_iter().collect::<Vec<_>>();
152 dependencies.sort();
153 entries.push(ExcludeDependency::Package(PackageExclusion {
154 package: PackageExclusionTarget {
155 name: name.clone(),
156 version: package.version,
157 },
158 dependencies: dependencies.into_boxed_slice(),
159 }));
160 }
161 }
162 entries.sort();
163 entries
164 }
165
166 pub(crate) fn contains(&self, name: &PackageName) -> bool {
168 if let Some(recorder) = &self.recorder {
169 recorder.exclusion(name);
170 }
171 self.global.contains(name)
172 }
173
174 pub(crate) fn has_scoped_package(&self, package: &PackageName) -> bool {
176 self.scoped.contains_key(package)
177 }
178
179 pub(crate) fn contains_for(
181 &self,
182 package: &PackageName,
183 version: &Version,
184 dependency: &PackageName,
185 ) -> bool {
186 self.contains_for_package(Some((package, version)), dependency)
187 }
188
189 pub(crate) fn contains_for_scope(
194 &self,
195 overrides: &Overrides,
196 package: &PackageName,
197 version: Option<&Version>,
198 dependency: &PackageName,
199 ) -> bool {
200 if let Some(version) = version {
201 return self.contains_for(package, version, dependency);
202 }
203 if self.contains(dependency) {
204 return true;
205 }
206
207 let Some(entries) = self.scoped.get(package) else {
208 return false;
209 };
210 entries
211 .iter()
212 .find(|entry| entry.version.is_none())
213 .is_some_and(|entry| entry.excludes.contains(dependency))
214 && entries
215 .iter()
216 .filter(|entry| {
217 entry
218 .version
219 .as_ref()
220 .is_some_and(|version| !overrides.has_exact_scope(package, version))
221 })
222 .all(|entry| entry.excludes.contains(dependency))
223 }
224
225 pub(crate) fn contains_for_package(
227 &self,
228 package: Option<(&PackageName, &Version)>,
229 dependency: &PackageName,
230 ) -> bool {
231 self.contains(dependency)
232 || package.is_some_and(|(package, version)| {
233 if let Some(recorder) = &self.recorder {
234 recorder.scoped_exclusion(package);
235 }
236 self.scoped.get(package).is_some_and(|entries| {
237 entries
238 .iter()
239 .find(|entry| entry.version.as_ref() == Some(version))
240 .or_else(|| entries.iter().find(|entry| entry.version.is_none()))
241 .is_some_and(|entry| entry.excludes.contains(dependency))
242 })
243 })
244 }
245}
246
247impl FromIterator<PackageName> for Excludes {
248 fn from_iter<I: IntoIterator<Item = PackageName>>(iter: I) -> Self {
249 Self::from_entries(iter.into_iter().map(ExcludeDependency::Dependency))
250 }
251}