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 2,
75 output,
76 ))
77 } else {
78 Ok(Some(crate::report::OwnershipResolver::Package(
79 crate::report::grouping::PackageResolver::new(root, &workspaces),
80 )))
81 }
82 }
83 }
84}
85
86fn log_config_loaded(path: &Path, output: OutputFormat, quiet: bool) {
91 if quiet || !matches!(output, OutputFormat::Human) {
92 return;
93 }
94 eprintln!("loaded config: {}", path.display());
95}
96
97#[expect(clippy::ref_option, reason = "&Option matches clap's field type")]
98pub fn load_config(
99 root: &Path,
100 config_path: &Option<PathBuf>,
101 output: OutputFormat,
102 no_cache: bool,
103 threads: usize,
104 production: bool,
105 quiet: bool,
106) -> Result<ResolvedConfig, ExitCode> {
107 load_config_for_analysis(
108 root,
109 config_path,
110 output,
111 no_cache,
112 threads,
113 production.then_some(true),
114 quiet,
115 ProductionAnalysis::DeadCode,
116 )
117}
118
119#[expect(clippy::ref_option, reason = "&Option matches clap's field type")]
120#[expect(
121 clippy::too_many_arguments,
122 reason = "central config loader mirrors CLI dispatch options"
123)]
124pub fn load_config_for_analysis(
125 root: &Path,
126 config_path: &Option<PathBuf>,
127 output: OutputFormat,
128 no_cache: bool,
129 threads: usize,
130 production_override: Option<bool>,
131 quiet: bool,
132 analysis: ProductionAnalysis,
133) -> Result<ResolvedConfig, ExitCode> {
134 let user_config = if let Some(path) = config_path {
135 match FallowConfig::load(path) {
136 Ok(c) => {
137 log_config_loaded(path, output, quiet);
138 Some(c)
139 }
140 Err(e) => {
141 let msg = format!("failed to load config '{}': {e}", path.display());
142 return Err(crate::error::emit_error(&msg, 2, output));
143 }
144 }
145 } else {
146 match FallowConfig::find_and_load(root) {
147 Ok(Some((config, found_path))) => {
148 log_config_loaded(&found_path, output, quiet);
149 Some(config)
150 }
151 Ok(None) => None,
152 Err(e) => {
153 return Err(crate::error::emit_error(&e, 2, output));
154 }
155 }
156 };
157
158 Ok(match user_config {
159 Some(mut config) => {
160 let production =
161 production_override.unwrap_or_else(|| config.production.for_analysis(analysis));
162 config.production = production.into();
163 config.resolve(root.to_path_buf(), output, threads, no_cache, quiet)
164 }
165 None => FallowConfig {
166 production: production_override.unwrap_or(false).into(),
167 ..FallowConfig::default()
168 }
169 .resolve(root.to_path_buf(), output, threads, no_cache, quiet),
170 })
171}