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;
12pub mod scripts;
13pub mod suppress;
14
15use std::path::Path;
16use std::time::Instant;
17
18use errors::FallowError;
19use fallow_config::{PackageJson, ResolvedConfig, discover_workspaces};
20use results::AnalysisResults;
21
22pub fn analyze(config: &ResolvedConfig) -> Result<AnalysisResults, FallowError> {
24 let _span = tracing::info_span!("fallow_analyze").entered();
25 let pipeline_start = Instant::now();
26
27 if !config.root.join("node_modules").is_dir() {
29 tracing::warn!(
30 "node_modules directory not found. Run `npm install` / `pnpm install` first for accurate results."
31 );
32 }
33
34 let t = Instant::now();
36 let workspaces = discover_workspaces(&config.root);
37 let workspaces_ms = t.elapsed().as_secs_f64() * 1000.0;
38 if !workspaces.is_empty() {
39 tracing::info!(count = workspaces.len(), "workspaces discovered");
40 }
41
42 let t = Instant::now();
44 let files = discover::discover_files(config);
45 let discover_ms = t.elapsed().as_secs_f64() * 1000.0;
46
47 let t = Instant::now();
49 let mut plugin_result = run_plugins(config, &files, &workspaces);
50 let plugins_ms = t.elapsed().as_secs_f64() * 1000.0;
51
52 let t = Instant::now();
54 let pkg_path = config.root.join("package.json");
55 if let Ok(pkg) = PackageJson::load(&pkg_path)
56 && let Some(ref pkg_scripts) = pkg.scripts
57 {
58 let scripts_to_analyze = if config.production {
60 scripts::filter_production_scripts(pkg_scripts)
61 } else {
62 pkg_scripts.clone()
63 };
64 let script_analysis = scripts::analyze_scripts(&scripts_to_analyze, &config.root);
65 plugin_result.script_used_packages = script_analysis.used_packages;
66
67 for config_file in &script_analysis.config_files {
69 plugin_result.entry_patterns.push(config_file.clone());
70 }
71 }
72 for ws in &workspaces {
74 let ws_pkg_path = ws.root.join("package.json");
75 if let Ok(ws_pkg) = PackageJson::load(&ws_pkg_path)
76 && let Some(ref ws_scripts) = ws_pkg.scripts
77 {
78 let scripts_to_analyze = if config.production {
79 scripts::filter_production_scripts(ws_scripts)
80 } else {
81 ws_scripts.clone()
82 };
83 let ws_analysis = scripts::analyze_scripts(&scripts_to_analyze, &ws.root);
84 plugin_result
85 .script_used_packages
86 .extend(ws_analysis.used_packages);
87
88 let ws_prefix = ws
89 .root
90 .strip_prefix(&config.root)
91 .unwrap_or(&ws.root)
92 .to_string_lossy();
93 for config_file in &ws_analysis.config_files {
94 plugin_result
95 .entry_patterns
96 .push(format!("{ws_prefix}/{config_file}"));
97 }
98 }
99 }
100 let scripts_ms = t.elapsed().as_secs_f64() * 1000.0;
101
102 let t = Instant::now();
104 let mut cache_store = if config.no_cache {
105 None
106 } else {
107 cache::CacheStore::load(&config.cache_dir)
108 };
109
110 let modules = extract::parse_all_files(&files, config, cache_store.as_ref());
111 let parse_ms = t.elapsed().as_secs_f64() * 1000.0;
112
113 let t = Instant::now();
115 if !config.no_cache {
116 let store = cache_store.get_or_insert_with(cache::CacheStore::new);
117 for module in &modules {
118 if let Some(file) = files.get(module.file_id.0 as usize) {
119 store.insert(&file.path, cache::module_to_cached(module));
120 }
121 }
122 if let Err(e) = store.save(&config.cache_dir) {
123 tracing::warn!("Failed to save cache: {e}");
124 }
125 }
126 let cache_ms = t.elapsed().as_secs_f64() * 1000.0;
127
128 let t = Instant::now();
130 let mut entry_points = discover::discover_entry_points(config, &files);
131 for ws in &workspaces {
132 let ws_entries = discover::discover_workspace_entry_points(&ws.root, config, &files);
133 entry_points.extend(ws_entries);
134 }
135 let plugin_entries = discover::discover_plugin_entry_points(&plugin_result, config, &files);
136 entry_points.extend(plugin_entries);
137 let entry_points_ms = t.elapsed().as_secs_f64() * 1000.0;
138
139 let t = Instant::now();
141 let resolved = resolve::resolve_all_imports(&modules, config, &files);
142 let resolve_ms = t.elapsed().as_secs_f64() * 1000.0;
143
144 let t = Instant::now();
146 let graph = graph::ModuleGraph::build(&resolved, &entry_points, &files);
147 let graph_ms = t.elapsed().as_secs_f64() * 1000.0;
148
149 let t = Instant::now();
151 let result = analyze::find_dead_code_full(
152 &graph,
153 config,
154 &resolved,
155 Some(&plugin_result),
156 &workspaces,
157 &modules,
158 );
159 let analyze_ms = t.elapsed().as_secs_f64() * 1000.0;
160
161 let total_ms = pipeline_start.elapsed().as_secs_f64() * 1000.0;
162
163 tracing::debug!(
164 "\n┌─ Pipeline Profile ─────────────────────────────\n\
165 │ discover files: {:>8.1}ms ({} files)\n\
166 │ workspaces: {:>8.1}ms\n\
167 │ plugins: {:>8.1}ms\n\
168 │ script analysis: {:>8.1}ms\n\
169 │ parse/extract: {:>8.1}ms ({} modules)\n\
170 │ cache update: {:>8.1}ms\n\
171 │ entry points: {:>8.1}ms ({} entries)\n\
172 │ resolve imports: {:>8.1}ms\n\
173 │ build graph: {:>8.1}ms\n\
174 │ analyze: {:>8.1}ms\n\
175 │ ────────────────────────────────────────────\n\
176 │ TOTAL: {:>8.1}ms\n\
177 └─────────────────────────────────────────────────",
178 discover_ms,
179 files.len(),
180 workspaces_ms,
181 plugins_ms,
182 scripts_ms,
183 parse_ms,
184 modules.len(),
185 cache_ms,
186 entry_points_ms,
187 entry_points.len(),
188 resolve_ms,
189 graph_ms,
190 analyze_ms,
191 total_ms,
192 );
193
194 Ok(result)
195}
196
197fn run_plugins(
199 config: &ResolvedConfig,
200 files: &[discover::DiscoveredFile],
201 workspaces: &[fallow_config::WorkspaceInfo],
202) -> plugins::AggregatedPluginResult {
203 let registry = plugins::PluginRegistry::new();
204 let file_paths: Vec<std::path::PathBuf> = files.iter().map(|f| f.path.clone()).collect();
205
206 let pkg_path = config.root.join("package.json");
208 let mut result = if let Ok(pkg) = PackageJson::load(&pkg_path) {
209 registry.run(&pkg, &config.root, &file_paths)
210 } else {
211 plugins::AggregatedPluginResult::default()
212 };
213
214 for ws in workspaces {
216 let ws_pkg_path = ws.root.join("package.json");
217 if let Ok(ws_pkg) = PackageJson::load(&ws_pkg_path) {
218 let ws_result = registry.run(&ws_pkg, &ws.root, &file_paths);
219
220 let ws_prefix = ws
225 .root
226 .strip_prefix(&config.root)
227 .unwrap_or(&ws.root)
228 .to_string_lossy();
229
230 for pat in &ws_result.entry_patterns {
231 result.entry_patterns.push(format!("{ws_prefix}/{pat}"));
232 }
233 for pat in &ws_result.always_used {
234 result.always_used.push(format!("{ws_prefix}/{pat}"));
235 }
236 for pat in &ws_result.discovered_always_used {
237 result
238 .discovered_always_used
239 .push(format!("{ws_prefix}/{pat}"));
240 }
241 for (file_pat, exports) in &ws_result.used_exports {
242 result
243 .used_exports
244 .push((format!("{ws_prefix}/{file_pat}"), exports.clone()));
245 }
246 for plugin_name in &ws_result.active_plugins {
248 if !result.active_plugins.contains(plugin_name) {
249 result.active_plugins.push(plugin_name.clone());
250 }
251 }
252 result
254 .referenced_dependencies
255 .extend(ws_result.referenced_dependencies);
256 result.setup_files.extend(ws_result.setup_files);
257 result
258 .tooling_dependencies
259 .extend(ws_result.tooling_dependencies);
260 }
261 }
262
263 result
264}
265
266pub fn analyze_project(root: &Path) -> Result<AnalysisResults, FallowError> {
268 let config = default_config(root);
269 analyze(&config)
270}
271
272pub(crate) fn default_config(root: &Path) -> ResolvedConfig {
274 let user_config = fallow_config::FallowConfig::find_and_load(root)
275 .ok()
276 .flatten();
277 match user_config {
278 Some((config, _path)) => config.resolve(root.to_path_buf(), num_cpus(), false),
279 None => fallow_config::FallowConfig {
280 schema: None,
281 entry: vec![],
282 ignore: vec![],
283 detect: fallow_config::DetectConfig::default(),
284 framework: vec![],
285 workspaces: None,
286 ignore_dependencies: vec![],
287 ignore_exports: vec![],
288 output: fallow_config::OutputFormat::Human,
289 duplicates: fallow_config::DuplicatesConfig::default(),
290 rules: fallow_config::RulesConfig::default(),
291 production: false,
292 }
293 .resolve(root.to_path_buf(), num_cpus(), false),
294 }
295}
296
297fn num_cpus() -> usize {
298 std::thread::available_parallelism()
299 .map(|n| n.get())
300 .unwrap_or(4)
301}