1use std::collections::HashMap;
2use std::fmt::Display;
3use std::path::{Path, PathBuf};
4
5use crate::configuration_file::ConfigurationFile;
6use crate::io::FromFileError;
7use crate::monorepo_manifest::{EnumeratePackageManifestsError, MonorepoManifest};
8
9#[derive(Debug)]
10#[non_exhaustive]
11pub struct LintError {
12 pub kind: LintErrorKind,
13}
14
15impl Display for LintError {
16 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17 match &self.kind {
18 LintErrorKind::UnknownDependency(dependency) => write!(
19 f,
20 "expected dependency `{}` to be used in at least one package",
21 dependency
22 ),
23 LintErrorKind::UnexpectedInternalDependencyVersion => write!(f, "lint errors detected"),
24 LintErrorKind::InvalidUtf8(path) => {
25 write!(f, "path cannot be expressed as UTF-8: {:?}", path)
26 }
27 _ => write!(f, "error linting dependency versions"),
28 }
29 }
30}
31
32impl std::error::Error for LintError {
33 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
34 match &self.kind {
35 LintErrorKind::EnumeratePackageManifests(err) => Some(err),
36 LintErrorKind::FromFile(err) => Some(err),
37 LintErrorKind::UnknownDependency(_) => None,
38 LintErrorKind::UnexpectedInternalDependencyVersion => None,
39 LintErrorKind::InvalidUtf8(_) => None,
40 }
41 }
42}
43
44#[derive(Debug)]
45pub enum LintErrorKind {
46 #[non_exhaustive]
47 FromFile(FromFileError),
48 #[non_exhaustive]
49 EnumeratePackageManifests(EnumeratePackageManifestsError),
50 #[non_exhaustive]
51 UnknownDependency(String),
52 #[non_exhaustive]
54 UnexpectedInternalDependencyVersion,
55 #[non_exhaustive]
56 InvalidUtf8(PathBuf),
57}
58
59impl From<FromFileError> for LintError {
60 fn from(err: FromFileError) -> Self {
61 Self {
62 kind: LintErrorKind::FromFile(err),
63 }
64 }
65}
66
67impl From<EnumeratePackageManifestsError> for LintError {
68 fn from(err: EnumeratePackageManifestsError) -> Self {
69 Self {
70 kind: LintErrorKind::EnumeratePackageManifests(err),
71 }
72 }
73}
74
75impl From<LintErrorKind> for LintError {
76 fn from(kind: LintErrorKind) -> Self {
77 Self { kind }
78 }
79}
80
81fn most_common_dependency_version(
82 package_manifests_by_dependency_version: &HashMap<String, Vec<String>>,
83) -> Option<String> {
84 package_manifests_by_dependency_version
85 .iter()
86 .map(|(dependency_version, package_manifests)| {
88 (dependency_version, package_manifests.len())
89 })
90 .max_by(|a, b| a.1.cmp(&b.1))
92 .map(|(k, _v)| k.to_owned())
93}
94
95pub fn lint_dependency_version<P, S>(root: P, dependencies: &[S]) -> Result<(), LintError>
96where
97 P: AsRef<Path>,
98 S: AsRef<str> + std::fmt::Display,
99{
100 let root = root.as_ref();
101
102 let lerna_manifest = MonorepoManifest::from_directory(root)?;
103 let package_manifest_by_package_name = lerna_manifest.package_manifests_by_package_name()?;
104
105 let mut is_exit_success = true;
106
107 for dependency in dependencies {
108 let package_manifests_by_dependency_version: HashMap<String, Vec<String>> =
109 package_manifest_by_package_name
110 .values()
111 .filter_map(|package_manifest| {
112 package_manifest
113 .get_dependency_version(dependency)
114 .map(|dependency_version| (package_manifest, dependency_version))
115 })
116 .try_fold(
117 HashMap::new(),
118 |mut accumulator,
119 (package_manifest, dependency_version)|
120 -> Result<HashMap<_, _>, LintError> {
121 let packages_using_this_dependency_version: &mut Vec<String> =
122 accumulator.entry(dependency_version).or_default();
123 packages_using_this_dependency_version.push(
124 package_manifest
125 .path()
126 .to_str()
127 .map(ToOwned::to_owned)
128 .ok_or_else(|| {
129 LintErrorKind::InvalidUtf8(package_manifest.path())
130 })?,
131 );
132 Ok(accumulator)
133 },
134 )?;
135
136 if package_manifests_by_dependency_version.keys().len() <= 1 {
137 return Ok(());
138 }
139
140 let expected_version_number =
141 most_common_dependency_version(&package_manifests_by_dependency_version)
142 .ok_or_else(|| LintErrorKind::UnknownDependency(dependency.to_string()))?;
143
144 println!("Linting versions of dependency \"{}\"", &dependency);
145
146 package_manifests_by_dependency_version
147 .into_iter()
148 .filter(|(dependency_version, _package_manifests)| {
150 !dependency_version.eq(&expected_version_number)
151 })
152 .for_each(|(dependency_version, package_manifests)| {
153 package_manifests.into_iter().for_each(|package_manifest| {
154 println!(
155 "\tIn {}, expected version {} but found version {}",
156 &package_manifest, &expected_version_number, dependency_version
157 );
158 });
159 });
160
161 is_exit_success = false;
162 }
163
164 if !is_exit_success {
165 return Err(LintError {
166 kind: LintErrorKind::UnexpectedInternalDependencyVersion,
167 });
168 }
169 Ok(())
170}