1pub mod analyze;
2pub mod cache;
3pub mod discover;
4pub mod duplicates;
5pub mod errors;
6pub mod extract;
7pub mod graph;
8pub mod plugins;
9pub mod progress;
10pub mod resolve;
11pub mod results;
12
13use std::path::Path;
14
15use errors::FallowError;
16use fallow_config::{PackageJson, ResolvedConfig, discover_workspaces};
17use results::AnalysisResults;
18
19pub fn analyze(config: &ResolvedConfig) -> Result<AnalysisResults, FallowError> {
21 let _span = tracing::info_span!("fallow_analyze").entered();
22
23 if !config.root.join("node_modules").is_dir() {
25 tracing::warn!(
26 "node_modules directory not found. Run `npm install` / `pnpm install` first for accurate results."
27 );
28 }
29
30 let workspaces = discover_workspaces(&config.root);
32 if !workspaces.is_empty() {
33 tracing::info!(count = workspaces.len(), "workspaces discovered");
34 }
35
36 let files = discover::discover_files(config);
40
41 let plugin_result = run_plugins(config, &files, &workspaces);
43
44 let mut cache_store = if config.no_cache {
47 None
48 } else {
49 cache::CacheStore::load(&config.cache_dir)
50 };
51
52 let modules = extract::parse_all_files(&files, config, cache_store.as_ref());
53
54 if !config.no_cache {
56 let store = cache_store.get_or_insert_with(cache::CacheStore::new);
57 for module in &modules {
58 if let Some(file) = files.get(module.file_id.0 as usize) {
59 store.insert(&file.path, cache::module_to_cached(module));
60 }
61 }
62 if let Err(e) = store.save(&config.cache_dir) {
63 tracing::warn!("Failed to save cache: {e}");
64 }
65 }
66
67 let mut entry_points = discover::discover_entry_points(config, &files);
69 for ws in &workspaces {
71 let ws_entries = discover::discover_workspace_entry_points(&ws.root, config, &files);
72 entry_points.extend(ws_entries);
73 }
74
75 let plugin_entries = discover::discover_plugin_entry_points(&plugin_result, config, &files);
77 entry_points.extend(plugin_entries);
78
79 let resolved = resolve::resolve_all_imports(&modules, config, &files);
81
82 let graph = graph::ModuleGraph::build(&resolved, &entry_points, &files);
84
85 Ok(analyze::find_dead_code_full(
87 &graph,
88 config,
89 &resolved,
90 Some(&plugin_result),
91 &workspaces,
92 ))
93}
94
95fn run_plugins(
97 config: &ResolvedConfig,
98 files: &[discover::DiscoveredFile],
99 workspaces: &[fallow_config::WorkspaceInfo],
100) -> plugins::AggregatedPluginResult {
101 let registry = plugins::PluginRegistry::new();
102 let file_paths: Vec<std::path::PathBuf> = files.iter().map(|f| f.path.clone()).collect();
103
104 let pkg_path = config.root.join("package.json");
106 let mut result = if let Ok(pkg) = PackageJson::load(&pkg_path) {
107 registry.run(&pkg, &config.root, &file_paths)
108 } else {
109 plugins::AggregatedPluginResult::default()
110 };
111
112 for ws in workspaces {
114 let ws_pkg_path = ws.root.join("package.json");
115 if let Ok(ws_pkg) = PackageJson::load(&ws_pkg_path) {
116 let ws_result = registry.run(&ws_pkg, &ws.root, &file_paths);
117
118 let ws_prefix = ws
123 .root
124 .strip_prefix(&config.root)
125 .unwrap_or(&ws.root)
126 .to_string_lossy();
127
128 for pat in &ws_result.entry_patterns {
129 result.entry_patterns.push(format!("{ws_prefix}/{pat}"));
130 }
131 for pat in &ws_result.always_used {
132 result.always_used.push(format!("{ws_prefix}/{pat}"));
133 }
134 for pat in &ws_result.discovered_always_used {
135 result
136 .discovered_always_used
137 .push(format!("{ws_prefix}/{pat}"));
138 }
139 for (file_pat, exports) in &ws_result.used_exports {
140 result
141 .used_exports
142 .push((format!("{ws_prefix}/{file_pat}"), exports.clone()));
143 }
144 result
146 .referenced_dependencies
147 .extend(ws_result.referenced_dependencies);
148 result.setup_files.extend(ws_result.setup_files);
149 result
150 .tooling_dependencies
151 .extend(ws_result.tooling_dependencies);
152 }
153 }
154
155 result
156}
157
158pub fn analyze_project(root: &Path) -> Result<AnalysisResults, FallowError> {
160 let config = default_config(root);
161 analyze(&config)
162}
163
164pub(crate) fn default_config(root: &Path) -> ResolvedConfig {
166 let user_config = fallow_config::FallowConfig::find_and_load(root)
167 .ok()
168 .flatten();
169 match user_config {
170 Some((config, _path)) => config.resolve(root.to_path_buf(), num_cpus(), false),
171 None => fallow_config::FallowConfig {
172 entry: vec![],
173 ignore: vec![],
174 detect: fallow_config::DetectConfig::default(),
175 framework: vec![],
176 workspaces: None,
177 ignore_dependencies: vec![],
178 ignore_exports: vec![],
179 output: fallow_config::OutputFormat::Human,
180 duplicates: fallow_config::DuplicatesConfig::default(),
181 }
182 .resolve(root.to_path_buf(), num_cpus(), false),
183 }
184}
185
186fn num_cpus() -> usize {
187 std::thread::available_parallelism()
188 .map(|n| n.get())
189 .unwrap_or(4)
190}