1use std::collections::HashMap;
2use std::fmt::Display;
3use std::path::Path;
4
5use crate::configuration_file::{ConfigurationFile, WriteError};
6use crate::io::FromFileError;
7use crate::monorepo_manifest::{EnumeratePackageManifestsError, MonorepoManifest};
8use crate::package_manifest::{DependencyGroup, PackageManifest};
9use crate::types::PackageName;
10use crate::unpinned_dependencies::{UnpinnedDependency, UnpinnedMonorepoDependencies};
11
12#[derive(Debug)]
13#[non_exhaustive]
14pub struct PinError {
15 pub kind: PinErrorKind,
16}
17
18impl Display for PinError {
19 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 match &self.kind {
21 PinErrorKind::NonStringVersionNumber {
22 package_name,
23 dependency_name,
24 } => {
25 write!(f, "unable to parse `{}` package.json: encountered non-string version for dependency `{}`", package_name, dependency_name)
26 }
27 _ => write!(f, "error pinning dependency versions"),
28 }
29 }
30}
31
32impl std::error::Error for PinError {
33 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
34 match &self.kind {
35 PinErrorKind::FromFile(err) => Some(err),
36 PinErrorKind::EnumeratePackageManifests(err) => Some(err),
37 PinErrorKind::Write(err) => Some(err),
38 PinErrorKind::NonStringVersionNumber {
39 package_name: _,
40 dependency_name: _,
41 } => None,
42 }
43 }
44}
45
46impl From<FromFileError> for PinError {
47 fn from(err: FromFileError) -> Self {
48 Self {
49 kind: PinErrorKind::FromFile(err),
50 }
51 }
52}
53
54impl From<EnumeratePackageManifestsError> for PinError {
55 fn from(err: EnumeratePackageManifestsError) -> Self {
56 Self {
57 kind: PinErrorKind::EnumeratePackageManifests(err),
58 }
59 }
60}
61
62impl From<WriteError> for PinError {
63 fn from(err: WriteError) -> Self {
64 Self {
65 kind: PinErrorKind::Write(err),
66 }
67 }
68}
69
70impl From<PinErrorKind> for PinError {
71 fn from(kind: PinErrorKind) -> Self {
72 Self { kind }
73 }
74}
75
76#[derive(Debug)]
77pub enum PinErrorKind {
78 #[non_exhaustive]
79 FromFile(FromFileError),
80 #[non_exhaustive]
81 EnumeratePackageManifests(EnumeratePackageManifestsError),
82 #[non_exhaustive]
83 Write(WriteError),
84 #[non_exhaustive]
85 NonStringVersionNumber {
86 package_name: PackageName,
87 dependency_name: String,
88 },
89}
90
91fn needs_modification<'a, 'b>(
92 dependency_name: &'a String,
93 dependency_version: &'a String,
94 package_version_by_package_name: &'b HashMap<PackageName, String>,
95) -> Option<&'b String> {
96 package_version_by_package_name
97 .get(&PackageName::from(dependency_name))
98 .and_then(|expected| match expected == dependency_version {
99 true => None,
100 false => Some(expected),
101 })
102}
103
104fn get_dependency_group_mut<'a>(
105 package_manifest: &'a mut PackageManifest,
106 dependency_group: &str,
107) -> Option<&'a mut serde_json::Map<String, serde_json::Value>> {
108 package_manifest
109 .contents
110 .extra_fields
111 .get_mut(dependency_group)
112 .and_then(serde_json::Value::as_object_mut)
113}
114
115pub fn modify<P>(root: P) -> Result<(), PinError>
116where
117 P: AsRef<Path>,
118{
119 let root = root.as_ref();
120 let lerna_manifest = MonorepoManifest::from_directory(root)?;
121
122 let package_manifest_by_package_name = lerna_manifest.package_manifests_by_package_name()?;
123
124 let package_version_by_package_name: HashMap<PackageName, String> =
125 package_manifest_by_package_name
126 .values()
127 .map(|package| {
128 (
129 package.contents.name.clone(),
130 package.contents.version.clone(),
131 )
132 })
133 .collect();
134
135 for (package_name, mut package_manifest) in package_manifest_by_package_name {
136 let mut dirty = false;
137 for dependency_group in DependencyGroup::VALUES {
138 let dependencies = get_dependency_group_mut(&mut package_manifest, dependency_group);
139 if dependencies.is_none() {
140 continue;
141 }
142 let dependencies = dependencies.unwrap();
143
144 dependencies
145 .into_iter()
146 .try_for_each(
147 |(dependency_name, dependency_version)| match &dependency_version {
148 serde_json::Value::String(dep_version) => {
149 if let Some(expected) = needs_modification(
150 dependency_name,
151 dep_version,
152 &package_version_by_package_name,
153 ) {
154 *dependency_version = expected.to_owned().into();
155 dirty = true;
156 }
157 Ok(())
158 }
159 _ => Err(PinErrorKind::NonStringVersionNumber {
160 package_name: package_name.clone(),
161 dependency_name: dependency_name.to_owned(),
162 }),
163 },
164 )?;
165 }
166
167 if dirty {
168 PackageManifest::write(root, package_manifest)?
169 }
170 }
171
172 Ok(())
173}
174
175#[derive(Debug)]
176#[non_exhaustive]
177pub struct PinLintError {
178 pub kind: PinLintErrorKind,
179}
180
181impl Display for PinLintError {
182 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183 match &self.kind {
184 PinLintErrorKind::NonStringVersionNumber {
185 package_name,
186 dependency_name,
187 } => {
188 write!(f, "unable to parse `{}` package.json: encountered non-string version for dependency `{}`", package_name, dependency_name)
189 }
190 PinLintErrorKind::UnpinnedDependencies(unpinned_dependencies) => {
191 writeln!(f, "found unpinned dependency versions\n")?;
192 write!(f, "{}", unpinned_dependencies)
193 }
194 _ => write!(f, "error linting internal dependency versions"),
195 }
196 }
197}
198
199impl std::error::Error for PinLintError {
200 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
201 match &self.kind {
202 PinLintErrorKind::FromFile(err) => Some(err),
203 PinLintErrorKind::EnumeratePackageManifests(err) => Some(err),
204 PinLintErrorKind::NonStringVersionNumber {
205 package_name: _,
206 dependency_name: _,
207 } => None,
208 PinLintErrorKind::UnpinnedDependencies(_) => None,
209 }
210 }
211}
212
213impl From<FromFileError> for PinLintError {
214 fn from(err: FromFileError) -> Self {
215 Self {
216 kind: PinLintErrorKind::FromFile(err),
217 }
218 }
219}
220
221impl From<EnumeratePackageManifestsError> for PinLintError {
222 fn from(err: EnumeratePackageManifestsError) -> Self {
223 Self {
224 kind: PinLintErrorKind::EnumeratePackageManifests(err),
225 }
226 }
227}
228
229impl From<PinLintErrorKind> for PinLintError {
230 fn from(kind: PinLintErrorKind) -> Self {
231 Self { kind }
232 }
233}
234
235#[derive(Debug)]
236pub enum PinLintErrorKind {
237 #[non_exhaustive]
238 FromFile(FromFileError),
239 #[non_exhaustive]
240 EnumeratePackageManifests(EnumeratePackageManifestsError),
241 #[non_exhaustive]
242 NonStringVersionNumber {
243 package_name: PackageName,
244 dependency_name: PackageName,
245 },
246 #[non_exhaustive]
247 UnpinnedDependencies(UnpinnedMonorepoDependencies),
248}
249
250fn get_unpinned_dependency(
251 dependency_name: PackageName,
252 dependency_version: &String,
253 package_version_by_package_name: &HashMap<PackageName, String>,
254) -> Option<UnpinnedDependency> {
255 package_version_by_package_name
256 .get(&dependency_name)
257 .and_then(|expected| match expected == dependency_version {
258 true => None,
259 false => Some(UnpinnedDependency {
260 name: dependency_name.to_owned(),
261 actual: dependency_version.to_owned(),
262 expected: expected.to_owned(),
263 }),
264 })
265}
266
267pub fn lint<P>(root: P) -> Result<(), PinLintError>
268where
269 P: AsRef<Path>,
270{
271 let root = root.as_ref();
272 let lerna_manifest = MonorepoManifest::from_directory(root)?;
273
274 let package_manifest_by_package_name = lerna_manifest.package_manifests_by_package_name()?;
275
276 let package_version_by_package_name: HashMap<PackageName, String> =
277 package_manifest_by_package_name
278 .values()
279 .map(|package| {
280 (
281 package.contents.name.clone(),
282 package.contents.version.clone(),
283 )
284 })
285 .collect();
286
287 let unpinned_dependencies: UnpinnedMonorepoDependencies = package_manifest_by_package_name
288 .into_iter()
289 .map(|(package_name, package_manifest)| {
290 let unpinned_deps = package_manifest
291 .dependencies_iter()
292 .filter_map(|(dependency_name, dependency_version)| -> Option<Result<UnpinnedDependency, PinLintErrorKind>> {
293 match dependency_version {
294 serde_json::Value::String(dep_version) => {
295 get_unpinned_dependency(
296 dependency_name,
297 dep_version,
298 &package_version_by_package_name,
299 ).map(Ok)
300 }
301 _ => Some(Err(PinLintErrorKind::NonStringVersionNumber {
302 package_name: package_name.clone(),
303 dependency_name: dependency_name.to_owned(),
304 })),
305 }
306 })
307 .collect::<Result<_, _>>()?;
308 Ok((package_manifest.path(), unpinned_deps))
309 })
310 .collect::<Result<_, PinLintErrorKind>>()?;
311
312 match unpinned_dependencies.is_empty() {
313 true => Ok(()),
314 false => Err(PinLintErrorKind::UnpinnedDependencies(
315 unpinned_dependencies,
316 ))?,
317 }
318}