use crate::crate_name::CrateName;
use crate::error::{InstallerError, Result};
use log::debug;
pub const LINT_CRATES: &[&str] = &[
"bumpy_road_function",
"conditional_max_n_branches",
"function_attrs_follow_docs",
"module_max_lines",
"module_must_have_inner_docs",
"no_expect_outside_tests",
"test_must_not_have_example",
"no_std_fs_operations",
"no_unwrap_or_else_panic",
];
pub const EXPERIMENTAL_LINT_CRATES: &[&str] = &["rstest_helper_should_be_fixture"];
pub const SUITE_CRATE: &str = "whitaker_suite";
#[derive(Debug, Clone, Default)]
pub struct CrateResolutionOptions {
pub individual_lints: bool,
pub experimental: bool,
}
#[must_use]
pub fn resolve_crates(
specific_lints: &[CrateName],
options: &CrateResolutionOptions,
) -> Vec<CrateName> {
if !specific_lints.is_empty() {
debug!(
target: "whitaker_installer::resolution",
"using explicit lint crate selection: {:?}",
specific_lints
);
return specific_lints.to_vec();
}
if options.individual_lints {
let mut crates: Vec<CrateName> = LINT_CRATES.iter().map(|&c| CrateName::from(c)).collect();
if options.experimental {
debug!(
target: "whitaker_installer::resolution",
"including experimental lint crates because --experimental is enabled"
);
crates.extend(EXPERIMENTAL_LINT_CRATES.iter().map(|&c| CrateName::from(c)));
} else {
debug!(
target: "whitaker_installer::resolution",
"excluding experimental lint crates because --experimental is disabled"
);
}
return crates;
}
debug!(
target: "whitaker_installer::resolution",
"using suite crate selection; experimental feature handling is deferred to build config"
);
vec![CrateName::from(SUITE_CRATE)]
}
#[must_use]
pub fn is_known_crate(name: &CrateName) -> bool {
let s = name.as_str();
LINT_CRATES.contains(&s) || EXPERIMENTAL_LINT_CRATES.contains(&s) || s == SUITE_CRATE
}
#[must_use]
pub fn is_experimental_crate(name: &CrateName) -> bool {
EXPERIMENTAL_LINT_CRATES.contains(&name.as_str())
}
pub fn validate_crate_names(names: &[CrateName], options: &CrateResolutionOptions) -> Result<()> {
for name in names {
if !is_known_crate(name) {
debug!(
target: "whitaker_installer::resolution",
"rejecting unknown lint crate `{}`",
name.as_str()
);
return Err(InstallerError::LintCrateNotFound { name: name.clone() });
}
if is_experimental_crate(name) && !options.experimental {
debug!(
target: "whitaker_installer::resolution",
"rejecting experimental lint crate `{}` because --experimental is disabled",
name.as_str()
);
return Err(InstallerError::ExperimentalLintRequiresFlag { name: name.clone() });
}
debug!(
target: "whitaker_installer::resolution",
"accepted lint crate `{}` for resolution",
name.as_str()
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
struct ResolveCratesCase {
individual_lints: bool,
experimental: bool,
expect_lint: bool,
expect_suite: bool,
expect_bumpy_road: bool,
expect_experimental_lint: bool,
}
#[rstest]
#[case::default_suite_only(ResolveCratesCase { individual_lints: false, experimental: false, expect_lint: false, expect_suite: true, expect_bumpy_road: false, expect_experimental_lint: false })]
#[case::individual_lints(ResolveCratesCase { individual_lints: true, experimental: false, expect_lint: true, expect_suite: false, expect_bumpy_road: true, expect_experimental_lint: false })]
#[case::individual_with_experimental(ResolveCratesCase { individual_lints: true, experimental: true, expect_lint: true, expect_suite: false, expect_bumpy_road: true, expect_experimental_lint: true })]
#[case::suite_with_experimental(ResolveCratesCase { individual_lints: false, experimental: true, expect_lint: false, expect_suite: true, expect_bumpy_road: false, expect_experimental_lint: false })]
fn resolve_crates_variants(#[case] case: ResolveCratesCase) {
let options = CrateResolutionOptions {
individual_lints: case.individual_lints,
experimental: case.experimental,
};
let crates = resolve_crates(&[], &options);
assert_eq!(
crates.contains(&CrateName::from("module_max_lines")),
case.expect_lint,
"lint crate inclusion mismatch"
);
assert_eq!(
crates.contains(&CrateName::from(SUITE_CRATE)),
case.expect_suite,
"suite crate inclusion mismatch"
);
assert_eq!(
crates.contains(&CrateName::from("bumpy_road_function")),
case.expect_bumpy_road,
"bumpy_road_function inclusion mismatch"
);
assert_eq!(
crates.contains(&CrateName::from("rstest_helper_should_be_fixture")),
case.expect_experimental_lint,
"rstest_helper_should_be_fixture inclusion mismatch"
);
}
#[test]
fn resolve_crates_specific_lints() {
let specific = vec![CrateName::from("module_max_lines")];
let crates = resolve_crates(&specific, &CrateResolutionOptions::default());
assert_eq!(crates, vec![CrateName::from("module_max_lines")]);
}
#[rstest]
#[case::valid(&["module_max_lines", "whitaker_suite"], false, true)]
#[case::bumpy_road_function(&["bumpy_road_function"], false, true)]
#[case::experimental_with_flag(&["rstest_helper_should_be_fixture"], true, true)]
#[case::experimental_without_flag(&["rstest_helper_should_be_fixture"], false, false)]
#[case::unknown(&["nonexistent_lint"], false, false)]
fn validate_crate_names_variants(
#[case] names: &[&str],
#[case] experimental: bool,
#[case] expect_ok: bool,
) {
let crate_names: Vec<CrateName> = names.iter().map(|&s| CrateName::from(s)).collect();
let options = CrateResolutionOptions {
experimental,
..CrateResolutionOptions::default()
};
let res = validate_crate_names(&crate_names, &options);
if expect_ok {
assert!(res.is_ok());
} else {
let err = res.expect_err("expected validation failure");
assert!(
matches!(
&err,
InstallerError::LintCrateNotFound { name }
| InstallerError::ExperimentalLintRequiresFlag { name }
if *name == crate_names[0]
),
"unexpected error: {err:?}"
);
}
}
#[test]
fn validate_crate_names_returns_experimental_lint_requires_flag_error() {
let name = CrateName::from("rstest_helper_should_be_fixture");
let options = CrateResolutionOptions {
experimental: false,
..CrateResolutionOptions::default()
};
let error = validate_crate_names(std::slice::from_ref(&name), &options)
.expect_err("experimental lint should require explicit opt-in");
assert!(
matches!(&error, InstallerError::ExperimentalLintRequiresFlag { name: error_name } if *error_name == name),
"expected ExperimentalLintRequiresFlag, got {error:?}"
);
}
}