1use std::path::{Path, PathBuf};
4
5use fallow_config::{
6 ConfigLoadOptions, FallowConfig, ProductionAnalysis, ResolvedConfig, WorkspaceDiagnostic,
7 WorkspaceInfo,
8};
9use fallow_types::output_format::OutputFormat;
10use rustc_hash::FxHashSet;
11
12use crate::{EngineError, EngineResult};
13
14#[derive(Debug)]
16pub struct ProjectConfig {
17 pub config: ResolvedConfig,
19 pub path: Option<PathBuf>,
21 pub workspaces: Vec<WorkspaceInfo>,
23 pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
25 pub workspace_discovery_ms: Option<f64>,
27}
28
29#[derive(Debug, Clone, Copy)]
31pub struct ProjectConfigOptions {
32 pub output: OutputFormat,
34 pub no_cache: bool,
36 pub threads: usize,
38 pub production_override: Option<bool>,
41 pub quiet: bool,
43 pub analysis: ProductionAnalysis,
45 pub allow_remote_extends: bool,
47}
48
49#[derive(Debug)]
51#[non_exhaustive]
52pub struct ProjectConfigReadiness {
53 pub project: ProjectConfig,
55 pub configured_plugin_diagnostics: Vec<fallow_config::ConfiguredPluginDiagnostic>,
57}
58
59pub fn config_for_project(root: &Path, config_path: Option<&Path>) -> EngineResult<ProjectConfig> {
66 config_for_project_with_load_options(root, config_path, ConfigLoadOptions::default())
67}
68
69pub fn config_for_project_with_load_options(
75 root: &Path,
76 config_path: Option<&Path>,
77 load_options: ConfigLoadOptions,
78) -> EngineResult<ProjectConfig> {
79 let user_config = load_user_config(root, config_path, load_options)?;
80 let (mut config, path) = match user_config {
81 Some((config, path)) => (config, Some(path)),
82 None => (FallowConfig::default(), None),
83 };
84 if path.is_some() {
85 config.production = config
86 .production
87 .for_analysis(ProductionAnalysis::DeadCode)
88 .into();
89 validate_boundaries_and_rule_packs(root, &config)?;
90 }
91 let threads = std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get);
92 let mut resolved = config.resolve(
93 root.to_path_buf(),
94 OutputFormat::Human,
95 threads,
96 false,
97 true,
98 None,
99 );
100 apply_max_file_size_env(&mut resolved);
101 let (workspaces, workspace_diagnostics, workspace_discovery_ms) =
102 collect_workspace_metadata(&resolved)?;
103 Ok(ProjectConfig {
104 config: resolved,
105 path,
106 workspaces,
107 workspace_diagnostics,
108 workspace_discovery_ms: Some(workspace_discovery_ms),
109 })
110}
111
112#[must_use]
114pub(crate) fn resolve_cache_max_size_bytes(config: &ResolvedConfig) -> usize {
115 config
116 .cache_max_size_mb
117 .map_or(fallow_extract::cache::DEFAULT_CACHE_MAX_SIZE, |mb| {
118 (mb as usize).saturating_mul(1024 * 1024)
119 })
120}
121
122pub(crate) fn default_project_config(root: &Path) -> ProjectConfig {
123 let threads = std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get);
124 let config = FallowConfig::default().resolve(
125 root.to_path_buf(),
126 OutputFormat::Human,
127 threads,
128 false,
129 true,
130 None,
131 );
132 let (workspaces, workspace_diagnostics, workspace_discovery_ms) =
133 collect_workspace_metadata_lossy(&config);
134 ProjectConfig {
135 config,
136 path: None,
137 workspaces,
138 workspace_diagnostics,
139 workspace_discovery_ms: Some(workspace_discovery_ms),
140 }
141}
142
143pub fn config_for_project_analysis(
154 root: &Path,
155 config_path: Option<&Path>,
156 options: ProjectConfigOptions,
157) -> EngineResult<ProjectConfig> {
158 resolve_project_config_analysis(root, config_path, options).map(|(project, _)| project)
159}
160
161pub fn config_for_project_readiness(
168 root: &Path,
169 config_path: Option<&Path>,
170 options: ProjectConfigOptions,
171) -> EngineResult<ProjectConfigReadiness> {
172 let (project, configured_plugin_paths) =
173 resolve_project_config_analysis(root, config_path, options)?;
174 let configured_plugin_diagnostics =
175 fallow_config::diagnose_configured_external_plugins(root, &configured_plugin_paths);
176 Ok(ProjectConfigReadiness {
177 project,
178 configured_plugin_diagnostics,
179 })
180}
181
182fn resolve_project_config_analysis(
183 root: &Path,
184 config_path: Option<&Path>,
185 options: ProjectConfigOptions,
186) -> EngineResult<(ProjectConfig, Vec<String>)> {
187 let user_config = load_user_config(
188 root,
189 config_path,
190 ConfigLoadOptions {
191 allow_remote_extends: options.allow_remote_extends,
192 },
193 )?;
194 let loaded_user_config = user_config.is_some();
195 let (mut config, path) = match user_config {
196 Some((config, path)) => (config, Some(path)),
197 None => (
198 FallowConfig {
199 production: options.production_override.unwrap_or(false).into(),
200 ..FallowConfig::default()
201 },
202 None,
203 ),
204 };
205
206 if loaded_user_config {
207 let production = options
208 .production_override
209 .unwrap_or_else(|| config.production.for_analysis(options.analysis));
210 config.production = production.into();
211 }
212 validate_config(root, &config)?;
213 let configured_plugin_paths = config.plugins.clone();
214 let mut resolved = config.resolve(
215 root.to_path_buf(),
216 options.output,
217 options.threads,
218 options.no_cache,
219 options.quiet,
220 None,
221 );
222 apply_max_file_size_env(&mut resolved);
223 let (workspaces, workspace_diagnostics, workspace_discovery_ms) =
224 collect_workspace_metadata(&resolved)?;
225 Ok((
226 ProjectConfig {
227 config: resolved,
228 path,
229 workspaces,
230 workspace_diagnostics,
231 workspace_discovery_ms: Some(workspace_discovery_ms),
232 },
233 configured_plugin_paths,
234 ))
235}
236
237fn apply_max_file_size_env(config: &mut ResolvedConfig) {
238 let max_file_size_mb = std::env::var("FALLOW_MAX_FILE_SIZE")
239 .ok()
240 .and_then(|raw| raw.trim().parse::<u32>().ok());
241 if let Some(max_file_size_mb) = max_file_size_mb {
242 config.max_file_size_bytes =
243 fallow_config::resolve_max_file_size_bytes(Some(max_file_size_mb));
244 }
245}
246
247pub(crate) fn collect_workspace_metadata(
248 config: &ResolvedConfig,
249) -> EngineResult<(Vec<WorkspaceInfo>, Vec<WorkspaceDiagnostic>, f64)> {
250 let start = std::time::Instant::now();
251 let (workspaces, diagnostics) =
252 fallow_config::discover_workspaces_with_diagnostics(&config.root, &config.ignore_patterns)
253 .map_err(|err| EngineError::new(err.to_string()))?;
254 let diagnostics = with_undeclared_workspace_diagnostics(config, &workspaces, diagnostics);
255 let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
256 Ok((workspaces, diagnostics, elapsed_ms))
257}
258
259fn collect_workspace_metadata_lossy(
260 config: &ResolvedConfig,
261) -> (Vec<WorkspaceInfo>, Vec<WorkspaceDiagnostic>, f64) {
262 collect_workspace_metadata(config).unwrap_or_default()
263}
264
265fn with_undeclared_workspace_diagnostics(
266 config: &ResolvedConfig,
267 workspaces: &[WorkspaceInfo],
268 mut diagnostics: Vec<WorkspaceDiagnostic>,
269) -> Vec<WorkspaceDiagnostic> {
270 let mut existing: FxHashSet<PathBuf> = diagnostics
271 .iter()
272 .map(|diagnostic| {
273 dunce::canonicalize(&diagnostic.path).unwrap_or_else(|_| diagnostic.path.clone())
274 })
275 .collect();
276 for diagnostic in fallow_config::find_undeclared_workspaces_with_ignores(
277 &config.root,
278 workspaces,
279 &config.ignore_patterns,
280 ) {
281 let canonical =
282 dunce::canonicalize(&diagnostic.path).unwrap_or_else(|_| diagnostic.path.clone());
283 if existing.insert(canonical) {
284 diagnostics.push(diagnostic);
285 }
286 }
287 diagnostics
288}
289
290fn load_user_config(
291 root: &Path,
292 config_path: Option<&Path>,
293 options: ConfigLoadOptions,
294) -> EngineResult<Option<(FallowConfig, PathBuf)>> {
295 if let Some(path) = config_path {
296 let config = FallowConfig::load_with_options(path, options)
297 .map_err(|err| EngineError::new(format!("invalid config: {err:#}")))?;
298 return Ok(Some((config, path.to_path_buf())));
299 }
300 FallowConfig::find_and_load_with_options(root, options)
301 .map_err(|err| EngineError::new(format!("invalid config: {err}")))
302}
303
304fn validate_config(root: &Path, config: &FallowConfig) -> EngineResult<()> {
305 fallow_config::discover_and_validate_external_plugins(root, &config.plugins)
306 .map_err(|errors| joined_config_errors("invalid external plugin definition", &errors))?;
307 validate_boundaries_and_rule_packs(root, config)
308}
309
310fn validate_boundaries_and_rule_packs(root: &Path, config: &FallowConfig) -> EngineResult<()> {
311 config
312 .validate_resolved_boundaries(root)
313 .map_err(|errors| joined_config_errors("invalid boundary configuration", &errors))?;
314 let packs = fallow_config::load_rule_packs(root, &config.rule_packs)
315 .map_err(|errors| joined_config_errors("invalid rule pack", &errors))?;
316 let boundaries =
317 fallow_config::resolve_boundaries_for_rule_pack_validation(config.boundaries.clone(), root);
318 let zone_errors = fallow_config::validate_rule_pack_zone_references(
319 root,
320 &config.rule_packs,
321 &packs,
322 &boundaries,
323 );
324 if !zone_errors.is_empty() {
325 return Err(joined_config_errors("invalid rule pack", &zone_errors));
326 }
327 Ok(())
328}
329
330fn joined_config_errors(label: &str, errors: &[impl ToString]) -> EngineError {
331 let joined = errors
332 .iter()
333 .map(ToString::to_string)
334 .collect::<Vec<_>>()
335 .join("\n - ");
336 EngineError::new(format!("{label}:\n - {joined}"))
337}