use std::path::{Path, PathBuf};
use std::process::ExitCode;
use fallow_config::{FallowConfig, OutputFormat, ProductionAnalysis, ResolvedConfig};
#[derive(Clone, PartialEq, Eq, clap::ValueEnum)]
pub enum AnalysisKind {
#[value(alias = "check")]
DeadCode,
Dupes,
Health,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
pub enum GroupBy {
#[value(alias = "team", alias = "codeowner")]
Owner,
Directory,
#[value(alias = "workspace", alias = "pkg")]
Package,
#[value(alias = "gl-section")]
Section,
}
pub fn build_ownership_resolver(
group_by: Option<GroupBy>,
root: &Path,
codeowners_path: Option<&str>,
output: OutputFormat,
) -> Result<Option<crate::report::OwnershipResolver>, ExitCode> {
let Some(mode) = group_by else {
return Ok(None);
};
match mode {
GroupBy::Owner => match crate::codeowners::CodeOwners::load(root, codeowners_path) {
Ok(co) => Ok(Some(crate::report::OwnershipResolver::Owner(co))),
Err(e) => Err(crate::error::emit_error(&e, 2, output)),
},
GroupBy::Section => match crate::codeowners::CodeOwners::load(root, codeowners_path) {
Ok(co) => {
if co.has_sections() {
Ok(Some(crate::report::OwnershipResolver::Section(co)))
} else {
Err(crate::error::emit_error(
"--group-by section requires a GitLab-style CODEOWNERS file \
with `[Section]` headers. This CODEOWNERS has no sections; \
use --group-by owner instead.",
2,
output,
))
}
}
Err(e) => Err(crate::error::emit_error(&e, 2, output)),
},
GroupBy::Directory => Ok(Some(crate::report::OwnershipResolver::Directory)),
GroupBy::Package => {
let workspaces = fallow_config::discover_workspaces(root);
if workspaces.is_empty() {
Err(crate::error::emit_error(
"--group-by package requires a monorepo with workspace packages \
(package.json workspaces, pnpm-workspace.yaml, or tsconfig references). \
For single-package projects try --group-by directory instead.",
2,
output,
))
} else {
Ok(Some(crate::report::OwnershipResolver::Package(
crate::report::grouping::PackageResolver::new(root, &workspaces),
)))
}
}
}
}
fn log_config_loaded(path: &Path, output: OutputFormat, quiet: bool) {
if quiet || !matches!(output, OutputFormat::Human) {
return;
}
eprintln!("loaded config: {}", path.display());
}
#[expect(clippy::ref_option, reason = "&Option matches clap's field type")]
pub fn load_config(
root: &Path,
config_path: &Option<PathBuf>,
output: OutputFormat,
no_cache: bool,
threads: usize,
production: bool,
quiet: bool,
) -> Result<ResolvedConfig, ExitCode> {
load_config_for_analysis(
root,
config_path,
output,
no_cache,
threads,
production.then_some(true),
quiet,
ProductionAnalysis::DeadCode,
)
}
#[expect(clippy::ref_option, reason = "&Option matches clap's field type")]
#[expect(
clippy::too_many_arguments,
reason = "central config loader mirrors CLI dispatch options"
)]
pub fn load_config_for_analysis(
root: &Path,
config_path: &Option<PathBuf>,
output: OutputFormat,
no_cache: bool,
threads: usize,
production_override: Option<bool>,
quiet: bool,
analysis: ProductionAnalysis,
) -> Result<ResolvedConfig, ExitCode> {
let user_config = if let Some(path) = config_path {
match FallowConfig::load(path) {
Ok(c) => {
log_config_loaded(path, output, quiet);
Some(c)
}
Err(e) => {
let msg = format!("failed to load config '{}': {e}", path.display());
return Err(crate::error::emit_error(&msg, 2, output));
}
}
} else {
match FallowConfig::find_and_load(root) {
Ok(Some((config, found_path))) => {
log_config_loaded(&found_path, output, quiet);
Some(config)
}
Ok(None) => None,
Err(e) => {
return Err(crate::error::emit_error(&e, 2, output));
}
}
};
Ok(match user_config {
Some(mut config) => {
let production =
production_override.unwrap_or_else(|| config.production.for_analysis(analysis));
config.production = production.into();
config.resolve(root.to_path_buf(), output, threads, no_cache, quiet)
}
None => FallowConfig {
production: production_override.unwrap_or(false).into(),
..FallowConfig::default()
}
.resolve(root.to_path_buf(), output, threads, no_cache, quiet),
})
}