Skip to content
This repository was archived by the owner on Sep 17, 2023. It is now read-only.

Commit c3810c0

Browse files
committed
feat: add subcommand to lint depenendency versions for consistent values
The `lint-dependency-version` subcommand takes a list of dependencies, and for each dependency will test that there is at most one version of that dependency in use throughout the target monorepo. If this condition is violated, the command will exit non-zero exit code. The plan is to use this in CI to enforce monorepo invariants for using only a single version of a given external dependency; for example, `typescript`. Ideally we separate out the library and binary code in this crate, and later we can write multiple binaries that use this library, one of which will invoke this new code and can function as a stand-alone Drone plugin. Thinking about it now, the coupling to Drone is not a goal of this crate but importing the library portion of this codebase from a different crate will be a prerequisite for the plugin.
1 parent 3f9dc82 commit c3810c0

File tree

4 files changed

+164
-8
lines changed

4 files changed

+164
-8
lines changed

src/lint.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
use std::collections::HashMap;
2+
use std::error::Error;
3+
4+
use crate::opts;
5+
6+
use crate::configuration_file::ConfigurationFile;
7+
use crate::monorepo_manifest::MonorepoManifest;
8+
9+
pub fn handle_subcommand(opts: opts::Lint) -> Result<(), Box<dyn Error>> {
10+
match opts.subcommand {
11+
opts::ClapLintSubCommand::DependencyVersion(args) => lint_dependency_version(&args),
12+
}
13+
}
14+
15+
fn most_common_dependency_version(
16+
package_manifests_by_dependency_version: &HashMap<String, Vec<String>>,
17+
) -> Option<String> {
18+
package_manifests_by_dependency_version
19+
.iter()
20+
// Map each dependecy version to its number of occurrences
21+
.map(|(dependency_version, package_manifests)| {
22+
(dependency_version, package_manifests.len())
23+
})
24+
// Take the max by value
25+
.max_by(|a, b| a.1.cmp(&b.1))
26+
.map(|(k, _v)| k.to_owned())
27+
}
28+
29+
fn lint_dependency_version(opts: &opts::DependencyVersion) -> Result<(), Box<dyn Error>> {
30+
let opts::DependencyVersion { root, dependencies } = opts;
31+
32+
let lerna_manifest = MonorepoManifest::from_directory(&root)?;
33+
let package_manifest_by_package_name = lerna_manifest.package_manifests_by_package_name()?;
34+
35+
let mut is_exit_success = true;
36+
37+
for dependency in dependencies {
38+
let package_manifests_by_dependency_version: HashMap<String, Vec<String>> =
39+
package_manifest_by_package_name
40+
.values()
41+
.filter_map(|package_manifest| {
42+
package_manifest
43+
.get_dependency_version(&dependency)
44+
.map(|dependency_version| (package_manifest, dependency_version))
45+
})
46+
.fold(
47+
HashMap::new(),
48+
|mut accumulator, (package_manifest, dependency_version)| {
49+
let packages_using_this_dependency_version =
50+
accumulator.entry(dependency_version).or_default();
51+
packages_using_this_dependency_version.push(
52+
package_manifest
53+
.path()
54+
.into_os_string()
55+
.into_string()
56+
.expect("Path not UTF-8 encoded"),
57+
);
58+
accumulator
59+
},
60+
);
61+
62+
if package_manifests_by_dependency_version.keys().len() <= 1 {
63+
return Ok(());
64+
}
65+
66+
let expected_version_number =
67+
most_common_dependency_version(&package_manifests_by_dependency_version)
68+
.expect("Expected dependency to be used in at least one package");
69+
70+
println!("Linting versions of dependency \"{}\"", &dependency);
71+
72+
package_manifests_by_dependency_version
73+
.into_iter()
74+
// filter out the packages using the expected dependency version
75+
.filter(|(dependency_version, _package_manifests)| {
76+
!dependency_version.eq(&expected_version_number)
77+
})
78+
.for_each(|(dependency_version, package_manifests)| {
79+
package_manifests.into_iter().for_each(|package_manifest| {
80+
println!(
81+
"\tIn {}, expected version {} but found version {}",
82+
&package_manifest, &expected_version_number, dependency_version
83+
);
84+
});
85+
});
86+
87+
is_exit_success = false;
88+
}
89+
90+
if is_exit_success {
91+
return Ok(());
92+
} else {
93+
return Err("Found unexpected dependency versions".into());
94+
}
95+
}

src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
mod configuration_file;
44
mod io;
55
mod link;
6+
mod lint;
67
mod make_depend;
78
mod monorepo_manifest;
89
mod opts;
@@ -23,5 +24,6 @@ fn main() -> Result<(), Box<dyn Error>> {
2324
opts::ClapSubCommand::Pin(args) => pin::pin_version_numbers_in_internal_packages(args),
2425
opts::ClapSubCommand::MakeDepend(args) => make_depend::make_dependency_makefile(args),
2526
opts::ClapSubCommand::Query(args) => query::handle_subcommand(args),
27+
opts::ClapSubCommand::Lint(args) => lint::handle_subcommand(args),
2628
}
2729
}

src/opts.rs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ pub enum ClapSubCommand {
1919
MakeDepend(MakeDepend),
2020
#[clap(about = "Query properties of the current monorepo state")]
2121
Query(Query),
22+
#[clap(about = "Lint internal packages for consistent use of external dependency versions")]
23+
Lint(Lint),
2224
}
2325

2426
#[derive(Parser)]
@@ -59,7 +61,6 @@ pub struct MakeDepend {
5961

6062
#[derive(Parser)]
6163
pub struct Query {
62-
/// internal-dependencies
6364
#[clap(subcommand)]
6465
pub subcommand: ClapQuerySubCommand,
6566
}
@@ -72,6 +73,12 @@ pub enum ClapQuerySubCommand {
7273
InternalDependencies(InternalDependencies),
7374
}
7475

76+
#[derive(ArgEnum, Clone)]
77+
pub enum InternalDependenciesFormat {
78+
Name,
79+
Path,
80+
}
81+
7582
#[derive(Parser)]
7683
pub struct InternalDependencies {
7784
/// Path to monorepo root
@@ -82,8 +89,24 @@ pub struct InternalDependencies {
8289
pub format: InternalDependenciesFormat,
8390
}
8491

85-
#[derive(ArgEnum, Clone)]
86-
pub enum InternalDependenciesFormat {
87-
Name,
88-
Path,
92+
#[derive(Parser)]
93+
pub struct Lint {
94+
#[clap(subcommand)]
95+
pub subcommand: ClapLintSubCommand,
96+
}
97+
98+
#[derive(Parser)]
99+
pub enum ClapLintSubCommand {
100+
#[clap(about = "Lint the used versions of an external dependency for consistency")]
101+
DependencyVersion(DependencyVersion),
102+
}
103+
104+
#[derive(Parser)]
105+
pub struct DependencyVersion {
106+
/// Path to monorepo root
107+
#[clap(short, long, default_value = ".")]
108+
pub root: PathBuf,
109+
/// External dependency to lint for consistency of version used
110+
#[clap(short, long = "dependency")]
111+
pub dependencies: Vec<String>,
89112
}

src/package_manifest.rs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,39 @@ impl AsRef<PackageManifest> for PackageManifest {
8888
}
8989

9090
impl PackageManifest {
91+
// Get the dependency
92+
pub fn get_dependency_version<S>(&self, dependency: S) -> Option<String>
93+
where
94+
S: AsRef<str>,
95+
{
96+
static DEPENDENCY_GROUPS: &[&str] = &[
97+
"dependencies",
98+
"devDependencies",
99+
"optionalDependencies",
100+
"peerDependencies",
101+
];
102+
103+
DEPENDENCY_GROUPS
104+
.iter()
105+
// only iterate over the objects corresponding to each dependency group
106+
.filter_map(|dependency_group| {
107+
self.contents
108+
.extra_fields
109+
.get(dependency_group)?
110+
.as_object()
111+
})
112+
// get the target dependency version, if exists
113+
.filter_map(|dependency_group_value| {
114+
dependency_group_value
115+
.get(dependency.as_ref())
116+
// DISCUSS(Grayson): neither clone nor cloned work here, only to_owned
117+
// How do I resolve this with https://github.com/typescript-tools/rust-implementation/pull/141#discussion_r845294514 ?
118+
.and_then(|version_value| version_value.as_str().map(|a| a.to_owned()))
119+
})
120+
.take(1)
121+
.next()
122+
}
123+
91124
pub fn internal_dependencies_iter<'a, T>(
92125
&'a self,
93126
package_manifests_by_package_name: &'a HashMap<String, &'a T>,
@@ -106,7 +139,10 @@ impl PackageManifest {
106139
.iter()
107140
// only iterate over the objects corresponding to each dependency group
108141
.filter_map(|dependency_group| {
109-
self.contents.extra_fields.get(dependency_group)?.as_object()
142+
self.contents
143+
.extra_fields
144+
.get(dependency_group)?
145+
.as_object()
110146
})
111147
// get all dependency names from all groups
112148
.flat_map(|dependency_group_value| dependency_group_value.keys())
@@ -132,8 +168,8 @@ impl PackageManifest {
132168
while let Some(current_manifest) = to_visit_package_manifests.pop_front() {
133169
seen_package_names.insert(&current_manifest.contents.name);
134170

135-
for dependency in current_manifest
136-
.internal_dependencies_iter(package_manifest_by_package_name)
171+
for dependency in
172+
current_manifest.internal_dependencies_iter(package_manifest_by_package_name)
137173
{
138174
internal_dependencies.insert(dependency.contents.name.to_owned());
139175
if !seen_package_names.contains(&dependency.contents.name) {

0 commit comments

Comments
 (0)