1pub mod analyze;
2pub mod cache;
3pub mod churn;
4pub mod cross_reference;
5pub mod discover;
6pub mod duplicates;
7pub mod errors;
8pub mod extract;
9pub mod plugins;
10pub mod progress;
11pub mod results;
12pub mod scripts;
13pub mod suppress;
14pub mod trace;
15
16pub use fallow_graph::graph;
18pub use fallow_graph::project;
19pub use fallow_graph::resolve;
20
21use std::path::Path;
22use std::time::Instant;
23
24use errors::FallowError;
25use fallow_config::{
26 EntryPointRole, PackageJson, ResolvedConfig, discover_workspaces, find_undeclared_workspaces,
27};
28use rayon::prelude::*;
29use results::AnalysisResults;
30use trace::PipelineTimings;
31
32pub struct AnalysisOutput {
34 pub results: AnalysisResults,
35 pub timings: Option<PipelineTimings>,
36 pub graph: Option<graph::ModuleGraph>,
37}
38
39fn update_cache(
41 store: &mut cache::CacheStore,
42 modules: &[extract::ModuleInfo],
43 files: &[discover::DiscoveredFile],
44) {
45 for module in modules {
46 if let Some(file) = files.get(module.file_id.0 as usize) {
47 let (mt, sz) = file_mtime_and_size(&file.path);
48 if let Some(cached) = store.get_by_path_only(&file.path)
50 && cached.content_hash == module.content_hash
51 {
52 if cached.mtime_secs != mt || cached.file_size != sz {
53 store.insert(&file.path, cache::module_to_cached(module, mt, sz));
54 }
55 continue;
56 }
57 store.insert(&file.path, cache::module_to_cached(module, mt, sz));
58 }
59 }
60 store.retain_paths(files);
61}
62
63fn file_mtime_and_size(path: &std::path::Path) -> (u64, u64) {
65 std::fs::metadata(path)
66 .map(|m| {
67 let mt = m
68 .modified()
69 .ok()
70 .and_then(|t| t.duration_since(std::time::SystemTime::UNIX_EPOCH).ok())
71 .map_or(0, |d| d.as_secs());
72 (mt, m.len())
73 })
74 .unwrap_or((0, 0))
75}
76
77pub fn analyze(config: &ResolvedConfig) -> Result<AnalysisResults, FallowError> {
83 let output = analyze_full(config, false, false)?;
84 Ok(output.results)
85}
86
87pub fn analyze_with_usages(config: &ResolvedConfig) -> Result<AnalysisResults, FallowError> {
93 let output = analyze_full(config, false, true)?;
94 Ok(output.results)
95}
96
97pub fn analyze_with_trace(config: &ResolvedConfig) -> Result<AnalysisOutput, FallowError> {
103 analyze_full(config, true, false)
104}
105
106pub fn analyze_with_parse_result(
117 config: &ResolvedConfig,
118 modules: &[extract::ModuleInfo],
119) -> Result<AnalysisOutput, FallowError> {
120 let _span = tracing::info_span!("fallow_analyze_with_parse_result").entered();
121 let pipeline_start = Instant::now();
122
123 let show_progress = !config.quiet
124 && std::io::IsTerminal::is_terminal(&std::io::stderr())
125 && matches!(
126 config.output,
127 fallow_config::OutputFormat::Human
128 | fallow_config::OutputFormat::Compact
129 | fallow_config::OutputFormat::Markdown
130 );
131 let progress = progress::AnalysisProgress::new(show_progress);
132
133 if !config.root.join("node_modules").is_dir() {
134 tracing::warn!(
135 "node_modules directory not found. Run `npm install` / `pnpm install` first for accurate results."
136 );
137 }
138
139 let t = Instant::now();
141 let workspaces_vec = discover_workspaces(&config.root);
142 let workspaces_ms = t.elapsed().as_secs_f64() * 1000.0;
143 if !workspaces_vec.is_empty() {
144 tracing::info!(count = workspaces_vec.len(), "workspaces discovered");
145 }
146
147 if !config.quiet {
149 let undeclared = find_undeclared_workspaces(&config.root, &workspaces_vec);
150 for diag in &undeclared {
151 tracing::warn!("{}", diag.message);
152 }
153 }
154
155 let t = Instant::now();
157 let pb = progress.stage_spinner("Discovering files...");
158 let discovered_files = discover::discover_files(config);
159 let discover_ms = t.elapsed().as_secs_f64() * 1000.0;
160 pb.finish_and_clear();
161
162 let project = project::ProjectState::new(discovered_files, workspaces_vec);
163 let files = project.files();
164 let workspaces = project.workspaces();
165
166 let t = Instant::now();
168 let pb = progress.stage_spinner("Detecting plugins...");
169 let mut plugin_result = run_plugins(config, files, workspaces);
170 let plugins_ms = t.elapsed().as_secs_f64() * 1000.0;
171 pb.finish_and_clear();
172
173 let t = Instant::now();
175 analyze_all_scripts(config, workspaces, &mut plugin_result);
176 let scripts_ms = t.elapsed().as_secs_f64() * 1000.0;
177
178 let t = Instant::now();
182 let entry_points = discover_all_entry_points(config, files, workspaces, &plugin_result);
183 let entry_points_ms = t.elapsed().as_secs_f64() * 1000.0;
184
185 let ep_summary = summarize_entry_points(&entry_points.all);
187
188 let t = Instant::now();
190 let pb = progress.stage_spinner("Resolving imports...");
191 let resolved = resolve::resolve_all_imports(
192 modules,
193 files,
194 workspaces,
195 &plugin_result.active_plugins,
196 &plugin_result.path_aliases,
197 &config.root,
198 );
199 let resolve_ms = t.elapsed().as_secs_f64() * 1000.0;
200 pb.finish_and_clear();
201
202 let t = Instant::now();
204 let pb = progress.stage_spinner("Building module graph...");
205 let graph = graph::ModuleGraph::build_with_reachability_roots(
206 &resolved,
207 &entry_points.all,
208 &entry_points.runtime,
209 &entry_points.test,
210 files,
211 );
212 let graph_ms = t.elapsed().as_secs_f64() * 1000.0;
213 pb.finish_and_clear();
214
215 let t = Instant::now();
217 let pb = progress.stage_spinner("Analyzing...");
218 let mut result = analyze::find_dead_code_full(
219 &graph,
220 config,
221 &resolved,
222 Some(&plugin_result),
223 workspaces,
224 modules,
225 false,
226 );
227 let analyze_ms = t.elapsed().as_secs_f64() * 1000.0;
228 pb.finish_and_clear();
229 progress.finish();
230
231 result.entry_point_summary = Some(ep_summary);
232
233 let total_ms = pipeline_start.elapsed().as_secs_f64() * 1000.0;
234
235 tracing::debug!(
236 "\n┌─ Pipeline Profile (reuse) ─────────────────────\n\
237 │ discover files: {:>8.1}ms ({} files)\n\
238 │ workspaces: {:>8.1}ms\n\
239 │ plugins: {:>8.1}ms\n\
240 │ script analysis: {:>8.1}ms\n\
241 │ parse/extract: SKIPPED (reused {} modules)\n\
242 │ entry points: {:>8.1}ms ({} entries)\n\
243 │ resolve imports: {:>8.1}ms\n\
244 │ build graph: {:>8.1}ms\n\
245 │ analyze: {:>8.1}ms\n\
246 │ ────────────────────────────────────────────\n\
247 │ TOTAL: {:>8.1}ms\n\
248 └─────────────────────────────────────────────────",
249 discover_ms,
250 files.len(),
251 workspaces_ms,
252 plugins_ms,
253 scripts_ms,
254 modules.len(),
255 entry_points_ms,
256 entry_points.all.len(),
257 resolve_ms,
258 graph_ms,
259 analyze_ms,
260 total_ms,
261 );
262
263 let timings = Some(PipelineTimings {
264 discover_files_ms: discover_ms,
265 file_count: files.len(),
266 workspaces_ms,
267 workspace_count: workspaces.len(),
268 plugins_ms,
269 script_analysis_ms: scripts_ms,
270 parse_extract_ms: 0.0, module_count: modules.len(),
272 cache_hits: 0,
273 cache_misses: 0,
274 cache_update_ms: 0.0,
275 entry_points_ms,
276 entry_point_count: entry_points.all.len(),
277 resolve_imports_ms: resolve_ms,
278 build_graph_ms: graph_ms,
279 analyze_ms,
280 total_ms,
281 });
282
283 Ok(AnalysisOutput {
284 results: result,
285 timings,
286 graph: Some(graph),
287 })
288}
289
290#[expect(
291 clippy::unnecessary_wraps,
292 reason = "Result kept for future error handling"
293)]
294#[expect(
295 clippy::too_many_lines,
296 reason = "main pipeline function; split candidate for sig-audit-loop"
297)]
298fn analyze_full(
299 config: &ResolvedConfig,
300 retain: bool,
301 collect_usages: bool,
302) -> Result<AnalysisOutput, FallowError> {
303 let _span = tracing::info_span!("fallow_analyze").entered();
304 let pipeline_start = Instant::now();
305
306 let show_progress = !config.quiet
310 && std::io::IsTerminal::is_terminal(&std::io::stderr())
311 && matches!(
312 config.output,
313 fallow_config::OutputFormat::Human
314 | fallow_config::OutputFormat::Compact
315 | fallow_config::OutputFormat::Markdown
316 );
317 let progress = progress::AnalysisProgress::new(show_progress);
318
319 if !config.root.join("node_modules").is_dir() {
321 tracing::warn!(
322 "node_modules directory not found. Run `npm install` / `pnpm install` first for accurate results."
323 );
324 }
325
326 let t = Instant::now();
328 let workspaces_vec = discover_workspaces(&config.root);
329 let workspaces_ms = t.elapsed().as_secs_f64() * 1000.0;
330 if !workspaces_vec.is_empty() {
331 tracing::info!(count = workspaces_vec.len(), "workspaces discovered");
332 }
333
334 if !config.quiet {
336 let undeclared = find_undeclared_workspaces(&config.root, &workspaces_vec);
337 for diag in &undeclared {
338 tracing::warn!("{}", diag.message);
339 }
340 }
341
342 let t = Instant::now();
344 let pb = progress.stage_spinner("Discovering files...");
345 let discovered_files = discover::discover_files(config);
346 let discover_ms = t.elapsed().as_secs_f64() * 1000.0;
347 pb.finish_and_clear();
348
349 let project = project::ProjectState::new(discovered_files, workspaces_vec);
352 let files = project.files();
353 let workspaces = project.workspaces();
354
355 let t = Instant::now();
357 let pb = progress.stage_spinner("Detecting plugins...");
358 let mut plugin_result = run_plugins(config, files, workspaces);
359 let plugins_ms = t.elapsed().as_secs_f64() * 1000.0;
360 pb.finish_and_clear();
361
362 let t = Instant::now();
364 analyze_all_scripts(config, workspaces, &mut plugin_result);
365 let scripts_ms = t.elapsed().as_secs_f64() * 1000.0;
366
367 let t = Instant::now();
369 let pb = progress.stage_spinner(&format!("Parsing {} files...", files.len()));
370 let mut cache_store = if config.no_cache {
371 None
372 } else {
373 cache::CacheStore::load(&config.cache_dir)
374 };
375
376 let parse_result = extract::parse_all_files(files, cache_store.as_ref(), false);
377 let modules = parse_result.modules;
378 let cache_hits = parse_result.cache_hits;
379 let cache_misses = parse_result.cache_misses;
380 let parse_ms = t.elapsed().as_secs_f64() * 1000.0;
381 pb.finish_and_clear();
382
383 let t = Instant::now();
385 if !config.no_cache {
386 let store = cache_store.get_or_insert_with(cache::CacheStore::new);
387 update_cache(store, &modules, files);
388 if let Err(e) = store.save(&config.cache_dir) {
389 tracing::warn!("Failed to save cache: {e}");
390 }
391 }
392 let cache_ms = t.elapsed().as_secs_f64() * 1000.0;
393
394 let t = Instant::now();
396 let entry_points = discover_all_entry_points(config, files, workspaces, &plugin_result);
397 let entry_points_ms = t.elapsed().as_secs_f64() * 1000.0;
398
399 let t = Instant::now();
401 let pb = progress.stage_spinner("Resolving imports...");
402 let resolved = resolve::resolve_all_imports(
403 &modules,
404 files,
405 workspaces,
406 &plugin_result.active_plugins,
407 &plugin_result.path_aliases,
408 &config.root,
409 );
410 let resolve_ms = t.elapsed().as_secs_f64() * 1000.0;
411 pb.finish_and_clear();
412
413 let t = Instant::now();
415 let pb = progress.stage_spinner("Building module graph...");
416 let graph = graph::ModuleGraph::build_with_reachability_roots(
417 &resolved,
418 &entry_points.all,
419 &entry_points.runtime,
420 &entry_points.test,
421 files,
422 );
423 let graph_ms = t.elapsed().as_secs_f64() * 1000.0;
424 pb.finish_and_clear();
425
426 let ep_summary = summarize_entry_points(&entry_points.all);
428
429 let t = Instant::now();
431 let pb = progress.stage_spinner("Analyzing...");
432 let mut result = analyze::find_dead_code_full(
433 &graph,
434 config,
435 &resolved,
436 Some(&plugin_result),
437 workspaces,
438 &modules,
439 collect_usages,
440 );
441 let analyze_ms = t.elapsed().as_secs_f64() * 1000.0;
442 pb.finish_and_clear();
443 progress.finish();
444
445 result.entry_point_summary = Some(ep_summary);
446
447 let total_ms = pipeline_start.elapsed().as_secs_f64() * 1000.0;
448
449 let cache_summary = if cache_hits > 0 {
450 format!(" ({cache_hits} cached, {cache_misses} parsed)")
451 } else {
452 String::new()
453 };
454
455 tracing::debug!(
456 "\n┌─ Pipeline Profile ─────────────────────────────\n\
457 │ discover files: {:>8.1}ms ({} files)\n\
458 │ workspaces: {:>8.1}ms\n\
459 │ plugins: {:>8.1}ms\n\
460 │ script analysis: {:>8.1}ms\n\
461 │ parse/extract: {:>8.1}ms ({} modules{})\n\
462 │ cache update: {:>8.1}ms\n\
463 │ entry points: {:>8.1}ms ({} entries)\n\
464 │ resolve imports: {:>8.1}ms\n\
465 │ build graph: {:>8.1}ms\n\
466 │ analyze: {:>8.1}ms\n\
467 │ ────────────────────────────────────────────\n\
468 │ TOTAL: {:>8.1}ms\n\
469 └─────────────────────────────────────────────────",
470 discover_ms,
471 files.len(),
472 workspaces_ms,
473 plugins_ms,
474 scripts_ms,
475 parse_ms,
476 modules.len(),
477 cache_summary,
478 cache_ms,
479 entry_points_ms,
480 entry_points.all.len(),
481 resolve_ms,
482 graph_ms,
483 analyze_ms,
484 total_ms,
485 );
486
487 let timings = if retain {
488 Some(PipelineTimings {
489 discover_files_ms: discover_ms,
490 file_count: files.len(),
491 workspaces_ms,
492 workspace_count: workspaces.len(),
493 plugins_ms,
494 script_analysis_ms: scripts_ms,
495 parse_extract_ms: parse_ms,
496 module_count: modules.len(),
497 cache_hits,
498 cache_misses,
499 cache_update_ms: cache_ms,
500 entry_points_ms,
501 entry_point_count: entry_points.all.len(),
502 resolve_imports_ms: resolve_ms,
503 build_graph_ms: graph_ms,
504 analyze_ms,
505 total_ms,
506 })
507 } else {
508 None
509 };
510
511 Ok(AnalysisOutput {
512 results: result,
513 timings,
514 graph: if retain { Some(graph) } else { None },
515 })
516}
517
518fn analyze_all_scripts(
523 config: &ResolvedConfig,
524 workspaces: &[fallow_config::WorkspaceInfo],
525 plugin_result: &mut plugins::AggregatedPluginResult,
526) {
527 let pkg_path = config.root.join("package.json");
528 if let Ok(pkg) = PackageJson::load(&pkg_path)
529 && let Some(ref pkg_scripts) = pkg.scripts
530 {
531 let scripts_to_analyze = if config.production {
532 scripts::filter_production_scripts(pkg_scripts)
533 } else {
534 pkg_scripts.clone()
535 };
536 let script_analysis = scripts::analyze_scripts(&scripts_to_analyze, &config.root);
537 plugin_result.script_used_packages = script_analysis.used_packages;
538
539 for config_file in &script_analysis.config_files {
540 plugin_result
541 .discovered_always_used
542 .push((config_file.clone(), "scripts".to_string()));
543 }
544 }
545 for ws in workspaces {
546 let ws_pkg_path = ws.root.join("package.json");
547 if let Ok(ws_pkg) = PackageJson::load(&ws_pkg_path)
548 && let Some(ref ws_scripts) = ws_pkg.scripts
549 {
550 let scripts_to_analyze = if config.production {
551 scripts::filter_production_scripts(ws_scripts)
552 } else {
553 ws_scripts.clone()
554 };
555 let ws_analysis = scripts::analyze_scripts(&scripts_to_analyze, &ws.root);
556 plugin_result
557 .script_used_packages
558 .extend(ws_analysis.used_packages);
559
560 let ws_prefix = ws
561 .root
562 .strip_prefix(&config.root)
563 .unwrap_or(&ws.root)
564 .to_string_lossy();
565 for config_file in &ws_analysis.config_files {
566 plugin_result
567 .discovered_always_used
568 .push((format!("{ws_prefix}/{config_file}"), "scripts".to_string()));
569 }
570 }
571 }
572
573 let ci_packages = scripts::ci::analyze_ci_files(&config.root);
575 plugin_result.script_used_packages.extend(ci_packages);
576 plugin_result
577 .entry_point_roles
578 .entry("scripts".to_string())
579 .or_insert(EntryPointRole::Support);
580}
581
582fn discover_all_entry_points(
584 config: &ResolvedConfig,
585 files: &[discover::DiscoveredFile],
586 workspaces: &[fallow_config::WorkspaceInfo],
587 plugin_result: &plugins::AggregatedPluginResult,
588) -> discover::CategorizedEntryPoints {
589 let mut entry_points = discover::CategorizedEntryPoints::default();
590 entry_points.extend_runtime(discover::discover_entry_points(config, files));
591
592 let ws_entries: Vec<_> = workspaces
593 .par_iter()
594 .flat_map(|ws| discover::discover_workspace_entry_points(&ws.root, config, files))
595 .collect();
596 entry_points.extend_runtime(ws_entries);
597
598 let plugin_entries = discover::discover_plugin_entry_point_sets(plugin_result, config, files);
599 entry_points.extend(plugin_entries);
600
601 let infra_entries = discover::discover_infrastructure_entry_points(&config.root);
602 entry_points.extend_runtime(infra_entries);
603
604 if !config.dynamically_loaded.is_empty() {
606 let dynamic_entries = discover::discover_dynamically_loaded_entry_points(config, files);
607 entry_points.extend_runtime(dynamic_entries);
608 }
609
610 entry_points.dedup()
611}
612
613fn summarize_entry_points(entry_points: &[discover::EntryPoint]) -> results::EntryPointSummary {
615 let mut counts: rustc_hash::FxHashMap<String, usize> = rustc_hash::FxHashMap::default();
616 for ep in entry_points {
617 let category = match &ep.source {
618 discover::EntryPointSource::PackageJsonMain
619 | discover::EntryPointSource::PackageJsonModule
620 | discover::EntryPointSource::PackageJsonExports
621 | discover::EntryPointSource::PackageJsonBin
622 | discover::EntryPointSource::PackageJsonScript => "package.json",
623 discover::EntryPointSource::Plugin { .. } => "plugin",
624 discover::EntryPointSource::TestFile => "test file",
625 discover::EntryPointSource::DefaultIndex => "default index",
626 discover::EntryPointSource::ManualEntry => "manual entry",
627 discover::EntryPointSource::InfrastructureConfig => "config",
628 discover::EntryPointSource::DynamicallyLoaded => "dynamically loaded",
629 };
630 *counts.entry(category.to_string()).or_insert(0) += 1;
631 }
632 let mut by_source: Vec<(String, usize)> = counts.into_iter().collect();
633 by_source.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
634 results::EntryPointSummary {
635 total: entry_points.len(),
636 by_source,
637 }
638}
639
640fn run_plugins(
642 config: &ResolvedConfig,
643 files: &[discover::DiscoveredFile],
644 workspaces: &[fallow_config::WorkspaceInfo],
645) -> plugins::AggregatedPluginResult {
646 let registry = plugins::PluginRegistry::new(config.external_plugins.clone());
647 let file_paths: Vec<std::path::PathBuf> = files.iter().map(|f| f.path.clone()).collect();
648
649 let pkg_path = config.root.join("package.json");
651 let mut result = PackageJson::load(&pkg_path).map_or_else(
652 |_| plugins::AggregatedPluginResult::default(),
653 |pkg| registry.run(&pkg, &config.root, &file_paths),
654 );
655
656 if workspaces.is_empty() {
657 return result;
658 }
659
660 let precompiled_matchers = registry.precompile_config_matchers();
664 let relative_files: Vec<(&std::path::PathBuf, String)> = file_paths
665 .iter()
666 .map(|f| {
667 let rel = f
668 .strip_prefix(&config.root)
669 .unwrap_or(f)
670 .to_string_lossy()
671 .into_owned();
672 (f, rel)
673 })
674 .collect();
675
676 let ws_results: Vec<_> = workspaces
678 .par_iter()
679 .filter_map(|ws| {
680 let ws_pkg_path = ws.root.join("package.json");
681 let ws_pkg = PackageJson::load(&ws_pkg_path).ok()?;
682 let ws_result = registry.run_workspace_fast(
683 &ws_pkg,
684 &ws.root,
685 &config.root,
686 &precompiled_matchers,
687 &relative_files,
688 );
689 if ws_result.active_plugins.is_empty() {
690 return None;
691 }
692 let ws_prefix = ws
693 .root
694 .strip_prefix(&config.root)
695 .unwrap_or(&ws.root)
696 .to_string_lossy()
697 .into_owned();
698 Some((ws_result, ws_prefix))
699 })
700 .collect();
701
702 let mut seen_plugins: rustc_hash::FxHashSet<String> =
705 result.active_plugins.iter().cloned().collect();
706 let mut seen_prefixes: rustc_hash::FxHashSet<String> =
707 result.virtual_module_prefixes.iter().cloned().collect();
708 let mut seen_generated: rustc_hash::FxHashSet<String> =
709 result.generated_import_patterns.iter().cloned().collect();
710 for (ws_result, ws_prefix) in ws_results {
711 let prefix_if_needed = |pat: &str| -> String {
716 if pat.starts_with(ws_prefix.as_str()) || pat.starts_with('/') {
717 pat.to_string()
718 } else {
719 format!("{ws_prefix}/{pat}")
720 }
721 };
722
723 for (pat, pname) in &ws_result.entry_patterns {
724 result
725 .entry_patterns
726 .push((prefix_if_needed(pat), pname.clone()));
727 }
728 for (plugin_name, role) in ws_result.entry_point_roles {
729 result.entry_point_roles.entry(plugin_name).or_insert(role);
730 }
731 for (pat, pname) in &ws_result.always_used {
732 result
733 .always_used
734 .push((prefix_if_needed(pat), pname.clone()));
735 }
736 for (pat, pname) in &ws_result.discovered_always_used {
737 result
738 .discovered_always_used
739 .push((prefix_if_needed(pat), pname.clone()));
740 }
741 for (pat, pname) in &ws_result.fixture_patterns {
742 result
743 .fixture_patterns
744 .push((prefix_if_needed(pat), pname.clone()));
745 }
746 for (file_pat, exports) in &ws_result.used_exports {
747 result
748 .used_exports
749 .push((prefix_if_needed(file_pat), exports.clone()));
750 }
751 for plugin_name in ws_result.active_plugins {
753 if !seen_plugins.contains(&plugin_name) {
754 seen_plugins.insert(plugin_name.clone());
755 result.active_plugins.push(plugin_name);
756 }
757 }
758 result
760 .referenced_dependencies
761 .extend(ws_result.referenced_dependencies);
762 result.setup_files.extend(ws_result.setup_files);
763 result
764 .tooling_dependencies
765 .extend(ws_result.tooling_dependencies);
766 for prefix in ws_result.virtual_module_prefixes {
769 if !seen_prefixes.contains(&prefix) {
770 seen_prefixes.insert(prefix.clone());
771 result.virtual_module_prefixes.push(prefix);
772 }
773 }
774 for pattern in ws_result.generated_import_patterns {
777 if !seen_generated.contains(&pattern) {
778 seen_generated.insert(pattern.clone());
779 result.generated_import_patterns.push(pattern);
780 }
781 }
782 for (prefix, replacement) in ws_result.path_aliases {
785 result
786 .path_aliases
787 .push((prefix, format!("{ws_prefix}/{replacement}")));
788 }
789 }
790
791 result
792}
793
794pub fn analyze_project(root: &Path) -> Result<AnalysisResults, FallowError> {
800 let config = default_config(root);
801 analyze_with_usages(&config)
802}
803
804pub(crate) fn default_config(root: &Path) -> ResolvedConfig {
806 let user_config = fallow_config::FallowConfig::find_and_load(root)
807 .ok()
808 .flatten();
809 match user_config {
810 Some((config, _path)) => config.resolve(
811 root.to_path_buf(),
812 fallow_config::OutputFormat::Human,
813 num_cpus(),
814 false,
815 true, ),
817 None => fallow_config::FallowConfig::default().resolve(
818 root.to_path_buf(),
819 fallow_config::OutputFormat::Human,
820 num_cpus(),
821 false,
822 true,
823 ),
824 }
825}
826
827fn num_cpus() -> usize {
828 std::thread::available_parallelism()
829 .map(std::num::NonZeroUsize::get)
830 .unwrap_or(4)
831}