Skip to main content

uv_configuration/
excludes.rs

1use std::str::FromStr;
2
3use rustc_hash::{FxHashMap, FxHashSet};
4use serde::de::Error;
5
6use uv_normalize::PackageName;
7use uv_pep440::Version;
8
9use crate::Overrides;
10
11/// A set of exclusions that applies to the dependencies of a specific package version.
12#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
13#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
14#[serde(rename_all = "kebab-case", deny_unknown_fields)]
15pub struct PackageExclusion {
16    pub package: PackageExclusionTarget,
17    pub dependencies: Box<[PackageName]>,
18}
19
20/// The package and optional version selected by a [`PackageExclusion`].
21#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
22#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
23#[serde(rename_all = "kebab-case", deny_unknown_fields)]
24pub struct PackageExclusionTarget {
25    pub name: PackageName,
26    #[cfg_attr(
27        feature = "schemars",
28        schemars(
29            with = "Option<String>",
30            description = "PEP 440-style package version, e.g., `1.2.3`"
31        )
32    )]
33    pub version: Option<Version>,
34}
35
36/// An exclusion, either global or scoped to a specific package version.
37#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
38#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema), schemars(untagged))]
39#[serde(untagged)]
40pub enum ExcludeDependency {
41    Package(PackageExclusion),
42    Dependency(PackageName),
43}
44
45impl<'de> serde::Deserialize<'de> for ExcludeDependency {
46    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
47    where
48        D: serde::Deserializer<'de>,
49    {
50        serde_untagged::UntaggedEnumVisitor::new()
51            .string(|string| {
52                PackageName::from_str(string)
53                    .map(Self::Dependency)
54                    .map_err(Error::custom)
55            })
56            .map(|map| map.deserialize().map(Self::Package))
57            .deserialize(deserializer)
58    }
59}
60
61/// A set of packages to exclude from resolution.
62#[derive(Debug, Default, Clone)]
63pub struct Excludes {
64    global: FxHashSet<PackageName>,
65    scoped: FxHashMap<PackageName, Vec<ScopedExclusions>>,
66}
67
68#[derive(Debug, Clone)]
69struct ScopedExclusions {
70    version: Option<Version>,
71    excludes: FxHashSet<PackageName>,
72}
73
74impl Excludes {
75    /// Create an indexed set of exclusions.
76    pub fn from_entries(entries: impl IntoIterator<Item = ExcludeDependency>) -> Self {
77        let mut excludes = Self::default();
78        for entry in entries {
79            match entry {
80                ExcludeDependency::Dependency(dependency) => {
81                    excludes.global.insert(dependency);
82                }
83                ExcludeDependency::Package(package) => {
84                    let packages = excludes.scoped.entry(package.package.name).or_default();
85                    if let Some(entry) = packages
86                        .iter_mut()
87                        .find(|entry| entry.version == package.package.version)
88                    {
89                        entry.excludes.extend(package.dependencies);
90                    } else {
91                        packages.push(ScopedExclusions {
92                            version: package.package.version,
93                            excludes: package.dependencies.into_iter().collect(),
94                        });
95                    }
96                }
97            }
98        }
99        excludes
100    }
101
102    /// Check if a package is excluded.
103    pub fn contains(&self, name: &PackageName) -> bool {
104        self.global.contains(name)
105    }
106
107    /// Check if a dependency is excluded from a specific package version.
108    pub fn contains_for(
109        &self,
110        package: &PackageName,
111        version: &Version,
112        dependency: &PackageName,
113    ) -> bool {
114        self.contains_for_package(Some((package, version)), dependency)
115    }
116
117    /// Check if a dependency is always excluded from a package scope.
118    ///
119    /// A versionless scope remains eligible if any exact-version exclusion allows the dependency
120    /// at a version where the override is not shadowed by an exact override scope.
121    pub fn contains_for_scope(
122        &self,
123        overrides: &Overrides,
124        package: &PackageName,
125        version: Option<&Version>,
126        dependency: &PackageName,
127    ) -> bool {
128        if let Some(version) = version {
129            return self.contains_for(package, version, dependency);
130        }
131        if self.contains(dependency) {
132            return true;
133        }
134
135        let Some(entries) = self.scoped.get(package) else {
136            return false;
137        };
138        entries
139            .iter()
140            .find(|entry| entry.version.is_none())
141            .is_some_and(|entry| entry.excludes.contains(dependency))
142            && entries
143                .iter()
144                .filter(|entry| {
145                    entry
146                        .version
147                        .as_ref()
148                        .is_some_and(|version| !overrides.has_exact_scope(package, version))
149                })
150                .all(|entry| entry.excludes.contains(dependency))
151    }
152
153    /// Check if a dependency is excluded with optional package-version context.
154    pub fn contains_for_package(
155        &self,
156        package: Option<(&PackageName, &Version)>,
157        dependency: &PackageName,
158    ) -> bool {
159        self.contains(dependency)
160            || package.is_some_and(|(package, version)| {
161                self.scoped.get(package).is_some_and(|entries| {
162                    entries
163                        .iter()
164                        .find(|entry| entry.version.as_ref() == Some(version))
165                        .or_else(|| entries.iter().find(|entry| entry.version.is_none()))
166                        .is_some_and(|entry| entry.excludes.contains(dependency))
167                })
168            })
169    }
170}
171
172impl FromIterator<PackageName> for Excludes {
173    fn from_iter<I: IntoIterator<Item = PackageName>>(iter: I) -> Self {
174        Self::from_entries(iter.into_iter().map(ExcludeDependency::Dependency))
175    }
176}