1use std::path::{Path, PathBuf};
4
5use fallow_config::{
6 FallowConfig, ProductionAnalysis, ResolvedConfig, WorkspaceDiagnostic, WorkspaceInfo,
7};
8use fallow_types::output_format::OutputFormat;
9use rustc_hash::FxHashSet;
10
11use crate::{EngineError, EngineResult};
12
13#[derive(Debug)]
15pub struct ProjectConfig {
16 pub config: ResolvedConfig,
17 pub path: Option<PathBuf>,
18 pub workspaces: Vec<WorkspaceInfo>,
19 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
20 pub workspace_discovery_ms: Option<f64>,
21}
22
23#[derive(Debug, Clone, Copy)]
25pub struct ProjectConfigOptions {
26 pub output: OutputFormat,
27 pub no_cache: bool,
28 pub threads: usize,
29 pub production_override: Option<bool>,
30 pub quiet: bool,
31 pub analysis: ProductionAnalysis,
32}
33
34pub fn config_for_project(root: &Path, config_path: Option<&Path>) -> EngineResult<ProjectConfig> {
41 let user_config = load_user_config(root, config_path)?;
42 let (mut config, path) = match user_config {
43 Some((config, path)) => (config, Some(path)),
44 None => (FallowConfig::default(), None),
45 };
46 if path.is_some() {
47 config.production = config
48 .production
49 .for_analysis(ProductionAnalysis::DeadCode)
50 .into();
51 validate_boundaries_and_rule_packs(root, &config)?;
52 }
53 let threads = std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get);
54 let resolved = config.resolve(
55 root.to_path_buf(),
56 OutputFormat::Human,
57 threads,
58 false,
59 true,
60 None,
61 );
62 let (workspaces, workspace_diagnostics, workspace_discovery_ms) =
63 collect_workspace_metadata(&resolved)?;
64 Ok(ProjectConfig {
65 config: resolved,
66 path,
67 workspaces,
68 workspace_diagnostics,
69 workspace_discovery_ms: Some(workspace_discovery_ms),
70 })
71}
72
73#[must_use]
75pub fn resolve_cache_max_size_bytes(config: &ResolvedConfig) -> usize {
76 config
77 .cache_max_size_mb
78 .map_or(fallow_extract::cache::DEFAULT_CACHE_MAX_SIZE, |mb| {
79 (mb as usize).saturating_mul(1024 * 1024)
80 })
81}
82
83pub fn default_project_config(root: &Path) -> ProjectConfig {
84 let threads = std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get);
85 let config = FallowConfig::default().resolve(
86 root.to_path_buf(),
87 OutputFormat::Human,
88 threads,
89 false,
90 true,
91 None,
92 );
93 let (workspaces, workspace_diagnostics, workspace_discovery_ms) =
94 collect_workspace_metadata_lossy(&config);
95 ProjectConfig {
96 config,
97 path: None,
98 workspaces,
99 workspace_diagnostics,
100 workspace_discovery_ms: Some(workspace_discovery_ms),
101 }
102}
103
104pub fn config_for_project_analysis(
115 root: &Path,
116 config_path: Option<&Path>,
117 options: ProjectConfigOptions,
118) -> EngineResult<ProjectConfig> {
119 let user_config = load_user_config(root, config_path)?;
120 let loaded_user_config = user_config.is_some();
121 let (mut config, path) = match user_config {
122 Some((config, path)) => (config, Some(path)),
123 None => (
124 FallowConfig {
125 production: options.production_override.unwrap_or(false).into(),
126 ..FallowConfig::default()
127 },
128 None,
129 ),
130 };
131
132 if loaded_user_config {
133 let production = options
134 .production_override
135 .unwrap_or_else(|| config.production.for_analysis(options.analysis));
136 config.production = production.into();
137 }
138 validate_config(root, &config)?;
139 let resolved = config.resolve(
140 root.to_path_buf(),
141 options.output,
142 options.threads,
143 options.no_cache,
144 options.quiet,
145 None,
146 );
147 let (workspaces, workspace_diagnostics, workspace_discovery_ms) =
148 collect_workspace_metadata(&resolved)?;
149 Ok(ProjectConfig {
150 config: resolved,
151 path,
152 workspaces,
153 workspace_diagnostics,
154 workspace_discovery_ms: Some(workspace_discovery_ms),
155 })
156}
157
158fn collect_workspace_metadata(
159 config: &ResolvedConfig,
160) -> EngineResult<(Vec<WorkspaceInfo>, Vec<WorkspaceDiagnostic>, f64)> {
161 let start = std::time::Instant::now();
162 let (workspaces, diagnostics) =
163 fallow_config::discover_workspaces_with_diagnostics(&config.root, &config.ignore_patterns)
164 .map_err(|err| EngineError::new(err.to_string()))?;
165 let diagnostics = with_undeclared_workspace_diagnostics(config, &workspaces, diagnostics);
166 let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
167 Ok((workspaces, diagnostics, elapsed_ms))
168}
169
170fn collect_workspace_metadata_lossy(
171 config: &ResolvedConfig,
172) -> (Vec<WorkspaceInfo>, Vec<WorkspaceDiagnostic>, f64) {
173 collect_workspace_metadata(config).unwrap_or_default()
174}
175
176fn with_undeclared_workspace_diagnostics(
177 config: &ResolvedConfig,
178 workspaces: &[WorkspaceInfo],
179 mut diagnostics: Vec<WorkspaceDiagnostic>,
180) -> Vec<WorkspaceDiagnostic> {
181 let mut existing: FxHashSet<PathBuf> = diagnostics
182 .iter()
183 .map(|diagnostic| {
184 dunce::canonicalize(&diagnostic.path).unwrap_or_else(|_| diagnostic.path.clone())
185 })
186 .collect();
187 for diagnostic in fallow_config::find_undeclared_workspaces_with_ignores(
188 &config.root,
189 workspaces,
190 &config.ignore_patterns,
191 ) {
192 let canonical =
193 dunce::canonicalize(&diagnostic.path).unwrap_or_else(|_| diagnostic.path.clone());
194 if existing.insert(canonical) {
195 diagnostics.push(diagnostic);
196 }
197 }
198 diagnostics
199}
200
201fn load_user_config(
202 root: &Path,
203 config_path: Option<&Path>,
204) -> EngineResult<Option<(FallowConfig, PathBuf)>> {
205 if let Some(path) = config_path {
206 let config = FallowConfig::load(path)
207 .map_err(|err| EngineError::new(format!("invalid config: {err:#}")))?;
208 return Ok(Some((config, path.to_path_buf())));
209 }
210 FallowConfig::find_and_load(root)
211 .map_err(|err| EngineError::new(format!("invalid config: {err}")))
212}
213
214fn validate_config(root: &Path, config: &FallowConfig) -> EngineResult<()> {
215 fallow_config::discover_and_validate_external_plugins(root, &config.plugins)
216 .map_err(|errors| joined_config_errors("invalid external plugin definition", &errors))?;
217 validate_boundaries_and_rule_packs(root, config)
218}
219
220fn validate_boundaries_and_rule_packs(root: &Path, config: &FallowConfig) -> EngineResult<()> {
221 config
222 .validate_resolved_boundaries(root)
223 .map_err(|errors| joined_config_errors("invalid boundary configuration", &errors))?;
224 let packs = fallow_config::load_rule_packs(root, &config.rule_packs)
225 .map_err(|errors| joined_config_errors("invalid rule pack", &errors))?;
226 let boundaries =
227 fallow_config::resolve_boundaries_for_rule_pack_validation(config.boundaries.clone(), root);
228 let zone_errors = fallow_config::validate_rule_pack_zone_references(
229 root,
230 &config.rule_packs,
231 &packs,
232 &boundaries,
233 );
234 if !zone_errors.is_empty() {
235 return Err(joined_config_errors("invalid rule pack", &zone_errors));
236 }
237 Ok(())
238}
239
240fn joined_config_errors(label: &str, errors: &[impl ToString]) -> EngineError {
241 let joined = errors
242 .iter()
243 .map(ToString::to_string)
244 .collect::<Vec<_>>()
245 .join("\n - ");
246 EngineError::new(format!("{label}:\n - {joined}"))
247}