typescript_tools/
lint.rs

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    // REFACTOR: move display logic into this type
53    #[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 each dependecy version to its number of occurrences
87        .map(|(dependency_version, package_manifests)| {
88            (dependency_version, package_manifests.len())
89        })
90        // Take the max by value
91        .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 out the packages using the expected dependency version
149            .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}