1use std::path::{Path, PathBuf};
2use std::process::ExitCode;
3
4use fallow_config::{FallowConfig, OutputFormat, ProductionAnalysis, ResolvedConfig};
5
6#[derive(Clone, PartialEq, Eq, clap::ValueEnum)]
8pub enum AnalysisKind {
9 #[value(alias = "check")]
10 DeadCode,
11 Dupes,
12 Health,
13}
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
17pub enum GroupBy {
18 #[value(alias = "team", alias = "codeowner")]
20 Owner,
21 Directory,
23 #[value(alias = "workspace", alias = "pkg")]
25 Package,
26 #[value(alias = "gl-section")]
30 Section,
31}
32
33pub fn build_ownership_resolver(
38 group_by: Option<GroupBy>,
39 root: &Path,
40 codeowners_path: Option<&str>,
41 output: OutputFormat,
42) -> Result<Option<crate::report::OwnershipResolver>, ExitCode> {
43 let Some(mode) = group_by else {
44 return Ok(None);
45 };
46 match mode {
47 GroupBy::Owner => match crate::codeowners::CodeOwners::load(root, codeowners_path) {
48 Ok(co) => Ok(Some(crate::report::OwnershipResolver::Owner(co))),
49 Err(e) => Err(crate::error::emit_error(&e, 2, output)),
50 },
51 GroupBy::Section => match crate::codeowners::CodeOwners::load(root, codeowners_path) {
52 Ok(co) => {
53 if co.has_sections() {
54 Ok(Some(crate::report::OwnershipResolver::Section(co)))
55 } else {
56 Err(crate::error::emit_error(
57 "--group-by section requires a GitLab-style CODEOWNERS file \
58 with `[Section]` headers. This CODEOWNERS has no sections; \
59 use --group-by owner instead.",
60 2,
61 output,
62 ))
63 }
64 }
65 Err(e) => Err(crate::error::emit_error(&e, 2, output)),
66 },
67 GroupBy::Directory => Ok(Some(crate::report::OwnershipResolver::Directory)),
68 GroupBy::Package => {
69 let workspaces = fallow_config::discover_workspaces(root);
70 if workspaces.is_empty() {
71 Err(crate::error::emit_error(
72 "--group-by package requires a monorepo with workspace packages \
73 (package.json workspaces, pnpm-workspace.yaml, or tsconfig references). \
74 For single-package projects try --group-by directory instead.",
75 2,
76 output,
77 ))
78 } else {
79 Ok(Some(crate::report::OwnershipResolver::Package(
80 crate::report::grouping::PackageResolver::new(root, &workspaces),
81 )))
82 }
83 }
84 }
85}
86
87fn log_config_loaded(path: &Path, output: OutputFormat, quiet: bool) {
92 if quiet || !matches!(output, OutputFormat::Human) {
93 return;
94 }
95 eprintln!("loaded config: {}", path.display());
96}
97
98#[expect(clippy::ref_option, reason = "&Option matches clap's field type")]
99pub fn load_config(
100 root: &Path,
101 config_path: &Option<PathBuf>,
102 output: OutputFormat,
103 no_cache: bool,
104 threads: usize,
105 production: bool,
106 quiet: bool,
107) -> Result<ResolvedConfig, ExitCode> {
108 load_config_for_analysis(
109 root,
110 config_path,
111 output,
112 no_cache,
113 threads,
114 production.then_some(true),
115 quiet,
116 ProductionAnalysis::DeadCode,
117 )
118}
119
120#[expect(clippy::ref_option, reason = "&Option matches clap's field type")]
121#[expect(
122 clippy::too_many_arguments,
123 reason = "central config loader mirrors CLI dispatch options"
124)]
125pub fn load_config_for_analysis(
126 root: &Path,
127 config_path: &Option<PathBuf>,
128 output: OutputFormat,
129 no_cache: bool,
130 threads: usize,
131 production_override: Option<bool>,
132 quiet: bool,
133 analysis: ProductionAnalysis,
134) -> Result<ResolvedConfig, ExitCode> {
135 let user_config = if let Some(path) = config_path {
136 match FallowConfig::load(path) {
137 Ok(c) => {
138 log_config_loaded(path, output, quiet);
139 Some(c)
140 }
141 Err(e) => {
142 let msg = format!("failed to load config '{}': {e}", path.display());
143 return Err(crate::error::emit_error(&msg, 2, output));
144 }
145 }
146 } else {
147 match FallowConfig::find_and_load(root) {
148 Ok(Some((config, found_path))) => {
149 log_config_loaded(&found_path, output, quiet);
150 Some(config)
151 }
152 Ok(None) => None,
153 Err(e) => {
154 return Err(crate::error::emit_error(&e, 2, output));
155 }
156 }
157 };
158
159 Ok(match user_config {
160 Some(mut config) => {
161 let production =
162 production_override.unwrap_or_else(|| config.production.for_analysis(analysis));
163 config.production = production.into();
164 config.resolve(root.to_path_buf(), output, threads, no_cache, quiet)
165 }
166 None => FallowConfig {
167 production: production_override.unwrap_or(false).into(),
168 ..FallowConfig::default()
169 }
170 .resolve(root.to_path_buf(), output, threads, no_cache, quiet),
171 })
172}